Add Kleinunternehmerregelung (§19 UStG) support end-to-end

Checkout forces 0% VAT without de-grossing prices when the tenant is a
Kleinunternehmer (a business decision, not just an engineering default —
unlike the existing intra-community VAT exemption, which does de-gross).
Snapshotted onto the order at checkout time so a later toggle of the
company-settings checkbox never rewrites an already-issued invoice's tax
treatment — same reasoning as the existing vatExempt field.

Threaded through: checkout route, order creation/confirmation email,
on-demand invoice/Storno/Gutschrift downloads, the Bestellbestätigung
page, and the account order-detail page. The four storefront "inkl. X%
MwSt." price hints (shop grid, cart upsell, ToDo-Karten landing page,
homepage spotlight) drop that clause live when the setting is on. The
company-settings Live Preview reflects the checkbox in real time too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-24 15:08:13 +00:00
parent 89dd11bf77
commit ba830947d2
23 changed files with 203 additions and 35 deletions
+62
View File
@@ -327,6 +327,11 @@ thumbnails per order row (`getProductImagesByIds()` in `app/lib/payload.ts`,
a plain product-id → image-url lookup separate from the slug-keyed
catalog, since an order only ever snapshots a numeric product id).
**None of this renders at all for a Kleinunternehmer tenant** (built
2026-07-24) — see "Kleinunternehmerregelung" below for the full list of
touched spots and why some read a live setting and others a persisted
per-order snapshot.
### Checkout state persistence
`app/lib/checkoutDraft.ts``localStorage` under `ep_checkout_draft`,
@@ -833,6 +838,63 @@ turns out unregistered, the seller retroactively owes the VAT itself).
live from the current catalog, which would show the wrong, VAT-inclusive
figures for an exempt order).
### Kleinunternehmerregelung (§19 UStG)
Built 2026-07-24. `company-settings.kleinunternehmer` — a standing
per-tenant setting (Payload backend), not a per-order decision like the
VAT exemption above — when on, this tenant never charges VAT on anything,
domestic or cross-border. See the Payload README's own
"Kleinunternehmerregelung" section for the field/collection side; this one
covers what changed here.
- **`api/checkout/route.ts`** forces every item's `taxRatePercent` to `0`
when `getCompanySettings().kleinunternehmer` is on — deliberately
**without** de-grossing `unitPrice` the way `vatExempt` above does
(that exemption zero-rates what would otherwise be a positive-rate
charge, so de-grossing means the buyer pays less; a Kleinunternehmer
never charged VAT on the sale to begin with, so the catalog gross price
already *is* the actual net charge — confirmed with the user as the
intended business decision, not an engineering default). The VIES
lookup/`isExemptionEligibleCountry()` check is skipped entirely in this
branch too — there's no VAT for the intra-community rule to exempt
either. Snapshotted onto the new order as `Orders.kleinunternehmer`
(mirrors `vatExempt`'s own snapshot reasoning — see the Payload README).
- **Storefront "inkl. X% MwSt." hints** — four spots read the *live*
setting (`getKleinunternehmer()` in `app/lib/payload.ts`, same ISR-cached
60s freshness as `getDefaultTaxRatePercent()`) and drop the MwSt. clause
entirely when it's on, since there's no order yet at that point to
snapshot from: `ProductGrid.tsx` (shop grid), `RelatedProducts.tsx`
(cart's upsell row), `Pricing.tsx`/`TodoKartenHero.tsx` (ToDo-Karten
landing page), `ProductSpotlight.tsx` (homepage). `Pricing.tsx`/
`ProductSpotlight.tsx` keep "zzgl. Versand" on its own when the MwSt.
clause drops; the other two had no such trailing clause to preserve.
- **Every already-placed-order display reads the persisted snapshot
instead** — `OrderSnapshot.kleinunternehmer` (`app/lib/order.ts`,
written into `sessionStorage` at checkout, read by
`BestellbestaetigungContent.tsx`) and `CustomerOrderDetail.
kleinunternehmer` (`app/lib/customerAuth.ts`, read by
`/konto/bestellungen/[orderNumber]`) — never the live company-settings
value, for the identical "don't retroactively rewrite an already-issued
invoice's tax treatment" reason `vatExempt` already established. Both
pages replace the per-item "inkl. X% MwSt." hint and the `VatBreakdown`
summary with "Gemäß § 19 UStG wird keine Umsatzsteuer berechnet." —
taking precedence over the `vatExempt` note wherever both would
otherwise apply. `CheckoutContent.tsx`'s live VIES-exemption *preview*
is also gated off (`!kleinunternehmer && ...`) so a Kleinunternehmer
tenant never shows a misleading "wird steuerfrei berechnet" preview for
VAT that was never going to be charged either way.
- **On-demand invoice/Stornorechnung/Gutschrift downloads**
(`api/account/orders/[orderNumber]/invoice/route.ts` and its
`correction-invoice` sibling) thread `order.kleinunternehmer` through to
`@einfach-produktiv/invoicing`'s renderers the same way they already
thread `vatExempt`.
- **`app/company-settings-preview`'s Live Preview** merges the live-edited
`kleinunternehmer` checkbox onto the fixed `SAMPLE_INVOICE_ORDER` before
rendering (`kleinunternehmer` lives on `InvoiceOrder`, not
`InvoiceSeller` — see the invoicing package's own README on why), so an
admin sees the §19 UStG notice appear/disappear live as they toggle the
field, without this preview needing its own separate mechanism.
### Company Settings & Live Preview
`company-settings` has a Live Preview button too, like `email-templates`