From fbbbb8ab7335f07ecf48e217632c7011bd7c88cd Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:02:59 +0200 Subject: [PATCH] feat(accounting): VAT registration/reclaim settings + un-gated VAT-codes read Slice 1 of the VAT consolidation backend: - PUT /admin/settings/accounting accepts accounting_vat_registered (bool) + accounting_vat_reclaim_countries (ISO-2 list); GET /:type already returns them parsed, so no GET change needed. - New read-only GET /api/admin/vat-codes (adminAuth, NOT accounting-gated) so the invoice/quote editors can populate their VAT dropdown even when the accounting layer is off. Management CRUD stays under /admin/ledger. --- backend/server.js | 3 +++ backend/src/routes/adminSettings.js | 23 ++++++++++++++++++++ backend/src/routes/adminVatCodes.js | 33 +++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 backend/src/routes/adminVatCodes.js diff --git a/backend/server.js b/backend/server.js index b1ddcf70..1f1e0966 100644 --- a/backend/server.js +++ b/backend/server.js @@ -709,6 +709,9 @@ app.use('/api/admin/deals', require('./src/routes/adminDeals')); app.use('/api/admin/tax-report', require('./src/routes/adminTaxReport')); app.use('/api/admin/expenses', require('./src/routes/adminExpenses')); app.use('/api/admin/ledger', require('./src/routes/adminLedger')); +// Read-only VAT-code registry for the invoice/quote editors — un-gated by the +// accounting flag (management stays under /ledger). +app.use('/api/admin/vat-codes', require('./src/routes/adminVatCodes')); app.use('/api/admin/system-health', require('./src/routes/adminSystemHealth')); app.use('/api/admin/dev', require('./src/routes/adminDev')); app.use('/api/public/quotes', require('./src/routes/publicQuotes')); diff --git a/backend/src/routes/adminSettings.js b/backend/src/routes/adminSettings.js index 674c7835..43670c08 100644 --- a/backend/src/routes/adminSettings.js +++ b/backend/src/routes/adminSettings.js @@ -255,6 +255,29 @@ router.put('/accounting', adminAuth, requirePermission('settings.edit'), async ( setting_type: 'accounting', }); } + // VAT registration + reclaim. `registered` drives whether output VAT applies + // + whether input VAT is deductible; `reclaim_countries` = the ISO-2 list of + // countries whose input VAT can be reclaimed (drives cost tax-treatment + + // the report's VAT-payable). + if (Object.prototype.hasOwnProperty.call(req.body, 'accounting_vat_registered')) { + updates.push({ + setting_key: 'accounting_vat_registered', + setting_value: JSON.stringify(!!req.body.accounting_vat_registered), + setting_type: 'accounting', + }); + } + if (Object.prototype.hasOwnProperty.call(req.body, 'accounting_vat_reclaim_countries')) { + const arr = Array.isArray(req.body.accounting_vat_reclaim_countries) + ? req.body.accounting_vat_reclaim_countries + .map((c) => String(c || '').toUpperCase().trim()) + .filter((c) => /^[A-Z]{2}$/.test(c)) + : []; + updates.push({ + setting_key: 'accounting_vat_reclaim_countries', + setting_value: JSON.stringify(arr), + setting_type: 'accounting', + }); + } for (const u of updates) { const existing = await db('app_settings').where('setting_key', u.setting_key).first(); if (existing) { diff --git a/backend/src/routes/adminVatCodes.js b/backend/src/routes/adminVatCodes.js new file mode 100644 index 00000000..d513bc8b --- /dev/null +++ b/backend/src/routes/adminVatCodes.js @@ -0,0 +1,33 @@ +/** + * Read-only VAT-code registry for the invoice / quote editors. + * + * Mounted at /api/admin/vat-codes. UN-gated by the `accounting` flag on purpose: + * invoices need their preset VAT codes even when the accounting layer is off, so + * the editor must be able to read the list regardless. Any authenticated admin + * may read the (innocuous) tax-code list. MANAGEMENT (create/update/delete) stays + * in the accounting-gated /api/admin/ledger routes — this is read-only. + * + * GET / [?direction=output|input] → { items: [{ id, code, name, rate, direction }] } + */ +const express = require('express'); +const { adminAuth } = require('../middleware/auth'); +const { handleAsync, successResponse } = require('../utils/routeHelpers'); +const ledgerService = require('../services/ledgerService'); + +const router = express.Router(); + +router.get('/', adminAuth, handleAsync(async (req, res) => { + const items = await ledgerService.listVatCodes(); + const active = items.filter((v) => v.active !== false); + const { direction } = req.query; + const filtered = (direction === 'output' || direction === 'input') + ? active.filter((v) => v.direction === direction) + : active; + return successResponse(res, { + items: filtered.map((v) => ({ + id: v.id, code: v.code, name: v.name, rate: Number(v.rate) || 0, direction: v.direction, + })), + }); +})); + +module.exports = router;