Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ bld/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/

# vs code
.vscode/

# Visual Studio 2017 auto generated files
Generated\ Files/

Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ Todas as mudancas notaveis deste projeto serao documentadas neste arquivo.
O formato e baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/),
e este projeto adere ao [Versionamento Semantico](https://semver.org/lang/pt-BR/).

## [3.3.1] - 2026-07-28 — Decimal null-safe na deserializacao

### Corrigido

- **Decimal deserialization**: Criado `FlexibleDecimalConverter` /
`FlexibleNullableDecimalConverter` para aceitar `null` em campos
`decimal` non-nullable retornados pela API Asaas (ex.:
`Invoice.deductions` apos `POST /v3/invoices` com status `SCHEDULED`).
Antes, a deserializacao lancava `JsonException: The JSON value could
not be converted to System.Decimal`. `null` e tratado como `0`.

## [3.3.0] - 2026-06-26 — Onboarding de subcontas (white-label/BaaS)

Ajustes para o fluxo de criação de subcontas via API e suporte a testes contra
Expand Down
22 changes: 22 additions & 0 deletions Codout.Apis.Asaas.Tests/Contract/InvoiceContractTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,28 @@ public void InvoiceResponse_DeserializesFromOfficialFixture()
Assert.NotNull(result.Taxes);
}

[Fact]
public void InvoiceResponse_DeserializesWhenDeductionsIsNull()
{
// Regressao: a API Asaas retorna deductions=null em NFSe SCHEDULED
// (ex.: apos POST /v3/invoices). Sem FlexibleDecimalConverter o
// System.Text.Json lanca JsonException em decimal non-nullable.
var json = FixtureLoader.Load("Invoice/invoice-response-null-deductions.json");

var result = JsonContractAssert.DeserializeFixture<Invoice>(json);

Assert.Equal("inv_000000000001", result.Id);
Assert.Equal(InvoiceStatus.SCHEDULED, result.Status);
Assert.Equal("pay_000000000001", result.PaymentId);
Assert.Equal(100.00m, result.Value);
Assert.Equal(0m, result.Deductions);
Assert.Equal(new DateTime(2026, 1, 15), result.EffectiveDate);
Assert.Equal("TEST-1", result.ExternalReference);
Assert.Equal("100001", result.MunicipalServiceId);
Assert.NotNull(result.Taxes);
Assert.Equal("1.0101.11.00", result.Taxes.NbsCode);
}

[Fact]
public void TaxesResponse_HasAllReformaTributariaFields()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"object": "invoice",
"id": "inv_000000000001",
"status": "SCHEDULED",
"customer": "cus_000000000001",
"type": "NFS-e",
"statusDescription": null,
"serviceDescription": "SERVICO DE TESTE: R$ 100,00\nTOTAL: R$ 100,00",
"pdfUrl": null,
"xmlUrl": null,
"rpsSerie": null,
"rpsNumber": null,
"number": null,
"validationCode": null,
"value": 100.00,
"deductions": null,
"effectiveDate": "2026-01-15",
"observations": "Referente a fatura TEST-1 (REF00001).",
"estimatedTaxesDescription": null,
"payment": "pay_000000000001",
"installment": null,
"externalReference": "TEST-1",
"taxes": {
"retainIss": false,
"iss": 0.0,
"pisCofinsRetentionType": null,
"pisCofinsTaxStatus": null,
"pis": 0.0,
"cofins": 0.0,
"operationPis": null,
"operationCofins": null,
"csll": 0.0,
"inss": 0.0,
"ir": 0.0,
"nbsCode": "1.0101.11.00",
"taxSituationCode": null,
"taxClassificationCode": null,
"operationIndicatorCode": null,
"stateIbs": 0.0,
"municipalIbsValue": 0.0,
"municipalIbs": 0.0,
"stateIbsValue": 0.0,
"cbs": 0.0,
"cbsValue": 0.0
},
"municipalServiceId": "100001",
"municipalServiceCode": null,
"municipalServiceName": "Analise e desenvolvimento de sistemas"
}
6 changes: 5 additions & 1 deletion Codout.Apis.Asaas/Codout.Apis.Asaas.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<!-- NuGet Package -->
<PackageId>Asaas.Api</PackageId>
<Version>3.3.0</Version>
<Version>3.3.1</Version>
<Authors>Clovis Coli Jr</Authors>
<Company>Codout</Company>
<Description>SDK .NET no-oficial para integracao com a API v3 do Asaas (asaas.com). Suporta cobrancas (Boleto, Pix, Cartao), assinaturas, transferencias, links de pagamento, notas fiscais, antecipacoes, negativacoes e muito mais. Zero dependencias externas.</Description>
Expand All @@ -18,6 +18,10 @@
<RepositoryUrl>https://github.com/codout/Codout.Apis.Asaas</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageReleaseNotes>
v3.3.1 — Decimal null-safe:
- FlexibleDecimalConverter: deductions (e demais decimal) null da API
nao quebram mais a deserializacao (null → 0).

v3.3.0 — Onboarding de subcontas (white-label/BaaS):
- CreateAccountRequest: + IncomeValue (obrigatorio no Asaas desde 2024) e
BirthDate (yyyy-MM-dd, PF).
Expand Down
65 changes: 65 additions & 0 deletions Codout.Apis.Asaas/Core/FlexibleDecimalConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Codout.Apis.Asaas.Core;

/// <summary>
/// Handles non-nullable <see cref="decimal"/> fields when the Asaas API returns
/// <c>null</c> (ex.: Invoice.deductions). Null is treated as <c>0</c>.
/// Also accepts numeric strings.
/// </summary>
internal sealed class FlexibleDecimalConverter : JsonConverter<decimal>
{
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
return 0m;

if (reader.TokenType == JsonTokenType.Number)
return reader.GetDecimal();

if (reader.TokenType == JsonTokenType.String)
{
var value = reader.GetString();
if (string.IsNullOrWhiteSpace(value))
return 0m;

if (decimal.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out var parsed))
return parsed;

throw new JsonException($"Unable to convert \"{value}\" to Decimal.");
}

throw new JsonException($"Unexpected token {reader.TokenType} when parsing Decimal.");
}

public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
=> writer.WriteNumberValue(value);
}

/// <summary>
/// Handles nullable <see cref="decimal"/> fields, preserving <c>null</c> when
/// the API omits the value.
/// </summary>
internal sealed class FlexibleNullableDecimalConverter : JsonConverter<decimal?>
{
private static readonly FlexibleDecimalConverter Inner = new();

public override decimal? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
return null;

return Inner.Read(ref reader, typeof(decimal), options);
}

public override void Write(Utf8JsonWriter writer, decimal? value, JsonSerializerOptions options)
{
if (value is null)
writer.WriteNullValue();
else
Inner.Write(writer, value.Value, options);
}
}
2 changes: 2 additions & 0 deletions Codout.Apis.Asaas/Core/JsonSerializerConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ private static JsonSerializerOptions CreateOptions()
options.Converters.Add(new SafeEnumConverterFactory());
options.Converters.Add(new FlexibleDateTimeConverter());
options.Converters.Add(new FlexibleNullableDateTimeConverter());
options.Converters.Add(new FlexibleDecimalConverter());
options.Converters.Add(new FlexibleNullableDecimalConverter());

return options;
}
Expand Down