From d88200f39873a33e12945d8a8e4075f4237a9d08 Mon Sep 17 00:00:00 2001 From: PJ Date: Sun, 31 May 2026 18:56:30 +0530 Subject: [PATCH] fix(folio): add parseTypedAmount helper matching app's parseCents Raw user input like "50" must become 5000 cents, not 50. The existing parseDollarCents helper strips non-digits and so reads "50" as 50 cents, which is correct for formatted balance text but off by 100x for raw input from the amount field. --- examples/folio/sanderling/predicates.ts | 16 +++++++ pkg/spec/test/folio-typed-amount.test.ts | 56 ++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 pkg/spec/test/folio-typed-amount.test.ts diff --git a/examples/folio/sanderling/predicates.ts b/examples/folio/sanderling/predicates.ts index 9ab381f..be5ef28 100644 --- a/examples/folio/sanderling/predicates.ts +++ b/examples/folio/sanderling/predicates.ts @@ -1,3 +1,19 @@ +// Parses raw user input in the transaction amount field into integer cents. +// Mirrors the Folio app's parseCents: whole numbers like "50" become 5000 +// cents, decimals like "5.50" become 550, more than 2 decimals or non-numeric +// input return 0. Leading +/- signs are tolerated and treated as positive. +export function parseTypedAmount(text: string | undefined | null): number { + if (!text) return 0; + let trimmed = text.trim().replace(/,/g, ""); + if (trimmed.startsWith("+") || trimmed.startsWith("-")) trimmed = trimmed.slice(1); + if (!/^\d+(\.\d{1,2})?$/.test(trimmed)) return 0; + const dot = trimmed.indexOf("."); + const whole = dot < 0 ? trimmed : trimmed.slice(0, dot); + const frac = dot < 0 ? "" : trimmed.slice(dot + 1); + const fracPadded = (frac + "00").slice(0, 2); + return parseInt(whole, 10) * 100 + parseInt(fracPadded, 10); +} + // When the last action is a tap (or double-tap) on the transaction Submit // button, the absolute change in total balance must equal the amount the // user typed. A double-submit lands two transactions and shifts the balance diff --git a/pkg/spec/test/folio-typed-amount.test.ts b/pkg/spec/test/folio-typed-amount.test.ts new file mode 100644 index 0000000..d0a387a --- /dev/null +++ b/pkg/spec/test/folio-typed-amount.test.ts @@ -0,0 +1,56 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { parseTypedAmount } from "../../../examples/folio/sanderling/predicates.ts"; + +test("empty string returns 0", () => { + assert.equal(parseTypedAmount(""), 0); +}); + +test("undefined returns 0", () => { + assert.equal(parseTypedAmount(undefined), 0); +}); + +test("null returns 0", () => { + assert.equal(parseTypedAmount(null), 0); +}); + +test("whole number 50 parses as 5000 cents", () => { + assert.equal(parseTypedAmount("50"), 5000); +}); + +test("decimal 5.50 parses as 550 cents", () => { + assert.equal(parseTypedAmount("5.50"), 550); +}); + +test("decimal 5.05 parses as 505 cents", () => { + assert.equal(parseTypedAmount("5.05"), 505); +}); + +test("one-decimal 0.5 parses as 50 cents", () => { + assert.equal(parseTypedAmount("0.5"), 50); +}); + +test("non-numeric returns 0", () => { + assert.equal(parseTypedAmount("abc"), 0); +}); + +test("too many decimals returns 0", () => { + assert.equal(parseTypedAmount("5.123"), 0); +}); + +test("zero returns 0", () => { + assert.equal(parseTypedAmount("0"), 0); +}); + +test("leading plus sign tolerated as positive", () => { + assert.equal(parseTypedAmount("+50"), 5000); +}); + +test("leading minus sign tolerated as positive", () => { + assert.equal(parseTypedAmount("-50"), 5000); +}); + +test("comma-separated thousands accepted", () => { + assert.equal(parseTypedAmount("1,234.56"), 123456); +});