Fix BR-CO-16: declare the PayableAmount/order.total gap via PayableRoundingAmount
Validate e-invoices / mustang (push) Successful in 18s
Validate e-invoices / mustang (push) Successful in 18s
The 1-cent drift between this file's per-rate breakdown and the original invoice's independently-stored order.total (checkout's own cart math) turned out to be a hard schematron error (BR-CO-16), not just the non-fatal "Arithmetical issue" warning it looked like from the log alone. EN16931 has a field for exactly this gap, cbc:PayableRoundingAmount (BT-114) — using it keeps both PayableAmount (the actually-charged total) and TaxInclusiveAmount (this breakdown's own correctly-summed total) intact and reconciling, verified by hand before pushing.
This commit is contained in:
@@ -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),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user