From 7a5cfba483d6fa5f3f0b1602fb57912945e7863c Mon Sep 17 00:00:00 2001 From: aiirvizionz Date: Sun, 19 Jul 2026 22:46:45 -0600 Subject: [PATCH] fix(auctions): reject partial dollar amounts --- app/api/auctions/route.ts | 6 ++---- app/api/bids/route.ts | 5 ++--- lib/money.ts | 16 ++++++++++++++++ tests/money.test.mjs | 25 +++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 lib/money.ts create mode 100644 tests/money.test.mjs diff --git a/app/api/auctions/route.ts b/app/api/auctions/route.ts index 3330974..fe06690 100644 --- a/app/api/auctions/route.ts +++ b/app/api/auctions/route.ts @@ -10,6 +10,7 @@ import { highBid, acceptBid, } from "@/lib/db"; +import { dollarsToCents as parseDollarCents } from "@/lib/money"; import { fireDomainEvent } from "@/lib/webhooks"; export const runtime = "nodejs"; @@ -27,10 +28,7 @@ async function resolveAccountId(req: NextRequest): Promise { /** Dollars → integer cents; "" / null / 0 → null (clears the field). */ function dollarsToCents(v: unknown): number | null { - if (v === "" || v == null) return null; - const n = typeof v === "number" ? v : parseFloat(String(v).replace(/[$,\s]/g, "")); - if (!isFinite(n) || n <= 0) return null; - return Math.round(n * 100); + return parseDollarCents(v); } // GET /api/auctions?dn= — public status; owner also gets reserve + the bid list. diff --git a/app/api/bids/route.ts b/app/api/bids/route.ts index f90a090..a3935e5 100644 --- a/app/api/bids/route.ts +++ b/app/api/bids/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { readSession, SESSION_COOKIE } from "@/lib/session"; import { safeDomain } from "@/lib/config"; import { addBid, getAuction, highBid } from "@/lib/db"; +import { dollarsToCents as parseDollarCents } from "@/lib/money"; import { fireDomainEvent } from "@/lib/webhooks"; export const runtime = "nodejs"; @@ -11,9 +12,7 @@ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; /** Dollars string/number → integer cents, or null if not a positive amount. */ function dollarsToCents(v: unknown): number | null { - const n = typeof v === "number" ? v : parseFloat(String(v ?? "").replace(/[$,\s]/g, "")); - if (!isFinite(n) || n <= 0) return null; - return Math.round(n * 100); + return parseDollarCents(v); } // GET /api/bids?dn= — public auction status (never leaks the reserve amount). diff --git a/lib/money.ts b/lib/money.ts new file mode 100644 index 0000000..01e4511 --- /dev/null +++ b/lib/money.ts @@ -0,0 +1,16 @@ +const MONEY_RE = /^\$?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d{1,2})?$/; + +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); + } + + const text = String(value).trim().replace(/\s+/g, ""); + if (!MONEY_RE.test(text)) return null; + + const amount = Number(text.replace(/[$,]/g, "")); + if (!Number.isFinite(amount) || amount <= 0) return null; + return Math.round(amount * 100); +} diff --git a/tests/money.test.mjs b/tests/money.test.mjs new file mode 100644 index 0000000..e12f083 --- /dev/null +++ b/tests/money.test.mjs @@ -0,0 +1,25 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { dollarsToCents } from "../lib/money.ts"; + +test("dollarsToCents accepts ordinary dollar strings", () => { + assert.equal(dollarsToCents("1"), 100); + assert.equal(dollarsToCents("$1.25"), 125); + assert.equal(dollarsToCents("$1,234.56"), 123456); + assert.equal(dollarsToCents(" $ 10.50 "), 1050); +}); + +test("dollarsToCents rejects partial or non-decimal strings", () => { + assert.equal(dollarsToCents("1abc"), null); + assert.equal(dollarsToCents("1e2"), null); + assert.equal(dollarsToCents("0x10"), null); + assert.equal(dollarsToCents("$1.234"), null); + assert.equal(dollarsToCents("0"), null); +}); + +test("dollarsToCents handles numeric values without accepting non-finite numbers", () => { + assert.equal(dollarsToCents(2.5), 250); + assert.equal(dollarsToCents(Number.NaN), null); + assert.equal(dollarsToCents(Number.POSITIVE_INFINITY), null); +});