From 9f8511114a78c4c6bca2666b9c59e8325fc66dfb Mon Sep 17 00:00:00 2001
From: Luca <102960244+Luca-Timo@users.noreply.github.com>
Date: Fri, 12 Jun 2026 17:43:42 +0200
Subject: [PATCH] fix(accounting): tax report degrades gracefully if cost side
fails (+ surface the error)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The cost side is supplementary — it must never 500 the core revenue report.
getTaxReport now wraps loadCosts in try/catch: on failure it returns empty
costs + a costsError string and logs the real error. The tax page shows the
revenue report plus a non-fatal amber banner with the cost-side error message,
so the actual cause is visible in the UI instead of an opaque 500.
---
backend/src/services/taxReportService.js | 20 +++++++++++++++----
.../src/pages/admin/clients/TaxReportPage.tsx | 13 ++++++++++++
frontend/src/services/taxReport.service.ts | 2 ++
3 files changed, 31 insertions(+), 4 deletions(-)
diff --git a/backend/src/services/taxReportService.js b/backend/src/services/taxReportService.js
index 66c9428b..cbbbd088 100644
--- a/backend/src/services/taxReportService.js
+++ b/backend/src/services/taxReportService.js
@@ -49,6 +49,7 @@ const REPORTABLE_STATUSES = ['sent', 'paid', 'overdue', 'pending_delivery', 'can
// D.2 — `ensureInt` consolidated into utils/numericHelpers.
const { ensureInt } = require('../utils/numericHelpers');
+const logger = require('../utils/logger');
function ensureRate(v) {
if (v === null || v === undefined || v === '') return 0;
@@ -449,10 +450,20 @@ async function getTaxReport({ from, to, currency, includeCosts = true } = {}) {
const totalsByVatRate = Array.from(byRate.values()).sort((a, b) => a.vatRate - b.vatRate);
// Cost side (Einnahmen-Ausgaben). Optional so legacy callers that
- // only want the revenue listing can opt out.
- const costs = includeCosts
- ? await loadCosts({ from, to, cur })
- : { rows: [], totalNet: 0, totalVat: 0, totalGross: 0 };
+ // only want the revenue listing can opt out. The cost side is
+ // SUPPLEMENTARY — if it fails (e.g. an accounting table/column missing
+ // on an older install) it must NOT take down the core revenue report.
+ // Degrade to empty costs + log the real error for diagnosis.
+ let costs = { rows: [], totalNet: 0, totalVat: 0, totalGross: 0 };
+ let costsError = null;
+ if (includeCosts) {
+ try {
+ costs = await loadCosts({ from, to, cur });
+ } catch (err) {
+ costsError = err.message;
+ logger.error?.(`taxReport: cost side failed (revenue still returned): ${err.message}`);
+ }
+ }
// Summary: income vs cost vs result. Result = a simplified
// Einnahmen-Ausgaben surplus (net basis); vatPayable = output VAT
@@ -478,6 +489,7 @@ async function getTaxReport({ from, to, currency, includeCosts = true } = {}) {
grandTotal,
cancelledCount,
costs,
+ costsError,
summary,
currency: cur,
period: { from, to },
diff --git a/frontend/src/pages/admin/clients/TaxReportPage.tsx b/frontend/src/pages/admin/clients/TaxReportPage.tsx
index 890980ba..afc788b1 100644
--- a/frontend/src/pages/admin/clients/TaxReportPage.tsx
+++ b/frontend/src/pages/admin/clients/TaxReportPage.tsx
@@ -358,6 +358,19 @@ export const TaxReportPage: React.FC = () => {
)}
+ {/* Non-fatal: the revenue report loaded but the cost side errored. */}
+ {report?.costsError && (
+
+
+
+
+
{t('taxReport.costsErrorTitle', 'Costs could not be loaded')}
+
{report.costsError}
+
+
+
+ )}
+
{/* Results */}
{isLoading ? (
diff --git a/frontend/src/services/taxReport.service.ts b/frontend/src/services/taxReport.service.ts
index 00c4c41a..8449e290 100644
--- a/frontend/src/services/taxReport.service.ts
+++ b/frontend/src/services/taxReport.service.ts
@@ -97,6 +97,8 @@ export interface TaxReport {
/** Cost side (#4). Present when the accounting tables exist; empty
* otherwise. */
costs: TaxReportCosts;
+ /** Non-fatal: set when the cost side failed to load (revenue still shown). */
+ costsError?: string | null;
/** Income/cost/result summary (#4). */
summary: TaxReportSummary;
currency: string;