From ea8f6bc88a4d4ed39c93f80bcfcfbab283fca230 Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:57:34 +0200 Subject: [PATCH] =?UTF-8?q?fix(accounting):=20tax=20report=20500=20on=20Po?= =?UTF-8?q?stgres=20=E2=80=94=20drop=20SQL=20date()=20from=20cost=20querie?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The #4 cost side used 'date(COALESCE(invoice_date, created_at)) BETWEEN ...' and 'date(created_at) BETWEEN ...'. The mocked unit tests never execute the SQL, so the Postgres failure (date()/COALESCE(date,timestamp)) slipped through and surfaced as a 500 on the live tax report. Replaced with plain range comparisons (col >= from AND col <= ' 23:59:59.999') — valid on both PG and SQLite, inclusive of the whole end day. Same fix applied to ledgerService buildPostings (the Treuhänder export would have 500'd identically). --- backend/src/services/ledgerService.js | 7 +++++-- backend/src/services/taxReportService.js | 9 +++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/backend/src/services/ledgerService.js b/backend/src/services/ledgerService.js index cd78d5fa..f1ac83f7 100644 --- a/backend/src/services/ledgerService.js +++ b/backend/src/services/ledgerService.js @@ -244,6 +244,9 @@ async function buildPostings({ from, to, currency } = {}) { if (!from || !to) throw httpError(400, '`from` and `to` are required (YYYY-MM-DD)', 'VALIDATION'); if (!currency) throw httpError(400, '`currency` is required', 'VALIDATION'); const cur = String(currency).toUpperCase(); + // Inclusive end-of-day bound; plain range comparison (no SQL date()) so it's + // valid on both Postgres and SQLite. + const toEnd = `${to} 23:59:59.999`; return withRetry(async () => { const cfg = await getConfig(); @@ -294,7 +297,7 @@ async function buildPostings({ from, to, currency } = {}) { .modify((q) => { if (hasCatCol) q.leftJoin('expense_categories', 'inbound_documents.category_id', 'expense_categories.id'); }) - .whereRaw('date(COALESCE(inbound_documents.invoice_date, inbound_documents.created_at)) BETWEEN ? AND ?', [from, to]) + .whereRaw('COALESCE(inbound_documents.invoice_date, inbound_documents.created_at) >= ? AND COALESCE(inbound_documents.invoice_date, inbound_documents.created_at) <= ?', [from, toEnd]) .where('inbound_documents.currency', cur) .whereNotIn('inbound_documents.status', ['declined', 'duplicate']) .orderByRaw('COALESCE(inbound_documents.invoice_date, inbound_documents.created_at) asc') @@ -333,7 +336,7 @@ async function buildPostings({ from, to, currency } = {}) { .modify((q) => { if (hasCatCol) q.leftJoin('expense_categories', 'expenses.category_id', 'expense_categories.id'); }) - .whereRaw('date(expenses.created_at) BETWEEN ? AND ?', [from, to]) + .whereRaw('expenses.created_at >= ? AND expenses.created_at <= ?', [from, toEnd]) .whereNot('expenses.status', 'declined') .whereNotIn('expenses.disposition', ['duplikat', 'abgelehnt']) .modify((q) => { if (cur !== 'CHF') q.where('expenses.original_currency', cur); }) diff --git a/backend/src/services/taxReportService.js b/backend/src/services/taxReportService.js index 7423923e..66c9428b 100644 --- a/backend/src/services/taxReportService.js +++ b/backend/src/services/taxReportService.js @@ -195,6 +195,11 @@ async function loadCosts({ from, to, cur }) { let totalNet = 0; let totalVat = 0; let totalGross = 0; + // Inclusive upper bound covering the whole `to` day. Plain range comparison + // (no SQL date() function) so it's valid on both Postgres and SQLite — the + // mocked unit tests can't catch a PG-only function error. invoice_date is a + // DATE, created_at a TIMESTAMP; both compare correctly against ISO literals. + const toEnd = `${to} 23:59:59.999`; const push = (r) => { rows.push(r); @@ -207,7 +212,7 @@ async function loadCosts({ from, to, cur }) { if (await db.schema.hasTable('inbound_documents')) { const inbound = await db('inbound_documents') .leftJoin('events', 'inbound_documents.event_id', 'events.id') - .whereRaw('date(COALESCE(inbound_documents.invoice_date, inbound_documents.created_at)) BETWEEN ? AND ?', [from, to]) + .whereRaw('COALESCE(inbound_documents.invoice_date, inbound_documents.created_at) >= ? AND COALESCE(inbound_documents.invoice_date, inbound_documents.created_at) <= ?', [from, toEnd]) .where('inbound_documents.currency', cur) .whereNotIn('inbound_documents.status', ['declined', 'duplicate']) .orderByRaw('COALESCE(inbound_documents.invoice_date, inbound_documents.created_at) asc') @@ -254,7 +259,7 @@ async function loadCosts({ from, to, cur }) { const isChf = cur === 'CHF'; const q = db('expenses') .leftJoin('events', 'expenses.event_id', 'events.id') - .whereRaw('date(expenses.created_at) BETWEEN ? AND ?', [from, to]) + .whereRaw('expenses.created_at >= ? AND expenses.created_at <= ?', [from, toEnd]) .whereNot('expenses.status', 'declined') .whereNotIn('expenses.disposition', ['duplikat', 'abgelehnt']); // CHF report includes every expense (all carry a CHF base). A