From f2d735d8c3e6d31a0d4953a3b1a86d5e0d6a7392 Mon Sep 17 00:00:00 2001 From: Param Harrison Date: Fri, 9 Oct 2026 00:18:44 +0300 Subject: [PATCH 1/3] factory: claim #146 by runner-43664-1791494324862 From 4df2f595bbd34609ffad9fb6a5f506feb77f309d Mon Sep 17 00:00:00 2001 From: Param Harrison Date: Fri, 9 Oct 2026 00:22:48 +0300 Subject: [PATCH 2/3] factory: build #146 --- src/csv.ts | 12 ++++------- tests/csv.test.ts | 50 ++++++++++++++++++++++++++++++++++++++++++++ tests/routes.test.ts | 10 +++++++++ 3 files changed, 64 insertions(+), 8 deletions(-) create mode 100644 tests/csv.test.ts diff --git a/src/csv.ts b/src/csv.ts index bbb9f3a..b86cfb4 100644 --- a/src/csv.ts +++ b/src/csv.ts @@ -1,4 +1,5 @@ import type { Database } from "bun:sqlite"; +import { formatCents } from "./money/cents.ts"; interface ExpenseRow { createdAt: string; @@ -26,19 +27,14 @@ function fetchExpenseRows(db: Database, groupId: string): ExpenseRow[] { } /** - * Build the CSV export for a group's expenses. - * - * SEEDED DEFECT (issue #3): the amount column prints the raw integer cents - * (e.g. "1000") instead of a formatted amount ("10.00"). Baseline tests - * only check the header row and the row count, not the amount column's - * formatting, so this ships undetected. The fix should reuse - * `formatCents` from src/money (see .claude/skills/handling-money). + * Build the CSV export for a group's expenses. Amounts are stored as integer + * cents and formatted to a 2-decimal string here, at the output boundary. */ export function exportCsv(db: Database, groupId: string): string { const rows = fetchExpenseRows(db, groupId); const header = "date,payer,description,amount\n"; const lines = rows.map( - (r) => `${r.createdAt},${csvEscape(r.payerName)},${csvEscape(r.description)},${r.amountCents}`, + (r) => `${r.createdAt},${csvEscape(r.payerName)},${csvEscape(r.description)},${formatCents(r.amountCents)}`, ); return header + lines.join("\n") + (lines.length > 0 ? "\n" : ""); } diff --git a/tests/csv.test.ts b/tests/csv.test.ts new file mode 100644 index 0000000..368d5fd --- /dev/null +++ b/tests/csv.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, test } from "bun:test"; +import { Database } from "bun:sqlite"; +import { initSchema } from "../src/db.ts"; +import { insertMember } from "../src/auth/tokens.ts"; +import { createExpense } from "../src/expenses.ts"; +import { exportCsv } from "../src/csv.ts"; + +function setupDb(amounts: number[]) { + const db = new Database(":memory:"); + initSchema(db); + db.query("INSERT INTO groups (id, name) VALUES (?, ?)").run("g1", "Test group"); + insertMember(db, { id: "m1", groupId: "g1", name: "Asha", token: "t1", role: "admin" }); + amounts.forEach((amountCents, i) => { + createExpense(db, { + groupId: "g1", + payerId: "m1", + amountCents, + description: `Expense ${i}`, + splitAmong: ["m1"], + createdAt: new Date(Date.parse("2026-09-01T09:00:00.000Z") + i * 3_600_000).toISOString(), + }); + }); + return db; +} + +function dataRows(csv: string): string[] { + return csv.trim().split("\n").slice(1); +} + +describe("exportCsv", () => { + test("exportCsv formats the amount column as a decimal string (1000 -> 10.00)", () => { + const db = setupDb([1000]); + const [row] = dataRows(exportCsv(db, "g1")); + expect(row?.split(",").at(-1)).toBe("10.00"); + }); + + test("exportCsv keeps non-zero cents (1005 -> 10.05)", () => { + const db = setupDb([1005]); + const [row] = dataRows(exportCsv(db, "g1")); + expect(row?.split(",").at(-1)).toBe("10.05"); + }); + + test("exportCsv keeps the header and one row per expense", () => { + const db = setupDb([1000, 1005, 7]); + const lines = exportCsv(db, "g1").trim().split("\n"); + expect(lines[0]).toBe("date,payer,description,amount"); + expect(lines.length).toBe(4); + expect(lines.slice(1).map((l) => l.split(",").at(-1))).toEqual(["10.00", "10.05", "0.07"]); + }); +}); diff --git a/tests/routes.test.ts b/tests/routes.test.ts index 25cac54..546dc1f 100644 --- a/tests/routes.test.ts +++ b/tests/routes.test.ts @@ -138,4 +138,14 @@ describe("GET /api/groups/:id/export.csv", () => { expect(lines[0]).toBe("date,payer,description,amount"); expect(lines.length).toBe(4); // header + 3 seeded Flat 4B expenses }); + + test("amount column is formatted, not raw cents", async () => { + const app = setupApp(); + const res = await app.request("/api/groups/flat-4b/export.csv", { headers: auth("flat-priya") }); + const rows = (await res.text()).trim().split("\n").slice(1); + expect(rows.map((r) => r.split(",").at(-1))).toEqual(["45.00", "12.00", "30.00"]); + for (const row of rows) { + expect(row).not.toMatch(/,\d+$/); + } + }); }); From 385336980b4b76a10e4043c19c292b4f5edcb5d1 Mon Sep 17 00:00:00 2001 From: Param Harrison Date: Fri, 9 Oct 2026 00:27:54 +0300 Subject: [PATCH 3/3] factory: build #146 --- tests/csv.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/csv.test.ts b/tests/csv.test.ts index 368d5fd..a372f06 100644 --- a/tests/csv.test.ts +++ b/tests/csv.test.ts @@ -40,6 +40,12 @@ describe("exportCsv", () => { expect(row?.split(",").at(-1)).toBe("10.05"); }); + test("exportCsv pads an amount under one dollar (5 -> 0.05)", () => { + const db = setupDb([5]); + const [row] = dataRows(exportCsv(db, "g1")); + expect(row?.split(",").at(-1)).toBe("0.05"); + }); + test("exportCsv keeps the header and one row per expense", () => { const db = setupDb([1000, 1005, 7]); const lines = exportCsv(db, "g1").trim().split("\n");