Dentolize · ZATCA E-Invoice Rounding Fix Walkthrough
On this pageWhat to verifyThe core assertionHighest‑value test: the multi‑line driftTest matrixConsistency checks within one e‑invoiceExplicitly unchanged — assert these did NOT moveEdge cases and known boundariesWhere to look in codeBuild / smokeSandbox note

For Quality

What to verify

The invariant under test: the total reported to ZATCA equals the invoice's stored/displayed total, and the e‑invoice is internally consistent. Focus on the aggregation the fix changed — document‑level tax total, per‑category subtotals, and payable amount.

The core assertion

For any invoice:

ZATCA TaxInclusiveAmount == ZATCA PayableAmount == Invoice.total
ZATCA document TaxAmount   == Invoice.tax
Σ per-category TaxSubtotal.TaxAmount == ZATCA document TaxAmount

The document tax for each category must equal round(rate × Σ taxable_base) — a single rounding — not Σ round(rate × baseᵢ).

Highest‑value test: the multi‑line drift

Reproduce the PR's own case and assert the corrected result:

  • Input: two lines, 15% VAT, taxable base 100.10 each (Σ = 200.20).
  • Expected (new): VAT 30.03, total 230.23 — matching the app.
  • Guard against regression: the old code produced 30.02 / 230.22. A test asserting 30.03 fails on the old algorithm and passes on the new one.

Vary the fractional cents so the per‑line rounding drifts up in some cases and down in others (the real bug was 2999.99 → 3000.00, a drift up; the example above drifts down). Both must now match the invoice total.

Test matrix

DimensionCases to cover
Line count1 line (must be unchanged), 2 lines, many lines
VAT rate15% only, 5% only, mixed 15% + 5% in one invoice, zero‑rated/exempt lines present
Fractional centsbases chosen so per‑line VAT rounds up; and so it rounds down
Discountsno discount; line discounts; discount not evenly divisible by quantity (see edge below)
Insurancepatient (B2C simplified) and insurance (B2B standard) invoices
Quantityquantity = 1; quantity > 1

Consistency checks within one e‑invoice

  • Document cac:TaxTotal TaxAmount == sum of cac:TaxSubtotal TaxAmount values (for standard‑rated categories).
  • LegalMonetaryTotal.TaxInclusiveAmount == LegalMonetaryTotal.PayableAmount.
  • TaxInclusiveAmount == TaxExclusiveAmount + document TaxAmount.
  • The 15% subtotal's TaxAmount == round(0.15 × its TaxableAmount); likewise 5%.

Explicitly unchanged — assert these did NOT move

  • Per‑line cac:TaxTotal amounts. Line‑level tax is still round(line_extension_amount × VAT_percent). A test that pins line‑level values protects against accidental change.
  • Zero‑rated / exempt handling (vat_category codes O/Z/E) — separate code path, should be untouched.
  • Line extension amounts and price/allowance‑charge fields.

Edge cases and known boundaries

  • Single‑line invoice: old and new formulas are identical — verify no diff.
  • Per‑unit discount rounding (out of scope, but worth a probe): a line whose total discount doesn't divide evenly by quantity can shift the taxable base by a cent via money(op.discount / op.amount) in invoiceCalculator.js:313. If a residual mismatch appears after this fix, this is the prime suspect — file it as the documented follow‑up, not as a regression of this PR.
  • Float precision: because monetary fields are Float, expect artifacts like 15.014999…; assertions should compare rounded 2‑dp values, and test data should include values that expose float boundaries.

Where to look in code

  • Changed logic: packages/zatca/src/zatca/calc.tsconstructTaxTotal (:203), category VAT computation (:344‑364), Calc wiring of LegalMonetaryTotal (:556‑564).
  • Compiled mirror that runtime uses: packages/zatca/lib/zatca/calc.js — confirm it reflects the source (the PR commits it).
  • App‑side source of truth to compare against: invoiceUtils.js calculateInvoiceValues (:163, :205).

Build / smoke

  • packages/zatca must compile cleanly (tsc), and lib/ must be regenerated so the server (main: lib/index.js) picks up the change — verify the committed lib/zatca/calc.js matches the source rather than trusting the .ts alone.

Sandbox note

The walkthrough sandbox company is not onboarded to ZATCA (isConfigured false; env not production), so invoices show "E‑Invoice Not Reported" and nothing is transmitted. End‑to‑end verification against real ZATCA XML needs an onboarded/compliance environment; unit‑level verification of the calculation does not and is the more reliable gate here.