Commit Graph

35 Commits

Author SHA1 Message Date
Marco 2b4b2eb91f Add shared VIES/VAT-ID/PLZ/carrier-tracking modules (v0.3.0)
Validate e-invoices / mustang (push) Successful in 23s
Unifies logic that was hand-duplicated (and, for PLZ, actually inconsistent) between the backend and frontend repos — both explicitly flagged this drift risk in their own code comments. vies.ts stays behind a separate ./vies subpath (server-only by convention, unlike the client-safe main barrel).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 15:32:55 +00:00
Marco 5f5b528db5 Add showcase-xml script for generating a sample e-invoice XML
Validate e-invoices / mustang (push) Successful in 23s
One-off script (sample seller data, writes to /tmp) used to produce the embedded-XML sample for the e-invoicing showcase/marketing post.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 13:45:07 +00:00
Marco e3d575e73b Add customer-onboarding section to README
Validate e-invoices / mustang (push) Successful in 45s
New clients licensing this module need setup steps (git dependency, transpilePackages, peer deps, which VAT/e-invoicing flags to wire up) up front, not buried in the changelog below.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 13:00:04 +00:00
Marco 87cf1db330 Fix fonts.ts crashing every client bundle that reaches it (v0.2.9)
Validate e-invoices / mustang (push) Successful in 25s
The v0.2.8 fileURLToPath fix (for a Turbopack asset-hash bug in the
Payload backend) wrongly assumed fonts.ts never reaches a browser
bundle. It does: einfach-produktiv's LiveCompanySettingsPreviewClient
renders <InvoiceDocument> client-side, and several Client Components
pull this module in transitively via the index.ts barrel just by
importing computeTaxBreakdown. fileURLToPath isn't a real function in
a browser's node:url shim, so calling it unconditionally at module
scope crashed module evaluation for any client bundle that included
it — including einfach-produktiv's entire /cart page.

