From df5b58ccf360a90dd4f34cd33462ccdadff92b54 Mon Sep 17 00:00:00 2001 From: Mason Wheeler Date: Wed, 25 Mar 2026 12:56:58 -0700 Subject: [PATCH 1/2] Fix floating point precision in dollar-to-cents conversion $17.60 * 100 = 1760.0000000000002 in IEEE 754, which the API rejects as "invalid type: floating point, expected i64" when deserializing max_price_per_node_hour. Fix: wrap all dollar * 100 conversions with Math.round() to ensure integer cents: - priceWholeToCents() number path - priceWholeToCents() string path - dollarsToCents() (was Math.ceil which also overcharges by 1 cent) Added 3 regression tests covering known problematic prices ($17.60, $12.50, $9.99, etc.) that verify cents are always integers. Fixes: "Failed to deserialize the JSON body" error when creating spot nodes with certain price values. --- src/helpers/test/units.test.ts | 63 ++++++++++++++++++++++++++++++++-- src/helpers/units.ts | 6 ++-- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/src/helpers/test/units.test.ts b/src/helpers/test/units.test.ts index 591db31..f745f0e 100644 --- a/src/helpers/test/units.test.ts +++ b/src/helpers/test/units.test.ts @@ -2,6 +2,7 @@ import { expect, test } from "vitest"; import { type Cents, centsToDollarsFormatted, + dollarsToCents, priceWholeToCents, } from "../units.ts"; @@ -22,7 +23,7 @@ test("price whole to cents", () => { ["$1.000", 100], ["$1.23", 123], - ["$1.234", 123.4], + ["$1.234", 123], // formatted as numbers ["0", 0], @@ -31,7 +32,7 @@ test("price whole to cents", () => { ["100", 100_00], ["1.23", 123], - ["1.234", 123.4], + ["1.234", 123], // nested quotes (double) ['"$0"', 0], @@ -91,3 +92,61 @@ test("cents to dollars formatted", () => { expect(result).toEqual(expected); } }); + +test("dollarsToCents returns integer cents without floating point errors", () => { + // These prices are known to produce floating point precision errors + // e.g. 17.60 * 100 = 1760.0000000000002 in IEEE 754 + const cases: [number, number][] = [ + [17.60, 1760], + [12.50, 1250], + [9.99, 999], + [0.10, 10], + [3.33, 333], + [7.77, 777], + [11.11, 1111], + [20.00, 2000], + [0.01, 1], + [99.99, 9999], + ]; + + for (const [dollars, expectedCents] of cases) { + const cents = dollarsToCents(dollars); + expect(cents).toBe(expectedCents); + expect(Number.isInteger(cents)).toBe(true); + } +}); + +test("priceWholeToCents returns integer cents for prices with floating point issues", () => { + // Regression test: $17.60 was sent to API as 1760.0000000000002 + // which failed deserialization as i64 + const cases: [string, number][] = [ + ["$17.60", 1760], + ["17.60", 1760], + ["$12.50", 1250], + ["$9.99", 999], + ["$0.10", 10], + ["$20.00", 2000], + ]; + + for (const [input, expectedCents] of cases) { + const { cents, invalid } = priceWholeToCents(input); + expect(invalid).toBe(false); + expect(cents).toBe(expectedCents); + expect(Number.isInteger(cents)).toBe(true); + } +}); + +test("priceWholeToCents handles numeric input without floating point errors", () => { + const cases: [number, number][] = [ + [17.60, 1760], + [12.50, 1250], + [9.99, 999], + ]; + + for (const [input, expectedCents] of cases) { + const { cents, invalid } = priceWholeToCents(input); + expect(invalid).toBe(false); + expect(cents).toBe(expectedCents); + expect(Number.isInteger(cents)).toBe(true); + } +}); diff --git a/src/helpers/units.ts b/src/helpers/units.ts index 48f2906..98fee6a 100644 --- a/src/helpers/units.ts +++ b/src/helpers/units.ts @@ -87,7 +87,7 @@ export function priceWholeToCents( return { cents: null, invalid: true }; } - return { cents: price * 100, invalid: false }; + return { cents: Math.round(price * 100), invalid: false }; } else if (typeof price === "string") { // remove any whitespace, dollar signs, negative signs, single and double quotes const priceCleaned = price.replace(/[\s$\-'"]/g, ""); @@ -97,7 +97,7 @@ export function priceWholeToCents( const parsedPrice = Number.parseFloat(priceCleaned); - return { cents: parsedPrice * 100, invalid: false }; + return { cents: Math.round(parsedPrice * 100), invalid: false }; } // default invalid @@ -113,7 +113,7 @@ export function centsToDollars(cents: Cents): number { } export function dollarsToCents(dollars: number): Cents { - return Math.ceil(dollars * 100); + return Math.round(dollars * 100); } export function parseStartDateOrNow(startDate?: string): Date | "NOW" { if (!startDate) return "NOW"; From 91ca9ab1c13004df1840635c5cf52bc658f8bf7e Mon Sep 17 00:00:00 2001 From: Mason Wheeler Date: Wed, 25 Mar 2026 13:03:34 -0700 Subject: [PATCH 2/2] Fix Biome formatting: remove trailing zeros from number literals --- src/helpers/test/units.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/helpers/test/units.test.ts b/src/helpers/test/units.test.ts index f745f0e..4b3bb2f 100644 --- a/src/helpers/test/units.test.ts +++ b/src/helpers/test/units.test.ts @@ -97,14 +97,14 @@ test("dollarsToCents returns integer cents without floating point errors", () => // These prices are known to produce floating point precision errors // e.g. 17.60 * 100 = 1760.0000000000002 in IEEE 754 const cases: [number, number][] = [ - [17.60, 1760], - [12.50, 1250], + [17.6, 1760], + [12.5, 1250], [9.99, 999], - [0.10, 10], + [0.1, 10], [3.33, 333], [7.77, 777], [11.11, 1111], - [20.00, 2000], + [20.0, 2000], [0.01, 1], [99.99, 9999], ]; @@ -138,8 +138,8 @@ test("priceWholeToCents returns integer cents for prices with floating point iss test("priceWholeToCents handles numeric input without floating point errors", () => { const cases: [number, number][] = [ - [17.60, 1760], - [12.50, 1250], + [17.6, 1760], + [12.5, 1250], [9.99, 999], ];