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.
This commit is contained in:
Luca
2026-06-16 00:02:59 +02:00
parent 5b52969e36
commit fbbbb8ab73
3 changed files with 59 additions and 0 deletions
+3
View File
@@ -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'));
+23
View File
@@ -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) {
+33
View File
@@ -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;