diff --git a/app/api/checkout/route.ts b/app/api/checkout/route.ts index 7d752e0..ee61c02 100644 --- a/app/api/checkout/route.ts +++ b/app/api/checkout/route.ts @@ -209,22 +209,31 @@ export async function POST(request: Request) { ); } - // Innergemeinschaftliche Lieferung (§4 Nr. 1b UStG) — only for the goods' - // actual destination (the shipping override's country when set, the - // billing country otherwise) being Österreich, the one EU-cross-border - // option this checkout offers, AND a VAT ID that VIES itself confirms is - // currently registered right now, at the moment of purchase — a merely - // format-valid id is never enough (see lib/vatExemption.ts's own - // comment). VIES being unreachable fails closed: normal VAT applies, - // never a guessed exemption. + // VAT-ID validity and the exemption decision are two separate questions. + // Validity (is this actually a currently-registered VAT ID at all) is + // checked via VIES for ANY country whenever one is given — worth + // recording regardless of destination, same "data quality" reasoning as + // company-settings.vatId's own VIES check on the backend; a merely + // format-valid id (e.g. "ED123456789" — "ED" isn't even a real country + // code) is never enough on its own. The exemption itself + // (innergemeinschaftliche Lieferung, §4 Nr. 1b UStG) additionally + // requires the goods' actual destination (the shipping override's + // country when set, the billing country otherwise) to be Österreich, + // the one EU-cross-border option this checkout offers — a validated + // *German* VAT ID never zero-rates a domestic sale, no matter how real + // it is. VIES being unreachable fails closed on the exemption: normal + // VAT applies, never a guessed exemption (vatIdValidatedAt just stays + // unset in that case too). let vatExempt = false; let vatIdValidatedAt: string | null = null; const buyerDestinationCountry = destinationCountry(body.country, Boolean(body.hasDifferentShippingAddress), body.shippingCountry); - if (normalizedVatId && isExemptionEligibleCountry(buyerDestinationCountry)) { + if (normalizedVatId) { const viesResult = await checkVatIdViaVies(normalizedVatId); if (viesResult.ok && viesResult.valid) { - vatExempt = true; vatIdValidatedAt = new Date().toISOString(); + if (isExemptionEligibleCountry(buyerDestinationCountry)) { + vatExempt = true; + } } } diff --git a/app/checkout/components/CheckoutContent.tsx b/app/checkout/components/CheckoutContent.tsx index bdaac0c..4121cbf 100644 --- a/app/checkout/components/CheckoutContent.tsx +++ b/app/checkout/components/CheckoutContent.tsx @@ -345,14 +345,19 @@ export function CheckoutContent({ const displayTotal = exemptTotalsPreview?.total ?? total; // USt-IdNr. blur — format-checks first (always), then a live VIES lookup - // only once the destination actually qualifies (Österreich) — no point - // hitting the EU's API for a Deutschland/Schweiz order, where this - // exemption never applies regardless of what VIES says. + // for ANY country, not just Österreich. Two genuinely separate concerns: + // whether this is a real, currently-registered VAT ID at all (data + // quality — worth knowing regardless of destination, same reasoning as + // company-settings.vatId's own VIES check on the backend) vs. whether + // *this transaction* qualifies for the cross-border exemption (a + // narrower legal question, still gated on isExemptionEligibleCountry() + // wherever vatExemptPreview/exemptTotalsPreview are computed below — a + // validated German VAT ID never zero-rates a domestic sale). async function handleVatIdBlur(e: React.FocusEvent) { const value = e.target.value; const formatError = validateVatIdFormat(value); setFieldError("vatId", formatError); - if (!value.trim() || formatError || !isExemptionEligibleCountry(destinationCountry(country, hasDifferentShippingAddress, shippingCountry))) { + if (!value.trim() || formatError) { setVatIdViesStatus("idle"); return; } @@ -708,23 +713,28 @@ export function CheckoutContent({ placeholder="DE123456789" autoComplete="off" pattern="[A-Za-z]{2}[A-Za-z0-9]{2,12}" + maxLength={14} title="EU-Format: 2 Buchstaben Länderpräfix + bis zu 12 alphanumerische Zeichen, z. B. DE123456789." wrapperClassName="w-full" /> - {/* Only shown once the destination actually qualifies - (Österreich) — a "checking..."/status message for a - Deutschland/Schweiz order would be meaningless noise, - the exemption never applies there regardless. */} - {isExemptionEligibleCountry(buyerDestinationCountry) && ( -

- {vatIdViesStatus === "checking" && "USt-IdNr. wird geprüft…"} - {vatIdViesStatus === "valid" && ( - ✓ Bestätigt — Lieferung wird steuerfrei berechnet. - )} - {vatIdViesStatus === "invalid" && "USt-IdNr. konnte nicht bestätigt werden — reguläre MwSt. wird berechnet."} - {vatIdViesStatus === "unavailable" && "Prüfung derzeit nicht möglich — reguläre MwSt. wird berechnet."} -

- )} + {/* Shown for any country — validity is worth confirming + regardless of destination (data quality: is this VAT ID + even real). Only the "valid" message's wording differs + by destination: Österreich additionally gets the + exemption note, Deutschland/Schweiz just get a plain + confirmation, since the exemption never applies there + even for a genuinely valid VAT ID. */} +

+ {vatIdViesStatus === "checking" && "USt-IdNr. wird geprüft…"} + {vatIdViesStatus === "valid" && ( + + ✓ USt-IdNr. bestätigt + {isExemptionEligibleCountry(buyerDestinationCountry) ? " — Lieferung wird steuerfrei berechnet." : "."} + + )} + {vatIdViesStatus === "invalid" && "USt-IdNr. konnte nicht bestätigt werden."} + {vatIdViesStatus === "unavailable" && "USt-IdNr.-Prüfung derzeit nicht möglich."} +

{/* w-[calc(50%-0.5rem)] at sm: — exactly matches Vorname's @@ -804,6 +814,7 @@ export function CheckoutContent({ autoComplete="postal-code" inputMode="numeric" pattern={plzPattern(country)} + maxLength={PLZ_DIGITS[country] ?? 4} title={`PLZ muss aus ${PLZ_DIGITS[country] ?? 4} Ziffern bestehen.`} required /> @@ -962,6 +973,7 @@ export function CheckoutContent({ autoComplete="off" inputMode="numeric" pattern={plzPattern(shippingCountry)} + maxLength={PLZ_DIGITS[shippingCountry] ?? 4} title={`PLZ muss aus ${PLZ_DIGITS[shippingCountry] ?? 4} Ziffern bestehen.`} required />