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
+19 -10
View File
@@ -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;
}
}
}
+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
/>