From d9f0a25a0f9c3bf6274fde8d1865a176ddbc8cf3 Mon Sep 17 00:00:00 2001 From: "@tanya_r" Date: Sun, 14 Jun 2026 08:06:14 -0300 Subject: [PATCH] feat(ledger): serialize api money amounts as decimal strings Money amounts were serialized as JSON numbers, which JS and other JSON clients truncate to float64, losing precision for large or high-scale NUMERIC(38,18) values. Add MoneyAmountSerializer writing BigDecimal via toPlainString (exact digits, preserved scale, no scientific notation) and apply it to BalanceResponse.amount with @Schema(type=string) so the generated OpenAPI matches. Input is unaffected; default deserialization already accepts string and number. Closes #113 --- .../api/dto/response/BalanceResponse.kt | 5 ++++ .../serialization/MoneyAmountSerializer.kt | 19 ++++++++++++ .../ledger/api/AccountControllerTest.kt | 2 +- .../MoneyAmountSerializerTest.kt | 29 +++++++++++++++++++ 4 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 services/ledger/src/main/kotlin/com/fincore/ledger/api/serialization/MoneyAmountSerializer.kt create mode 100644 services/ledger/src/test/kotlin/com/fincore/ledger/api/serialization/MoneyAmountSerializerTest.kt diff --git a/services/ledger/src/main/kotlin/com/fincore/ledger/api/dto/response/BalanceResponse.kt b/services/ledger/src/main/kotlin/com/fincore/ledger/api/dto/response/BalanceResponse.kt index c2faa0b..1af739c 100644 --- a/services/ledger/src/main/kotlin/com/fincore/ledger/api/dto/response/BalanceResponse.kt +++ b/services/ledger/src/main/kotlin/com/fincore/ledger/api/dto/response/BalanceResponse.kt @@ -3,12 +3,17 @@ package com.fincore.ledger.api.dto.response +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.fincore.ledger.api.serialization.MoneyAmountSerializer +import io.swagger.v3.oas.annotations.media.Schema import java.math.BigDecimal import java.time.Instant data class BalanceResponse( val accountId: String, val currency: String, + @JsonSerialize(using = MoneyAmountSerializer::class) + @Schema(type = "string", format = "decimal", example = "100.00") val amount: BigDecimal, val lastPostedAt: Instant?, ) diff --git a/services/ledger/src/main/kotlin/com/fincore/ledger/api/serialization/MoneyAmountSerializer.kt b/services/ledger/src/main/kotlin/com/fincore/ledger/api/serialization/MoneyAmountSerializer.kt new file mode 100644 index 0000000..205a2f6 --- /dev/null +++ b/services/ledger/src/main/kotlin/com/fincore/ledger/api/serialization/MoneyAmountSerializer.kt @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: BUSL-1.1 +// SPDX-FileCopyrightText: 2026 FinCore Engine Authors + +package com.fincore.ledger.api.serialization + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.JsonSerializer +import com.fasterxml.jackson.databind.SerializerProvider +import java.math.BigDecimal + +class MoneyAmountSerializer : JsonSerializer() { + override fun serialize( + value: BigDecimal, + gen: JsonGenerator, + serializers: SerializerProvider, + ) { + gen.writeString(value.toPlainString()) + } +} diff --git a/services/ledger/src/test/kotlin/com/fincore/ledger/api/AccountControllerTest.kt b/services/ledger/src/test/kotlin/com/fincore/ledger/api/AccountControllerTest.kt index 6739d7b..db8b7b4 100644 --- a/services/ledger/src/test/kotlin/com/fincore/ledger/api/AccountControllerTest.kt +++ b/services/ledger/src/test/kotlin/com/fincore/ledger/api/AccountControllerTest.kt @@ -131,7 +131,7 @@ class AccountControllerTest( .andExpect(status().isOk) .andExpect(jsonPath("$.accountId").value(id.toString())) .andExpect(jsonPath("$.currency").value("EUR")) - .andExpect(jsonPath("$.amount").value(100.00)) + .andExpect(jsonPath("$.amount").value("100.00")) } @Test diff --git a/services/ledger/src/test/kotlin/com/fincore/ledger/api/serialization/MoneyAmountSerializerTest.kt b/services/ledger/src/test/kotlin/com/fincore/ledger/api/serialization/MoneyAmountSerializerTest.kt new file mode 100644 index 0000000..bc5b13e --- /dev/null +++ b/services/ledger/src/test/kotlin/com/fincore/ledger/api/serialization/MoneyAmountSerializerTest.kt @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BUSL-1.1 +// SPDX-FileCopyrightText: 2026 FinCore Engine Authors + +package com.fincore.ledger.api.serialization + +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test +import java.math.BigDecimal + +class MoneyAmountSerializerTest { + private data class Holder( + @JsonSerialize(using = MoneyAmountSerializer::class) val amount: BigDecimal, + ) + + private val mapper = jacksonObjectMapper() + + @Test + fun `should serialize amount as a json string preserving trailing zeros`() { + mapper.writeValueAsString(Holder(BigDecimal("100.00"))) shouldBe """{"amount":"100.00"}""" + } + + @Test + fun `should serialize a high scale amount losslessly without scientific notation`() { + val value = "12345678901234567890.123456789012345678" + mapper.writeValueAsString(Holder(BigDecimal(value))) shouldBe """{"amount":"$value"}""" + } +}