From 5b52969e36a41bbc08a98e0bca1ce0e937d77f56 Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Mon, 15 Jun 2026 23:57:23 +0200 Subject: [PATCH] feat(accounting): snapshot the chosen VAT code on quote/invoice create + storno MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires the vat_code snapshot (migration 130) through the write paths: quote create/update, the main invoice create, and the Storno carry-over (so a cancellation exports the same code as the invoice it reverses). Guarded with hasColumnCached; reads payload.vatCode (sent by the editor dropdown, coming in a later slice — inert until then, falls back to the rate→code map). 72 tests pass. --- backend/src/services/invoiceService.js | 10 ++++++++++ backend/src/services/quoteService.js | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/backend/src/services/invoiceService.js b/backend/src/services/invoiceService.js index 4f708097..3248fad3 100644 --- a/backend/src/services/invoiceService.js +++ b/backend/src/services/invoiceService.js @@ -43,6 +43,7 @@ function getHierarchyHelpers() { // D.2 — `ensureInt` + `ensureNumber` consolidated into utils/numericHelpers. const { ensureInt, ensureNumber } = require('../utils/numericHelpers'); +const { hasColumnCached } = require('../utils/schemaCache'); function formatNumberInTemplate(format, year, seq) { return format @@ -955,6 +956,11 @@ async function createInvoice(payload, adminId, trx = db) { created_at: new Date(), updated_at: new Date(), }; + // Migration 130 — snapshot the chosen output VAT code (immutable; the + // accounting export emits exactly this rather than re-deriving from the map). + if (payload.vatCode !== undefined && await hasColumnCached('invoices', 'vat_code')) { + row.vat_code = payload.vatCode ? String(payload.vatCode).slice(0, 16) : null; + } const inserted = await trx('invoices').insert(row).returning('id'); const invoiceId = typeof inserted[0] === 'object' ? inserted[0].id : inserted[0]; @@ -2212,6 +2218,10 @@ async function createStorno(originalId, adminId, trx = db) { currency: original.currency, language: original.language, vat_rate: original.vat_rate, + // Migration 130 — carry the original's VAT-code snapshot onto the Storno so + // both documents export the same code. Conditional spread = safe on pre-130 + // DBs (undefined → omitted). + ...(original.vat_code ? { vat_code: original.vat_code } : {}), shipping_amount_minor: -ensureInt(original.shipping_amount_minor || 0), net_amount_minor: -ensureInt(original.net_amount_minor), vat_amount_minor: -ensureInt(original.vat_amount_minor), diff --git a/backend/src/services/quoteService.js b/backend/src/services/quoteService.js index 514864fa..c93abffc 100644 --- a/backend/src/services/quoteService.js +++ b/backend/src/services/quoteService.js @@ -558,6 +558,11 @@ async function createQuote(payload, adminId) { if (payload.projectId !== undefined && await hasColumnCached('quotes', 'project_id')) { row.project_id = payload.projectId || null; } + // Migration 130 — snapshot the chosen output VAT code (immutable; the export + // emits exactly this rather than re-deriving from the mutable rate→code map). + if (payload.vatCode !== undefined && await hasColumnCached('quotes', 'vat_code')) { + row.vat_code = payload.vatCode ? String(payload.vatCode).slice(0, 16) : null; + } const inserted = await trx('quotes').insert(row).returning('id'); const quoteId = typeof inserted[0] === 'object' ? inserted[0].id : inserted[0]; @@ -682,6 +687,10 @@ async function updateQuote(id, payload, adminId) { if (Object.prototype.hasOwnProperty.call(payload, 'projectId') && await hasColumnCached('quotes', 'project_id')) { updates.project_id = payload.projectId || null; } + // Migration 130 — VAT code snapshot. + if (Object.prototype.hasOwnProperty.call(payload, 'vatCode') && await hasColumnCached('quotes', 'vat_code')) { + updates.vat_code = payload.vatCode ? String(payload.vatCode).slice(0, 16) : null; + } await trx('quotes').where({ id }).update(updates); // When linked to a project, cascade across the deal lineage so the linked