diff --git a/lib/money.ts b/lib/money.ts index 01e4511..9c68729 100644 --- a/lib/money.ts +++ b/lib/money.ts @@ -4,7 +4,13 @@ export function dollarsToCents(value: unknown): number | null { if (value === "" || value == null) return null; if (typeof value === "number") { if (!Number.isFinite(value) || value <= 0) return null; - return Math.round(value * 100); + // Reject sub-cent precision, exactly as the string path does via MONEY_RE's + // `\.\d{1,2}`. Without this a bare number like 1.234 silently rounds to 123, + // letting a partial dollar amount slip through the number path while the + // equivalent "$1.234" string is rejected. + const cents = value * 100; + if (Math.abs(cents - Math.round(cents)) > 1e-6) return null; + return Math.round(cents); } const text = String(value).trim().replace(/\s+/g, ""); diff --git a/tests/money.test.mjs b/tests/money.test.mjs index e12f083..3ba0895 100644 --- a/tests/money.test.mjs +++ b/tests/money.test.mjs @@ -23,3 +23,15 @@ test("dollarsToCents handles numeric values without accepting non-finite numbers assert.equal(dollarsToCents(Number.NaN), null); assert.equal(dollarsToCents(Number.POSITIVE_INFINITY), null); }); + +test("dollarsToCents rejects partial dollar amounts passed as numbers", () => { + // The number path must reject sub-cent precision just like the "$1.234" + // string case above — otherwise numeric JSON bodies bypass the guard. + assert.equal(dollarsToCents(1.234), null); + assert.equal(dollarsToCents(5.005), null); + assert.equal(dollarsToCents(0.999), null); + // Whole-cent numbers still parse, including exact two-decimal values. + assert.equal(dollarsToCents(1), 100); + assert.equal(dollarsToCents(1.99), 199); + assert.equal(dollarsToCents(1234.56), 123456); +});