From b2144b3c76658e5cbe247ed86baebe8fc6b5f4f6 Mon Sep 17 00:00:00 2001 From: Charan Rathore <180254320+charan-rathore@users.noreply.github.com> Date: Tue, 6 Oct 2026 02:11:39 +0530 Subject: [PATCH] fix: accept a byte order mark before the transaction CSV header Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com> --- apps/server/src/engine/finance.ts | 2 ++ tests/finance.test.ts | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/apps/server/src/engine/finance.ts b/apps/server/src/engine/finance.ts index b42d1fac3..f7704ee7c 100644 --- a/apps/server/src/engine/finance.ts +++ b/apps/server/src/engine/finance.ts @@ -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 = "", diff --git a/tests/finance.test.ts b/tests/finance.test.ts index 94ae732d8..e26e1214e 100644 --- a/tests/finance.test.ts +++ b/tests/finance.test.ts @@ -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); + } +});