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>
This commit is contained in:
Marco
2026-07-24 13:09:30 +00:00
parent 51ae8a9c0a
commit 439165c98d
6 changed files with 35 additions and 12 deletions
+2
View File
@@ -80,6 +80,8 @@ Unlike `vatExempt`, this package does **not** expect `unitPrice` to be de-grosse
- Visual PDF (`invoicePdf.tsx`/`correctionInvoicePdf.tsx`): the "enthält X% MwSt." annotation under Gesamt becomes "Gemäß § 19 UStG wird keine Umsatzsteuer berechnet." instead — takes precedence over the `vatExempt` note if (implausibly) both were ever set.
- E-invoice XML (`buildEInvoiceData.ts`): VAT category `E` + a free-text exemption reason, no VATEX code — see "E-invoicing" above.
`InvoiceSeller.vatId` is now optional (`string | null`, was a plain required `string`) for the same reason — a Kleinunternehmer commonly never registers for an USt-IdNr. at all (no intra-EU trade). Every renderer treats a missing value as "omit the USt-IdNr. line/field" rather than printing an empty one: the visual PDF footer drops the whole `· USt-IdNr. …` segment, and `buildEInvoiceData.ts`'s `sellerParty()` omits `cac:PartyTaxScheme` entirely rather than emitting one with a blank `CompanyID`.
## CI validation (Mustang)
Every push/PR runs `.gitea/workflows/validate-einvoice.yml` against git.mk360.de's own self-hosted Gitea Actions runner (`vps-runner`, registered on the same VPS as Gitea/Payload — see `/home/marco/dev/docker/docker-compose.yml`'s `act_runner` service):
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@einfach-produktiv/invoicing",
"version": "0.2.0",
"version": "0.2.1",
"private": true,
"description": "Shared invoice / correction-invoice (Stornorechnung, Gutschrift) PDF generation and VAT-breakdown math, consumed as a git dependency by both the einfach-produktiv frontend and the payload backend — not published to npm.",
"type": "module",
+4 -2
View File
@@ -379,8 +379,10 @@ function CorrectionInvoiceDocument({ kind, order, seller }: { kind: CorrectionIn
<View style={styles.footer} fixed>
<Text>
{seller.sellerName} · {seller.sellerStreet}, {seller.sellerZip} {seller.sellerCity} · {seller.sellerEmail} · USt-IdNr.{" "}
{seller.vatId}
{seller.sellerName} · {seller.sellerStreet}, {seller.sellerZip} {seller.sellerCity} · {seller.sellerEmail}
{/* Same "omit entirely when unset" as invoicePdf.tsx's own
footer — see seller.ts's own comment on vatId. */}
{seller.vatId ? ` · USt-IdNr. ${seller.vatId}` : ""}
{seller.registerCourt && seller.registerNumber ? ` · ${seller.registerCourt} · ${seller.registerNumber}` : ""}
{seller.managingDirector ? ` · Geschäftsführung: ${seller.managingDirector}` : ""}
</Text>
+15 -6
View File
@@ -185,12 +185,21 @@ function sellerParty(seller: InvoiceSeller): UblInvoice["cac:AccountingSupplierP
"cbc:PostalZone": seller.sellerZip,
"cac:Country": { "cbc:IdentificationCode": countryCode(seller.sellerCountry) as SellerCountryCode },
},
"cac:PartyTaxScheme": [
{
"cbc:CompanyID": seller.vatId,
"cac:TaxScheme": { "cbc:ID": "VAT" },
},
],
// Omitted entirely (not a PartyTaxScheme with an empty CompanyID)
// when unset — a Kleinunternehmer (§19 UStG) often has no USt-IdNr.
// at all, see seller.ts's own comment. Same "always present in this
// shop's real production data today, but must degrade safely" spirit
// as paymentMeans()'s own `!seller.iban` check above.
...(seller.vatId
? {
"cac:PartyTaxScheme": [
{
"cbc:CompanyID": seller.vatId,
"cac:TaxScheme": { "cbc:ID": "VAT" },
},
],
}
: {}),
"cac:PartyLegalEntity": { "cbc:RegistrationName": seller.sellerName },
"cac:Contact": { "cbc:ElectronicMail": seller.sellerEmail },
},
+5 -2
View File
@@ -449,8 +449,11 @@ export function InvoiceDocument({ order, seller }: { order: InvoiceOrder; seller
<View style={styles.footer} fixed>
<Text>
{seller.sellerName} · {seller.sellerStreet}, {seller.sellerZip} {seller.sellerCity} · {seller.sellerEmail} · USt-IdNr.{" "}
{seller.vatId}
{seller.sellerName} · {seller.sellerStreet}, {seller.sellerZip} {seller.sellerCity} · {seller.sellerEmail}
{/* Omitted entirely (not "USt-IdNr. " with a blank value) when
unset — a Kleinunternehmer (§19 UStG) often has no USt-IdNr.
at all, see seller.ts's own comment. */}
{seller.vatId ? ` · USt-IdNr. ${seller.vatId}` : ""}
{seller.registerCourt && seller.registerNumber ? ` · ${seller.registerCourt} · ${seller.registerNumber}` : ""}
{seller.managingDirector ? ` · Geschäftsführung: ${seller.managingDirector}` : ""}
</Text>
+8 -1
View File
@@ -9,7 +9,14 @@ export type InvoiceSeller = {
sellerCity: string;
sellerCountry: string;
sellerEmail: string;
vatId: string;
// Optional as of the Kleinunternehmerregelung (§19 UStG) addition — a
// Kleinunternehmer often only has a Steuernummer, never registers for an
// USt-IdNr. at all (no intra-EU trade), so company-settings.vatId is no
// longer a hard-required field on the Payload side (see that
// collection's own comment). Every renderer here already treats a
// missing vatId as "omit the USt-IdNr. line/field" rather than printing
// an empty value.
vatId?: string | null;
taxRatePercent: number;
// Structured (Phase 2 of the e-invoicing migration — EN16931 wants
// discrete PaymentMeans data, not a free-text paragraph a human