fix(accounting): tax report 500 on Postgres — drop SQL date() from cost queries

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 <= '<to> 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).
This commit is contained in:
Luca
2026-06-12 16:57:34 +02:00
parent 9514f5cb8e
commit ea8f6bc88a
2 changed files with 12 additions and 4 deletions
+5 -2
View File
@@ -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); })
+7 -2
View File
@@ -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