fix(vat): enforce decimal vat_rate on supplier invoice items and normalize MCP percent extraction (#1049)
Supplier invoice items store vat_rate as a decimal fraction (0.25) while customer invoices use integer percent (25). The shared Zod schema accepted 0-100, so a percent-shaped vat_rate silently booked 2500 % VAT via line_total * vat_rate, and the MCP inbox-conversion path staged the AI extraction's percent-integer vatRate straight into the decimal column with per-line vat_amount 0. Part of #310. - CreateSupplierInvoiceItemSchema.vat_rate is now a literal union of the statutory decimal set (0, 0.06, 0.12, 0.25) with a unit-hint error, covering the cookie route, the invoice-inbox convert route, and /api/v1 (whose runtime ALLOWED_SV_VAT_RATES guard stays as defense in depth). - New shared normalizeVatRateToDecimal() in lib/vat: percent-shaped values (25, 12, 6) divide by 100, results snap to the legal Swedish set, and anything else (foreign 19/20, non-finite) maps to 0. - gnubok_create_supplier_invoice_from_inbox normalizes vatRate at the extraction boundary and derives per-line vat_amount when the extraction carries none, so the staged header vat_amount is honest. - The pending-operation executor normalizes staged vat_rate on insert, so rows staged before this fix cannot book percent-scaled VAT. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
03fd1b60b7
commit
5b8e3fa130
@@ -19,6 +19,7 @@ import { bulkBookMatchedInboxItems, categorizeMatchedTransaction } from '@/lib/t
|
||||
import { getVatRules, getAvailableVatRates } from '@/lib/invoices/vat-rules'
|
||||
import { fetchExchangeRate, convertToSEK } from '@/lib/currency/riksbanken'
|
||||
import { validateVatNumber } from '@/lib/vat/vies-client'
|
||||
import { normalizeVatRateToDecimal } from '@/lib/vat/supplier-invoice-line-checks'
|
||||
import {
|
||||
createInvoicePaymentJournalEntry,
|
||||
createInvoiceCashEntry,
|
||||
@@ -2276,7 +2277,10 @@ async function commitCreateSupplierInvoiceFromInbox(
|
||||
// 20-24 / 48 instead of double-counting input VAT into 2641. Tampered
|
||||
// params can't smuggle non-zero VAT into the items table.
|
||||
const itemInserts = rawItems.map((item, idx) => {
|
||||
const vatRate = reverseCharge ? 0 : (typeof item.vat_rate === 'number' && Number.isFinite(item.vat_rate) ? item.vat_rate : 0)
|
||||
// Normalize percent-shaped rates (25 -> 0.25) and snap to the statutory
|
||||
// set: rows staged before the issue #310 fix (or tampered params) carry
|
||||
// percent integers, and inserting one books 2500 % VAT downstream.
|
||||
const vatRate = reverseCharge ? 0 : (typeof item.vat_rate === 'number' ? normalizeVatRateToDecimal(item.vat_rate) : 0)
|
||||
const vatAmt = reverseCharge ? 0 : (typeof item.vat_amount === 'number' && Number.isFinite(item.vat_amount) ? item.vat_amount : 0)
|
||||
return {
|
||||
supplier_invoice_id: invoice.id,
|
||||
|
||||
Reference in New Issue
Block a user