From 0c0fb29770d7559b8b35a1b2a0aae1315485d875 Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Mon, 15 Jun 2026 23:01:20 +0200 Subject: [PATCH] fix(accounting): emit ISO dates in exports (Postgres returns Date objects) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Date column imported empty into Banana because dateOnly() did String(d).slice(0,10) — on Postgres the date columns come back as JS Date objects, so that yields "Thu Jan 15" instead of "2026-01-15", which Banana rejects. (SQLite returns strings, so the tests never caught it — the pg-date-serialisation trap.) - ledgerService.dateOnly + taxReportService CSV now format Date objects to yyyy-mm-dd via local calendar parts (DATE columns are local-midnight). - Regression test added with a real Date object (the existing tests all used string dates). --- backend/__tests__/services/ledgerService.test.js | 14 ++++++++++++++ backend/src/services/ledgerService.js | 15 ++++++++++++++- backend/src/services/taxReportService.js | 12 +++++++++++- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/backend/__tests__/services/ledgerService.test.js b/backend/__tests__/services/ledgerService.test.js index b282f5a9..edd84dd0 100644 --- a/backend/__tests__/services/ledgerService.test.js +++ b/backend/__tests__/services/ledgerService.test.js @@ -221,6 +221,20 @@ describe('exportPostings', () => { expect(contentType).toMatch(/text\/plain/); }); + it('formats a Postgres Date object as yyyy-mm-dd (not "Thu Jan ...")', async () => { + // PG returns DATE columns as JS Date objects (SQLite returns strings); the + // export must still emit an ISO date, or Banana rejects it and the Date + // column imports empty. + invoiceRows = [{ + id: 1, invoice_number: 'R-2026-0001', issue_date: new Date(2026, 0, 10), + vat_rate: 8.1, net_amount_minor: 10000, vat_amount_minor: 810, total_amount_minor: 10810, + customer_company_name: 'ACME', + }]; + const { content } = await ledgerService.exportPostings({ ...period, format: 'banana' }); + const dateCell = content.split('\r\n')[1].split('\t')[0]; + expect(dateCell).toBe('2026-01-10'); + }); + it('bexio format includes tax_code + currency', async () => { const { content } = await ledgerService.exportPostings({ ...period, format: 'bexio' }); const header = content.split('\r\n')[0]; diff --git a/backend/src/services/ledgerService.js b/backend/src/services/ledgerService.js index 6c570fb9..2d589ff0 100644 --- a/backend/src/services/ledgerService.js +++ b/backend/src/services/ledgerService.js @@ -390,7 +390,20 @@ function csvEscape(cell) { return `"${s.replace(/"/g, '""')}"`; } function minorToDecimal(m) { return ((Number(m) || 0) / 100).toFixed(2); } -function dateOnly(d) { return String(d || '').slice(0, 10); } +// yyyy-mm-dd, robust to Postgres returning DATE/TIMESTAMP columns as JS Date +// objects (SQLite returns strings). String(dateObj).slice(0,10) yields +// "Thu Jan 15", which Banana / accounting tools reject — so format the calendar +// parts explicitly. Uses local parts (DATE columns come back at local midnight). +function dateOnly(d) { + if (!d) return ''; + if (d instanceof Date) { + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + return `${y}-${m}-${day}`; + } + return String(d).slice(0, 10); +} const EXPORT_FORMATS = ['generic', 'banana', 'banana_ie', 'bexio']; diff --git a/backend/src/services/taxReportService.js b/backend/src/services/taxReportService.js index d8baf12f..ebeebe00 100644 --- a/backend/src/services/taxReportService.js +++ b/backend/src/services/taxReportService.js @@ -978,6 +978,16 @@ async function renderTaxReportCsv({ from, to, currency, locale } = {}) { const minorToDotDecimal = (m) => ((Number(m) || 0) / 100).toFixed(2); + // yyyy-mm-dd, robust to Postgres returning dates as JS Date objects (SQLite + // returns strings) — raw String(dateObj) is "Thu Jan 15", not an ISO date. + const isoDate = (d) => { + if (!d) return ''; + if (d instanceof Date) { + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; + } + return String(d).slice(0, 10); + }; + const typeLabelKey = (type) => ( type === 'outgoing' ? 'tax_type_outgoing' : type === 'incoming' ? 'tax_type_incoming' @@ -1013,7 +1023,7 @@ async function renderTaxReportCsv({ from, to, currency, locale } = {}) { lines.push([ i + 1, t(useLocale, typeLabelKey(row.type)), - row.date, + isoDate(row.date), reference, row.party, row.eventName,