diff --git a/src/einvoice/buildEInvoiceData.ts b/src/einvoice/buildEInvoiceData.ts index 3593590..77a006b 100644 --- a/src/einvoice/buildEInvoiceData.ts +++ b/src/einvoice/buildEInvoiceData.ts @@ -269,18 +269,36 @@ function invoiceLines(lines: ComputedLine[]): UblInvoice["cac:InvoiceLine"] { }) as unknown as UblInvoice["cac:InvoiceLine"]; } +// `payableCents` is the amount actually charged (the original invoice's +// own stored `order.total`, or — for a correction — the natural total +// this same breakdown derives, see the two call sites). It can differ +// from this document's own internally-consistent TaxInclusiveAmount by a +// cent, since `order.total` comes from checkout's own independent cart/ +// discount math, not this per-rate proportional allocation — EN16931 has +// a dedicated field for exactly this gap, `cbc:PayableRoundingAmount` +// (BT-114): BR-CO-16 requires PayableAmount = TaxInclusiveAmount - +// PrepaidAmount + RoundingAmount, so the gap is declared explicitly here +// rather than either silently forcing PayableAmount to disagree with the +// actually-charged total, or forcing TaxInclusiveAmount to disagree with +// its own correctly-summed parts (caught by Mustang's Phase 4 CI check — +// BR-CO-16 flagged this as a hard error the moment PayableAmount and +// TaxInclusiveAmount didn't match to the cent, not just the pre-existing +// non-fatal "Arithmetical issue" warning this file's comments used to +// (wrongly) assume was the full story). function legalMonetaryTotal(rateGroups: RateGroup[], payableCents: number): UblInvoice["cac:LegalMonetaryTotal"] { const lineExtensionCents = rateGroups.reduce((sum, g) => sum + g.lineNetCents, 0); const allowanceTotalCents = rateGroups.reduce((sum, g) => sum + g.allowanceCents, 0); const chargeTotalCents = rateGroups.reduce((sum, g) => sum + g.chargeCents, 0); const taxExclusiveCents = lineExtensionCents - allowanceTotalCents + chargeTotalCents; const taxInclusiveCents = taxExclusiveCents + rateGroups.reduce((sum, g) => sum + g.taxCents, 0); + const roundingCents = payableCents - taxInclusiveCents; return { ...amtCents("cbc:LineExtensionAmount", lineExtensionCents), ...(allowanceTotalCents > 0 ? amtCents("cbc:AllowanceTotalAmount", allowanceTotalCents) : {}), ...(chargeTotalCents > 0 ? amtCents("cbc:ChargeTotalAmount", chargeTotalCents) : {}), ...amtCents("cbc:TaxExclusiveAmount", taxExclusiveCents), ...amtCents("cbc:TaxInclusiveAmount", taxInclusiveCents), + ...(roundingCents !== 0 ? amtCents("cbc:PayableRoundingAmount", roundingCents) : {}), ...amtCents("cbc:PayableAmount", payableCents), } as unknown as UblInvoice["cac:LegalMonetaryTotal"]; } @@ -311,12 +329,10 @@ export function buildEInvoiceData(order: InvoiceOrder, seller: InvoiceSeller): I ...(allowanceChargeEntries ? { "cac:AllowanceCharge": allowanceChargeEntries } : {}), "cac:TaxTotal": taxTotal(rateGroups), // PayableAmount is the order's own stored `total` (what the customer - // was actually charged at checkout), not a re-derivation from this - // breakdown — the two can differ by a cent from independent rounding - // paths (this file's per-rate allocation vs. checkout's own cart - // math), which Mustang surfaces as a non-fatal arithmetic *warning*, - // not an error; the legally meaningful figure is what was actually - // charged. + // was actually charged at checkout) — any cent of drift against this + // breakdown's own TaxInclusiveAmount is declared via + // PayableRoundingAmount inside legalMonetaryTotal(), not silently + // absorbed into either figure. "cac:LegalMonetaryTotal": legalMonetaryTotal(rateGroups, Math.round(order.total * 100)), "cac:InvoiceLine": invoiceLines(lines), },