Fix: branch on typeof window === "undefined" — browser keeps the
original new URL(..., import.meta.url) idiom (correctly asset-hashed
by Turbopack for a consumer's own build), Node.js keeps the
fileURLToPath/path.join resolution from v0.2.8.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 18:22:53 +00:00
Marco ed96d36b54 Fix font path breaking every PDF render inside the Payload backend (v0.2.8)
Validate e-invoices / mustang (push) Successful in 23s
fonts.ts resolved the vendored Liberation Sans .ttf files via
`new URL('./assets/fonts/...', import.meta.url).pathname` — Next.js's
Turbopack/webpack bundler statically rewrites exactly that expression
into its own hashed static-asset system. Harmless in the frontend's own
build, but the Payload backend is also a Next.js app consuming this
package as a node_modules git dependency, where the hash referenced in
the compiled server code didn't match what was actually emitted to disk
— every invoice/correction-invoice admin download failed with
ENOENT: .../LiberationSans-Regular.<hash>.ttf. Not a cache issue,
reproduced identically after a from-scratch --no-cache rebuild. Fixed by
resolving the path via fileURLToPath(import.meta.url) + path.join
instead, which Turbopack does not special-case.
2026-07-30 10:48:42 +00:00
Marco e66007cc4a Add optional per-line SKU + shipping recipient company/contact fields (v0.2.7)
Validate e-invoices / mustang (push) Successful in 43s
Invoice PDFs (visual + EN16931 XML) and correction invoices now show an
"Art.-Nr." line when an order item has a SKU snapshot, and the
Lieferadresse block includes company name/contact email/phone when set.
2026-07-30 08:18:58 +00:00
Marco d6e83db5cf Embed Liberation Sans in invoice PDFs for PDF/A-3 compliance (v0.2.6)
Validate e-invoices / mustang (push) Successful in 41s
An E-Rechnung checker flagged that generated invoices don't embed their
fonts. Both invoicePdf.tsx and correctionInvoicePdf.tsx used react-pdf's
built-in Helvetica/Helvetica-Bold, which is never embedded — PDF/A-3
(already required by this package's own Factur-X/ZUGFeRD pipeline) has
no exemption for standard fonts. Vendors Liberation Sans (SIL OFL-1.1,
metrically identical to Helvetica) and registers it via Font.register in
a new fonts.ts, imported by both PDF modules.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 23:37:28 +00:00
Marco bcdf8d2889 Move §19/§4-Nr.-1b-UStG notice out of the summary card
Validate e-invoices / mustang (push) Successful in 22s
Same fix invoicePdf.tsx's own unpaid-notice row already got: the
exemption/Kleinunternehmer notice used to live inside summaryBox's
narrow, right-aligned column, squeezing what's actually a full
sentence. Now a full-width sibling row below the card, same treatment
in both invoicePdf.tsx and correctionInvoicePdf.tsx, and ordered ahead
of the unpaid-notice row when both apply.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 19:48:08 +00:00
Marco b3bd5c3f07 Fix unpaid-Vorkasse notice layout: full page width, more top spacing
Validate e-invoices / mustang (push) Successful in 22s
Reused paidBadgeRow, which lives inside summary's narrow flex-end
column meant for the short one-line paid confirmation — the longer
instruction shrank to content width and read as squeezed under the
summary card. Moved to its own full-width sibling row, left-aligned.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 16:58:35 +00:00
Marco 8dc56c9783 Fix isPaidImmediately's exact-match regression, add unpaid Vorkasse instructions
Validate e-invoices / mustang (push) Successful in 41s
isPaidImmediately() matched paymentMethodTitle with an exact !== check
against the literal "Überweisung" — the consuming shop renamed its
Vorkasse row to "Überweisung (Vorkasse)" the same day, which would have
silently made every unpaid Vorkasse invoice show "✓ Bereits beglichen".
Changed to startsWith("Überweisung") — same design, tolerant of a
suffix qualifier.

Also adds an explicit instruction for the unpaid case (previously the
absence of the paid confirmation was the only signal, no actual text):
pay to the listed bank details, order processed after payment received
(~1-2 business days).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 16:11:44 +00:00
Marco ceaa437724 Drop the Netto row for Kleinunternehmer invoices
Validate e-invoices / mustang (push) Successful in 24s
Netto = Gesamt is structural under §19 UStG (no tax component ever
existed), not the coincidental 0%-this-one-sale case vatExempt orders
are in — showing it right under the §19 notice just duplicated Gesamt.
At the user's own request after reviewing a real invoice.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 15:18:36 +00:00
Marco 439165c98d Make InvoiceSeller.vatId optional for Kleinunternehmer sellers
Validate e-invoices / mustang (push) Successful in 18s
Payload's company-settings.vatId is no longer hard-required (a
Kleinunternehmer often has no USt-IdNr. at all) — this package's
InvoiceSeller.vatId follows suit, with every renderer now omitting the
USt-IdNr. line/field entirely instead of printing/serializing an empty
value.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 13:09:30 +00:00
Marco 51ae8a9c0a Add Kleinunternehmerregelung (§19 UStG) support
Validate e-invoices / mustang (push) Successful in 43s
InvoiceOrder/CorrectionInvoiceOrder gain an optional order-level
kleinunternehmer flag, snapshotted per order (same pattern as vatExempt)
so a later toggle of the tenant's setting never rewrites an
already-issued invoice. Takes precedence over vatExempt.

- Visual PDF: "enthält X% MwSt." becomes the §19 UStG notice.
- E-invoice XML: new UNTDID 5305 category "E" with a free-text exemption
  reason (no VATEX code — §19 UStG has none, BR-E-10 allows text alone).
- New Mustang CI fixture + buildEInvoiceData unit test for the "E" path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 12:51:13 +00:00
Marco c28ceb7bd3 Style Netto the same size/weight as the MwSt annotation
Validate e-invoices / mustang (push) Successful in 21s
Was using summaryLabel/summaryValue (fontSize 10, same as Zwischensumme/
Versand — the build-up rows above Gesamt), which visually competed
with Gesamt instead of reading as supplementary detail below it. Now
uses the same small/muted vatNoteLabel/vatNoteValue as the "enthält
X% MwSt." line right above it — both are detail underneath the hero
Gesamt figure, same visual tier.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 21:33:57 +00:00
Marco 3922ea12e1 Move Netto below the MwSt annotation, not above it
Validate e-invoices / mustang (push) Successful in 20s
Order was Gesamt → Netto → "enthält X% MwSt." — reads as if the
annotation describes Netto, which is backwards: Netto already has the
tax removed, Gesamt is what contains it. Reordered to Gesamt →
"enthält X% MwSt." → Netto so the annotation stays unambiguously
attached to the row it actually describes. Verified by re-rendering
and visually inspecting both the original invoice and a Storno.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 21:26:51 +00:00
Marco 70c39f8b3a Document the Netto row and its universal-scope fix
Validate e-invoices / mustang (push) Successful in 22s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 21:07:04 +00:00
Marco 31978d5ea2 Show Netto on every invoice, not just non-exempt ones
Validate e-invoices / mustang (push) Successful in 23s
Netto/Gesamt/MwSt breakdown was gated on !order.vatExempt — on a
vatExempt (innergemeinschaftliche Lieferung) invoice, Netto happens to
equal Gesamt exactly (0% tax), which read as redundant next to the
exemption note, so the row was skipped there. Turns out that's wrong:
the ask was for Netto on every invoice this shop issues, B2B/exempt
included, for layout consistency — not conditional on the tax rate.
Same fix in both invoicePdf.tsx and correctionInvoicePdf.tsx (the
latter covers both Storno and Gutschrift via the same summary code).

Verified by rendering all three cases (normal 19% invoice, exempt B2B
invoice, Storno correction) and visually inspecting the output.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 20:58:16 +00:00
Marco 83ece1efef Document VAT exemption support and today's invoice-layout fixes
Validate e-invoices / mustang (push) Successful in 22s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 19:19:26 +00:00
Marco 942b8a8632 Add innergemeinschaftliche-Lieferung (VAT exemption) support
Validate e-invoices / mustang (push) Successful in 18s
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>
2026-07-23 18:59:19 +00:00
Marco 01374c47cd Fix invoice/correction-invoice summary: rows now actually sum to Gesamt
Validate e-invoices / mustang (push) Successful in 21s
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>
2026-07-23 18:50:23 +00:00
Marco 48da8c069a Plain-text paid badge, uniform item rows, show B2B recipient fields
Validate e-invoices / mustang (push) Successful in 22s
- 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>
2026-07-23 18:16:27 +00:00
Marco ceedb4be05 Move paid badge outside the summary card, not just below the last row inside it
Validate e-invoices / mustang (push) Successful in 23s
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.
2026-07-23 14:51:45 +00:00
Marco 6af96eb9b8 Invoice PDF layout: centered footer, clustered summary rows, more image/text spacing, relocated paid badge, stacked Packstation/Postnummer
Validate e-invoices / mustang (push) Successful in 22s
- 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.
2026-07-23 14:33:27 +00:00
Marco f6d8e9eaf5 Fix missing degross step in shipping/discount allocation (real ~1 EUR bug on a real order)
Validate e-invoices / mustang (push) Successful in 23s
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.
2026-07-23 14:02:25 +00:00
Marco fd5d8e688b Fix BR-CO-16: declare the PayableAmount/order.total gap via PayableRoundingAmount
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.
2026-07-23 12:43:06 +00:00
Marco 97a7f6a036 Rebuild EN16931 monetary fields on an integer-cents pipeline
Validate e-invoices / mustang (push) Failing after 16s
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.
2026-07-23 12:40:07 +00:00
Marco 9ff4d39de4 Fix EN16931 line/allowance-charge amounts: report net, not gross
Validate e-invoices / mustang (push) Failing after 18s
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.
2026-07-23 12:34:10 +00:00
Marco dd37b8dc78 Actually fix the Delivery element: an empty object wasn't enough
Validate e-invoices / mustang (push) Failing after 17s
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.
2026-07-23 12:25:21 +00:00
Marco ffd4c40d92 Fix CII schema violation: emit an empty ApplicableHeaderTradeDelivery element
Validate e-invoices / mustang (push) Failing after 19s
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).
2026-07-23 12:23:03 +00:00
Marco 29ea9fafd8 Phase 4: Mustang EN16931/PDF-A-3 validation in CI
Validate e-invoices / mustang (push) Failing after 1m30s
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.
2026-07-23 12:12:51 +00:00
Marco 68d7054557 Split e-invoice exports into a separate server-only subpath
@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>
2026-07-23 11:31:15 +00:00
Marco 63e3a02212 Phase 3: actual ZUGFeRD/Factur-X e-invoice generation
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>
2026-07-23 11:28:03 +00:00
Marco 4e9b4b9dd4 Add bankName back to InvoiceSeller, shown alongside IBAN/BIC
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>
2026-07-23 10:26:23 +00:00
Marco 62821ddc8a Phase 2: structured iban/bic instead of free-text bankDetails
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>
2026-07-23 10:06:12 +00:00
Marco 9336cfd0ba Initial commit: shared invoice/correction-invoice PDF generation
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>
2026-07-23 08:33:12 +00:00