From 212ade630877a078a8842400bd709523a8083a1f Mon Sep 17 00:00:00 2001 From: luis8quali Date: Mon, 27 Jul 2026 23:53:46 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20desserializa=C3=A7=C3=A3o=20de=20decima?= =?UTF-8?q?is=20null-safe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adiciona suporte para deserialização de campos decimal não nulos que podem ser nulos na API Asaas. Implementa `FlexibleDecimalConverter` e `FlexibleNullableDecimalConverter` para tratar `null` como `0`. Inclui testes para garantir a funcionalidade. Atualiza CHANGELOG e versão do pacote para 3.3.1. --- .gitignore | 3 + CHANGELOG.md | 11 ++++ .../Contract/InvoiceContractTests.cs | 22 +++++++ .../invoice-response-null-deductions.json | 49 ++++++++++++++ Codout.Apis.Asaas/Codout.Apis.Asaas.csproj | 6 +- .../Core/FlexibleDecimalConverter.cs | 65 +++++++++++++++++++ .../Core/JsonSerializerConfiguration.cs | 2 + 7 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 Codout.Apis.Asaas.Tests/Fixtures/Invoice/invoice-response-null-deductions.json create mode 100644 Codout.Apis.Asaas/Core/FlexibleDecimalConverter.cs diff --git a/.gitignore b/.gitignore index 9b09c42..82838e6 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 571eb4e..11ebd11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Codout.Apis.Asaas.Tests/Contract/InvoiceContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/InvoiceContractTests.cs index db11483..1a15cb4 100644 --- a/Codout.Apis.Asaas.Tests/Contract/InvoiceContractTests.cs +++ b/Codout.Apis.Asaas.Tests/Contract/InvoiceContractTests.cs @@ -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(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() { diff --git a/Codout.Apis.Asaas.Tests/Fixtures/Invoice/invoice-response-null-deductions.json b/Codout.Apis.Asaas.Tests/Fixtures/Invoice/invoice-response-null-deductions.json new file mode 100644 index 0000000..8618b59 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/Invoice/invoice-response-null-deductions.json @@ -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" +} diff --git a/Codout.Apis.Asaas/Codout.Apis.Asaas.csproj b/Codout.Apis.Asaas/Codout.Apis.Asaas.csproj index f815f80..b1fb0c0 100644 --- a/Codout.Apis.Asaas/Codout.Apis.Asaas.csproj +++ b/Codout.Apis.Asaas/Codout.Apis.Asaas.csproj @@ -6,7 +6,7 @@ Asaas.Api - 3.3.0 + 3.3.1 Clovis Coli Jr Codout 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. @@ -18,6 +18,10 @@ https://github.com/codout/Codout.Apis.Asaas git +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). diff --git a/Codout.Apis.Asaas/Core/FlexibleDecimalConverter.cs b/Codout.Apis.Asaas/Core/FlexibleDecimalConverter.cs new file mode 100644 index 0000000..43592e0 --- /dev/null +++ b/Codout.Apis.Asaas/Core/FlexibleDecimalConverter.cs @@ -0,0 +1,65 @@ +using System; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Codout.Apis.Asaas.Core; + +/// +/// Handles non-nullable fields when the Asaas API returns +/// null (ex.: Invoice.deductions). Null is treated as 0. +/// Also accepts numeric strings. +/// +internal sealed class FlexibleDecimalConverter : JsonConverter +{ + 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); +} + +/// +/// Handles nullable fields, preserving null when +/// the API omits the value. +/// +internal sealed class FlexibleNullableDecimalConverter : JsonConverter +{ + 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); + } +} diff --git a/Codout.Apis.Asaas/Core/JsonSerializerConfiguration.cs b/Codout.Apis.Asaas/Core/JsonSerializerConfiguration.cs index f9704fb..e9d6e65 100644 --- a/Codout.Apis.Asaas/Core/JsonSerializerConfiguration.cs +++ b/Codout.Apis.Asaas/Core/JsonSerializerConfiguration.cs @@ -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; }