Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/server/src/engine/finance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { z } from "zod";
/** CSV amounts use positive expenses and negative income. No currency conversion is inferred. */
export function analyzeSpending(csv: string) {
if (csv.length > 500000) throw new Error("Import at most 500 KB of transaction CSV");
// Excel and many bank exports start a UTF-8 CSV with a byte order mark. It is not part of the first header.
if (csv.charCodeAt(0) === 0xfeff) csv = csv.slice(1);
const rows: string[][] = [];
let row: string[] = [],
cell = "",
Expand Down
11 changes: 11 additions & 0 deletions tests/finance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,14 @@ test("transaction imports preserve valid quoted fields at CSV boundaries", () =>
assert.equal(report.transactions[1].category, "Uncategorized");
}
});

test("transaction imports accept a UTF-8 byte order mark before the header", () => {
for (const header of [
"date,description,amount,category",
'"date","description","amount","category"',
]) {
const report = analyzeSpending(`\uFEFF${header}\n2026-09-01,Coffee,10.10,Food`);
assert.equal(report.count, 1);
assert.equal(report.spending, 10.1);
}
});
Loading