From 2e2964c1f3cc85f7ffe10a4fe913f0af41085b8f Mon Sep 17 00:00:00 2001 From: Abhi Reddy Date: Wed, 23 Sep 2026 00:12:39 -0400 Subject: [PATCH] Fix payments sorting lexicographically instead of numerically by amount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sortPayments compared String(amount).localeCompare(String(amount)) instead of the numbers themselves, so a shorter amount with a higher leading digit (e.g. 994) ranked above a longer, larger amount (e.g. 9873) — visible on /payments and GET /api/payments with sort=amount. Every other localeCompare in the codebase sorts ISO date strings, which is safe (fixed-width, lexicographic order matches chronological order); this was the only place applying string comparison to a number. analytics.ts already sorts numerically elsewhere (b.volume - a.volume) — this fix matches that existing pattern instead of introducing a new one. --- .../merchant-console/src/data/queries.test.ts | 41 +++++++++++++++++++ .../merchant-console/src/data/queries.ts | 3 +- 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 build-battle/merchant-console/src/data/queries.test.ts diff --git a/build-battle/merchant-console/src/data/queries.test.ts b/build-battle/merchant-console/src/data/queries.test.ts new file mode 100644 index 00000000..59d7f632 --- /dev/null +++ b/build-battle/merchant-console/src/data/queries.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest" +import { Payment } from "./types" +import { sortPayments } from "./queries" + +/** + * Amount is a number, not a padded string like createdAt — sorting it with + * localeCompare was a lexicographic bug hiding behind dates that happened to + * sort correctly the same way. This pins numeric order specifically. + */ + +function payment(overrides: Partial): Payment { + return { + id: "pay_0000", + merchantId: "mch_01", + amount: 0, + currency: "USD", + status: "captured", + method: "card", + cardBrand: "visa", + last4: "4242", + createdAt: "2026-03-14T10:15:00.000Z", + description: "Order", + ...overrides, + } +} + +describe("sortPayments", () => { + it("orders amounts numerically, not lexicographically", () => { + // Lexicographic comparison would rank 994 above 9873, since "994" > + // "9873" character-by-character (position 1: '9' vs '8'). Numeric order + // puts the larger amount first. + const small = payment({ id: "pay_small", amount: 994 }) + const large = payment({ id: "pay_large", amount: 9873 }) + + const desc = sortPayments([small, large], "amount", "desc") + expect(desc.map((p) => p.id)).toEqual(["pay_large", "pay_small"]) + + const asc = sortPayments([small, large], "amount", "asc") + expect(asc.map((p) => p.id)).toEqual(["pay_small", "pay_large"]) + }) +}) diff --git a/build-battle/merchant-console/src/data/queries.ts b/build-battle/merchant-console/src/data/queries.ts index cc4ca009..78d933db 100644 --- a/build-battle/merchant-console/src/data/queries.ts +++ b/build-battle/merchant-console/src/data/queries.ts @@ -77,8 +77,7 @@ export function sortPayments( const factor = direction === "asc" ? 1 : -1 return [...payments].sort((a, b) => { if (sort === "amount") { - // Sort by the formatted amount so the order matches what the table shows. - return String(a.amount).localeCompare(String(b.amount)) * factor + return (a.amount - b.amount) * factor } return a.createdAt.localeCompare(b.createdAt) * factor })