From c72aa18a33854cceffe8db62ca1c5f037f3a3cd5 Mon Sep 17 00:00:00 2001 From: Dread Date: Tue, 14 Jul 2026 22:03:40 -0700 Subject: [PATCH 1/2] fix(cashout): decimal-safe JMD rate on the ACTUAL JMDAmount used by cashout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to #449. There are two `JMDAmount` classes in the tree: `src/domain/shared/MoneyAmount.ts` (fixed in #449) and `src/domain/shared/money/JMDAmount.ts`. The barrel `domain/shared/index.ts` exports JMDAmount from `./money` (`export * from "./money"`, taking only USDTAmount from MoneyAmount.ts), so cashout — via `@domain/shared` — imports the `money/JMDAmount.ts` copy, which still did `new JMDAmount(BigInt(d) * 100n)` and threw on every fractional NCB rate. #449's conversion fix therefore had no effect on cashout (its error-map half did work). Applied the same decimal-safe rewrite (mirror `money/USDAmount.dollars`) to `money/JMDAmount.ts`. Verified: dollars(155.5)=155.50, 152.7=152.70, 0.5=0.50. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/domain/shared/money/JMDAmount.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/domain/shared/money/JMDAmount.ts b/src/domain/shared/money/JMDAmount.ts index 87331aeb1..232515bbb 100644 --- a/src/domain/shared/money/JMDAmount.ts +++ b/src/domain/shared/money/JMDAmount.ts @@ -1,6 +1,6 @@ import { getCurrencyMajorExponent } from "@domain/fiat/display-currency" -import Money from "../bigint-money" +import Money, { Round } from "../bigint-money" import { BigIntConversionError } from "../errors" import { WalletCurrency } from "../primitives" @@ -26,7 +26,13 @@ export class JMDAmount extends MoneyAmount { static dollars(d: number): JMDAmount | BigIntConversionError { try { - return new JMDAmount(BigInt(d) * 100n) + // Mirror USDAmount.dollars: use the Money lib for a decimal-safe conversion. + // BigInt(d) throws on any fractional rate (e.g. an NCB rate of 155.5), which + // is what the exchange-rate value virtually always is. + const dollarAmt = new Money(d.toString(), "JMDollars", Round.HALF_TO_EVEN) + const cents = JMDAmount.cents("100") + if (cents instanceof BigIntConversionError) return cents // should never happen + return new JMDAmount(cents.money.multiply(dollarAmt).toFixed(2)) } catch (error) { return new BigIntConversionError( error instanceof Error ? error.message : String(error), From 3ef6afece757725c2dd069b563300a24c705c992 Mon Sep 17 00:00:00 2001 From: Dread Date: Tue, 14 Jul 2026 22:23:33 -0700 Subject: [PATCH 2/2] test(cashout): regression test for fractional JMD rates [ENG-509] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a fractional-rate case to Money.spec.ts, which imports JMDAmount from @domain/shared — the exact barrel-resolved class the cashout path uses. The only pre-existing JMDAmount.dollars test used an integer (160), so the suite stayed green while every fractional NCB rate crashed cashout in prod. Verified: fails without the fix (BigIntConversionError on 155.5), passes with it; full suite 22/22, tsc --noEmit clean. --- test/flash/unit/domain/shared/Money.spec.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/flash/unit/domain/shared/Money.spec.ts b/test/flash/unit/domain/shared/Money.spec.ts index fd4ce1a5e..349ae7c70 100644 --- a/test/flash/unit/domain/shared/Money.spec.ts +++ b/test/flash/unit/domain/shared/Money.spec.ts @@ -32,6 +32,19 @@ describe("Money Amount", () => { const jmdprice = usdAmount.convertAtRate(rate) expect(jmdprice.asDollars()).toBe("16000.00") }) + + // Regression: the live NCB cashout rate is virtually always fractional + // (e.g. 155.5, 152.7). The old JMDAmount.dollars did `BigInt(d) * 100n`, + // which threw on any non-integer, breaking every JMD cashout offer. + // This imports JMDAmount from @domain/shared — the exact barrel-resolved + // class the cashout path uses (see ErpNext.getCashoutExchangeRate). + it("accepts fractional JMD rates (regression: NCB rate 155.5)", () => { + const amt = JMDAmount.dollars(155.5) + if (amt instanceof Error) throw amt + expect(amt.asDollars()).toBe("155.50") + expect(JMDAmount.dollars(152.7)).not.toBeInstanceOf(Error) + expect(JMDAmount.dollars(0.5)).not.toBeInstanceOf(Error) + }) }) describe("USD Amount", () => {