Add innergemeinschaftliche-Lieferung VAT exemption for cross-border B2B
A validated EU business buyer (Österreich, the one cross-border option this checkout offers) gets the sale zero-rated per §4 Nr. 1b UStG — but only after a live VIES lookup confirms the VAT ID is actually registered right now, never from format-validity alone (real compliance risk otherwise). VIES unreachable fails closed: normal VAT applies, no guessed exemption. - lib/vies.ts: calls the EU's public VIES REST API. - lib/vatExemption.ts: de-grosses item/shipping prices and computes the exempt totals; also picks the actual destination country (shipping override when set, billing otherwise). - api/checkout/validate-vat: on-blur live check for instant feedback; api/checkout/route.ts re-runs the same check server-side at submit as the actual source of truth, and re-prices every line net-of-VAT when exempt. - CheckoutContent.tsx: VIES status + a live exempt-totals preview; BestellbestaetigungContent.tsx mirrors it from the persisted snapshot. Both blur-validate every other checkout field now too (immediate inline errors, not just on submit). - vatExempt/vatIdValidatedAt threaded through orderServer.ts, customerAuth.ts, orderEmail.ts, and both invoice-download routes so the invoice PDF and its e-invoice XML (companion payload-repo commit) reflect the exemption correctly wherever it's rendered. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import type { CartItem } from "../../lib/cart";
|
||||
import { useProducts } from "../../lib/products";
|
||||
import { computeCartTotals, effectivePrice, effectiveTaxRate } from "../../lib/cartTotals";
|
||||
import { computeTaxBreakdown } from "@einfach-produktiv/invoicing";
|
||||
import { computeExemptTotals } from "../../lib/vatExemption";
|
||||
import { formatPrice, formatDate } from "../../lib/format";
|
||||
import { Reveal } from "../../components/Reveal";
|
||||
import { CheckoutSteps } from "../../components/CheckoutSteps";
|
||||
@@ -31,7 +32,8 @@ function parseOrderSnapshot(raw: string): OrderSnapshot | null {
|
||||
typeof data.shippingCost !== "number" ||
|
||||
typeof data.paymentMethodTitle !== "string" ||
|
||||
(data.discountCode !== null && typeof data.discountCode !== "string") ||
|
||||
typeof data.discountAmount !== "number"
|
||||
typeof data.discountAmount !== "number" ||
|
||||
typeof data.vatExempt !== "boolean"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -100,20 +102,42 @@ export function BestellbestaetigungContent({ defaultTaxRate }: { defaultTaxRate:
|
||||
.map((entry) => ({ entry, product: products.find((p) => p.id === entry.id) }))
|
||||
.filter((row): row is { entry: CartItem; product: NonNullable<(typeof row)["product"]> } => Boolean(row.product));
|
||||
|
||||
// Displays the *persisted* discount from the snapshot, not a fresh
|
||||
// re-derivation — the purchase already happened, this page is a
|
||||
// Displays the *persisted* discount/shippingCost from the snapshot, not
|
||||
// a fresh re-derivation — the purchase already happened, this page is a
|
||||
// receipt, not a live cart, so it doesn't re-validate the code at all.
|
||||
const { subtotal, totalSavings, total } = computeCartTotals(items, order.shippingCost, {
|
||||
// order.shippingCost is already the actual (possibly de-grossed, if
|
||||
// vatExempt) figure charged at checkout — see api/checkout/route.ts's
|
||||
// own response. `subtotal`/`taxBreakdown` below still need their own
|
||||
// exempt branch, though: computeCartTotals/computeTaxBreakdown build
|
||||
// `subtotal` from each item's *current catalog* gross price via
|
||||
// effectivePrice(), which for an exempt order was never what was
|
||||
// actually charged (the catalog price includes VAT; the exempt order
|
||||
// paid the de-grossed net price instead).
|
||||
const { subtotal: catalogSubtotal, totalSavings, total: catalogTotal } = computeCartTotals(items, order.shippingCost, {
|
||||
type: "fixed",
|
||||
value: order.discountAmount,
|
||||
});
|
||||
const exemptTotals = order.vatExempt
|
||||
? computeExemptTotals(
|
||||
items.map(({ entry, product }) => ({
|
||||
quantity: entry.qty,
|
||||
grossUnitPrice: effectivePrice(entry, product),
|
||||
taxRatePercent: effectiveTaxRate(product, defaultTaxRate),
|
||||
})),
|
||||
order.shippingCost,
|
||||
defaultTaxRate,
|
||||
order.discountAmount,
|
||||
)
|
||||
: null;
|
||||
const subtotal = exemptTotals?.subtotal ?? catalogSubtotal;
|
||||
const total = exemptTotals?.total ?? catalogTotal;
|
||||
const taxBreakdown = computeTaxBreakdown(
|
||||
items.map(({ entry, product }) => ({
|
||||
quantity: entry.qty,
|
||||
unitPrice: effectivePrice(entry, product),
|
||||
taxRatePercent: effectiveTaxRate(product, defaultTaxRate),
|
||||
})),
|
||||
subtotal,
|
||||
catalogSubtotal,
|
||||
order.discountAmount,
|
||||
order.shippingCost,
|
||||
);
|
||||
@@ -257,7 +281,11 @@ export function BestellbestaetigungContent({ defaultTaxRate }: { defaultTaxRate:
|
||||
<span className="flex-1" />
|
||||
<span className="font-bold text-h-small text-text-primary">{formatPrice(total)}</span>
|
||||
</div>
|
||||
<VatBreakdown groups={taxBreakdown} />
|
||||
{order.vatExempt ? (
|
||||
<p className="text-label text-text-muted">Steuerfreie innergemeinschaftliche Lieferung (§4 Nr. 1b UStG)</p>
|
||||
) : (
|
||||
<VatBreakdown groups={taxBreakdown} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user