Check VAT ID validity via VIES for any country, not just Österreich

VAT-ID validity and the exemption decision are separate questions.
Previously VIES was only ever called when the destination already
qualified for the cross-border exemption (Österreich), so a garbage
VAT ID on a domestic order (e.g. "ED123456789" — not even a real
country code) sailed through with no feedback at all, and a
Deutschland/Schweiz customer got no confirmation their real VAT ID
was valid either. Now VIES checks any format-valid VAT ID regardless
of destination (data quality, same reasoning as company-settings'
own check) — the exemption itself still only applies when the
destination is also Österreich, a validated German VAT ID never
zero-rates a domestic sale. The status message now always shows
("✓ USt-IdNr. bestätigt", plus the exemption note only when it
actually applies) instead of staying hidden for non-Österreich
orders.

Also added maxLength to PLZ (per-country digit count) and USt-IdNr.
(14) checkout fields — they had pattern validation but nothing
stopping the browser from accepting more characters than could ever
be valid.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-23 19:37:29 +00:00
parent ba4d7b443f
commit 4d2e78dd2a
2 changed files with 49 additions and 28 deletions
+30 -18
View File
@@ -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<HTMLInputElement>) {
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) && (
<p className="text-label text-text-muted">
{vatIdViesStatus === "checking" && "USt-IdNr. wird geprüft…"}
{vatIdViesStatus === "valid" && (
<span className="text-success"> Bestätigt Lieferung wird steuerfrei berechnet.</span>
)}
{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."}
</p>
)}
{/* 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. */}
<p className="text-label text-text-muted min-h-[1.05rem]">
{vatIdViesStatus === "checking" && "USt-IdNr. wird geprüft…"}
{vatIdViesStatus === "valid" && (
<span className="text-success">
USt-IdNr. bestätigt
{isExemptionEligibleCountry(buyerDestinationCountry) ? " — Lieferung wird steuerfrei berechnet." : "."}
</span>
)}
{vatIdViesStatus === "invalid" && "USt-IdNr. konnte nicht bestätigt werden."}
{vatIdViesStatus === "unavailable" && "USt-IdNr.-Prüfung derzeit nicht möglich."}
</p>
</div>
</div>
{/* 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
/>