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>