InvoiceOrder/CorrectionInvoiceOrder gained an optional vatExempt flag
— items already carry 0% rate and de-grossed prices when true (the
caller's responsibility), this only controls display/XML mapping:
- Visual PDF: replaces the "enthält X% MwSt." annotation with
"Steuerfreie innergemeinschaftliche Lieferung (§4 Nr. 1b UStG)" on
both the invoice and its Storno/Gutschrift.
- EN16931 XML (buildEInvoiceData.ts): VAT category 'K' instead of 'S'
on every line/tax-subtotal, plus BT-120/BT-121 exemption reason
(VATEX-EU-IC) — but only on cac:TaxTotal's own TaxSubtotal.TaxCategory,
not on InvoiceLine/AllowanceCharge's TaxCategory, which
@e-invoice-eu/core's schema rejects those same two fields on (caught
locally before Mustang ever saw it — a fully-rendered PDF/A-3 with a
hand-inflated CII XML confirmed CategoryCode/ExemptionReasonCode/
ExemptionReason land correctly).
Also surfaced (not fixed, unrelated to this change): a seller with no
iban configured throws in @e-invoice-eu/core's UBL→CII conversion —
doesn't affect this shop's own production data (iban is always set),
noting for whoever touches this next.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Netto/MwSt. rows were computed per tax rate with shipping and
discount already proportionally folded in (computeTaxBreakdown's
scale factor — tax-correct, since ancillary costs are apportioned
across rates), but Rabatt/Versand were ALSO shown as their own
separate rows on top. Adding up the visible rows never actually
matched the printed Gesamt — off by exactly the shipping (and any
discount) amount. Confirmed against a real production order
(#EP-0006-ZDX6's Stornorechnung).
Restructured both templates into a genuinely additive chain
(Zwischensumme → Rabatt → Versand → Gesamt, using the raw order
fields directly) with the per-rate tax now shown as an "enthält X%
MwSt." annotation below Gesamt — informational, not part of the sum.
Mirrors the pattern the website's own VatBreakdown component already
uses on /cart and /checkout.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- The "Bereits beglichen" confirmation is now plain green Text, not a
tinted pill/box — read as too heavy for a status note.
- Item table rows no longer alternate white/tinted (zebra striping);
every row now shares the same tinted background for a more uniform
table, on both the invoice and its Storno/Gutschrift corrections.
- InvoiceOrder/CorrectionInvoiceOrder gained optional companyName/
vatId, rendered in the "An" recipient block when present — the
buyer-side counterpart to the sellers' own fields already shown in
the footer. Both consuming repos need their own commit to actually
pass these through from Orders.companyName/vatId.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The previous placement put it as one more row inside summaryBox, right after Gesamt — the user wanted it visually below/outside the tinted card entirely. Now a sibling of summaryBox instead of a child, verified by rendering to PDF.
- Footer is centered, not left-aligned.
- Summary rows are grouped into visual clusters (Rabatt+Versand; each rate's own Netto/MwSt pair) with a bit of spacing between clusters, tight spacing within — makes it clearer which lines belong together.
- More space between the product image and the article text in each line item.
- "Bereits beglichen" badge moved from the top meta row to directly below "Gesamt", where it reads more naturally against the amount it confirms.
- Packstation number and Postnummer render as two separate lines instead of one "Packstation X · Postnummer Y" line.
Applied identically to both invoicePdf.tsx and correctionInvoicePdf.tsx (Stornorechnung/Gutschrift) so every generated invoice stays visually consistent — verified by rendering both to PDF and comparing.
The integer-cents pipeline rewrite (97a7f6a) dropped the /(1+rate/100) degross step when allocating discountAmount/shippingCost per rate group -- allowance/charge amounts came out ~19%/7% too large. Mustang's BR-CO-* checks couldn't catch this since they only verify internal self-consistency, not agreement with the original order data -- surfaced instead via an external e-Rechnung checker on a real order, where the resulting PayableRoundingAmount was >1 EUR instead of a normal few-cents residual. Fixed; the same real order's numbers now reconcile to within 1 cent of order.total.
Also fixes BR-CO-25 (a positive Amount due needs a due date or payment terms): original invoices paid immediately at checkout (Kreditkarte/PayPal, same isPaidImmediately() definition the visual PDF's "Bereits beglichen" badge uses) now declare PrepaidAmount = the full charged total, making PayableAmount 0 -- matching what the visual PDF already told a human reader, now machine-readable too. Bank-transfer originals and all correction invoices (Storno/Gutschrift, which represent a refund owed rather than an amount due) get an explicit PaymentTerms note instead, since this shop has no due-date policy modeled to cite.
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.
The previous fix (net not gross line amounts + AllowanceCharge entries) still drifted a cent on BR-CO-12/13/14: rounding a float sum once at the end doesn't equal the sum of independently-rounded parts. Every amount in the document is now built by summing the same already-rounded-to-cent integers that appear in the individual line/allowance/charge/tax entries, so every EN16931 "declared total = sum of its own parts" rule holds exactly by construction rather than approximately. Verified by hand against all five affected business rules (BR-CO-10/12/13/14, BR-S-08) before pushing.
taxBreakdown.ts's earlier additive rawGross/rawNet fields are reverted — this pipeline no longer depends on it at all, computing everything fresh from order.items/discountAmount/shippingCost.
Mustang's Phase 4 CI check flagged BR-CO-10/BR-CO-14/BR-S-08 once the Delivery-element fix let it get that far: invoice line amounts were being reported as this shop's normal gross (VAT-inclusive) prices instead of the net amounts EN16931 requires, and shipping/discount had no explicit cac:AllowanceCharge representation at all (silently folded into a scaled net total instead) — so the document totals didn't reconcile against the line items the way a validating AP system checks.
De-grosses each line's own unit price per its own VAT rate for LineExtensionAmount/PriceAmount, and adds explicit per-rate AllowanceCharge entries (proportionally allocated the same way computeTaxBreakdown's scale factor already works internally, just split back into its two components instead of one combined adjustment). taxBreakdown.ts gained two additive fields (rawGross/rawNet) for this; the visual PDF renderers are unaffected, they only ever read rate/net/tax/gross.
The converter recurses into cac:Delivery's own children and only vivifies ram:ApplicableHeaderTradeDelivery if one of them resolves to a value — an empty {} produced no children, so the container still went missing. Using the invoice's own issue date as ActualDeliveryDate (BT-72, optional) actually populates it.
Mustang's Phase 4 CI check (validate-einvoice.yml) caught this on its very first real run: @e-invoice-eu/core's UBL->CII conversion silently drops the whole Delivery container when cac:Delivery is absent from the UBL input, breaking CII's fixed Agreement/Delivery/Settlement element order and failing schema validation on every generated e-invoice (original, Storno, and Gutschrift alike).
Adds a Gitea Actions workflow that generates realistic e-invoice fixtures (multi-VAT-rate original invoice, Storno, Gutschrift) and validates them against the reference Mustang validator on every push, catching a broken EN16931 mapping before it reaches production instead of relying on manual spot-checks.
@e-invoice-eu/core pulls in Node-only dependencies (tmp/tmp-promise, for
a LibreOffice conversion path this package never actually takes) that
broke Next.js's client bundle the moment renderInvoiceEInvoice() was
re-exported from the main "." entry — that entry is reachable from a
Client Component (email-templates.ts's Live Preview import chain), so
the bundler tried pulling e-invoice-eu/core in client-side too.
New "@einfach-produktiv/invoicing/einvoice" subpath (package.json
"exports" map + src/einvoice/index.ts) holds only the e-invoice
generation functions — server-only code (API routes, Server Components,
Payload job handlers) imports from there instead. The main "." entry
keeps everything already browser-safe (computeTaxBreakdown, formatters,
InvoiceDocument for Live Preview's <PDFViewer>, the plain PDF renderers).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
renderInvoiceEInvoice()/renderCorrectionInvoiceEInvoice() produce a
Factur-X-EN16931 hybrid PDF/A-3 (existing @react-pdf/renderer PDF +
embedded EN16931 XML) via @e-invoice-eu/core, instead of a plain PDF.
The plain renderInvoicePdf()/renderCorrectionInvoicePdf() stay unchanged
and are still what Live Preview uses — this only adds a post-processing
step for the actual send/download paths.
buildEInvoiceData()/buildCorrectionEInvoiceData() map InvoiceOrder/
CorrectionInvoiceOrder + InvoiceSeller into the library's raw UBL-shaped
Invoice object, reusing computeTaxBreakdown() for the tax math — one
implementation feeding both the human-readable and machine-readable side
of the same document. Verified end-to-end: rendered a sample invoice and
correction invoice, inflated the embedded XML stream out of the resulting
PDF/A-3 by hand (the library has no attachment-reading API to check
against), confirmed correct CrossIndustryInvoice XML, EN16931 guideline
reference, per-rate tax breakdown matching the order totals exactly, and
payment means with the IBAN from Phase 2 — not just "it didn't throw."
Found only through actually running it (not documented anywhere): every
EN16931 amount field requires a sibling `*@currencyID` key via the
library's runtime ajv validation, invisible in its TypeScript types.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Dropped by accident when bankDetails split into iban/bic — a bank name
has no fixed format to validate, so it stays free text, but it's still
useful context next to the account numbers in the invoice footer.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
InvoiceSeller.bankDetails becomes iban/bic (EN16931 wants discrete
PaymentMeans data, not a paragraph a human formatted by hand). The
footer's bank-details line also drops the misleading "(für Überweisung)"
qualifier — it was never actually conditional on the order's payment
method (only on whether the field was set at all), just worded as if it
were. Now shown whenever iban or bic is set, regardless of payment
method.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Consolidates code previously hand-duplicated between the einfach-produktiv
frontend and payload backend repos into one canonical implementation,
consumed as a git dependency by both instead of being kept in sync by eye.
While merging the two correction-invoice copies, found and fixed three
real drifts between them:
- variantName was silently dropped in the backend's emailed Stornorechnung/
Gutschrift, but present in the frontend's re-download copy
- the backend's correction-invoice footer wasn't position:fixed, unlike
the original invoice and the frontend's copy
- the backend used a numeric date format (03.07.2025) while the original
invoice and the frontend's copy both used a spelled-out month (03. Juli
2025) — a re-download didn't visually match what was emailed
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>