From 19f6559c297d2e3ef2607c4530a8f0e81523c977 Mon Sep 17 00:00:00 2001 From: Marco Date: Thu, 23 Jul 2026 21:21:46 +0000 Subject: [PATCH] Fix VIES check treating "member state unavailable" as "invalid" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VIES answers HTTP 200 even when it couldn't actually perform the check (actionSucceed: false, e.g. MS_UNAVAILABLE — Germany's own national gateway does this fairly regularly). checkVatIdViaVies() only ever read data.valid, which is absent on that response shape, so it silently read as valid: false — a real, currently-registered German VAT ID (reported: DE351362947) looked rejected. Worse, /api/checkout/validate- vat then wrapped even a correctly-returned ok:false as { ok: true, valid: false }, which the client reads as "invalid" rather than "unavailable" — the actual bug the user hit, compounding the vies.ts gap. Both are fixed now: an unconfirmable check surfaces to the client as ok:false, which CheckoutContent.tsx's handleVatIdBlur already correctly renders as "USt-IdNr.-Prüfung derzeit nicht möglich" instead of a rejection. Same fix applied to the payload backend's own copy of vies.ts (company-settings' VAT check). Co-Authored-By: Claude Sonnet 5 --- app/api/checkout/validate-vat/route.ts | 11 ++++++++++- app/lib/vies.ts | 14 +++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/app/api/checkout/validate-vat/route.ts b/app/api/checkout/validate-vat/route.ts index 96fdc45..e8e545c 100644 --- a/app/api/checkout/validate-vat/route.ts +++ b/app/api/checkout/validate-vat/route.ts @@ -24,7 +24,16 @@ export async function POST(request: Request) { const result = await checkVatIdViaVies(normalized); if (!result.ok) { - return NextResponse.json({ ok: true, valid: false, reason: `USt-IdNr.-Prüfung derzeit nicht möglich (${result.reason}).` }); + // `ok: false` here means "VIES couldn't confirm this one way or the + // other" (unreachable, or the member state's own gateway is briefly + // down — `MS_UNAVAILABLE`, which VIES itself answers 200 for, not an + // error status) — NOT "confirmed invalid". Previously this branch + // still answered `{ ok: true, valid: false }`, which the client reads + // as a rejected VAT ID (`vatIdViesStatus = "invalid"`) instead of + // "couldn't check right now" (`"unavailable"`) — a real, currently + // registered VAT ID looked wrong to the customer whenever VIES (or + // just Germany's own national gateway) had a hiccup. + return NextResponse.json({ ok: false, reason: result.reason }); } return NextResponse.json({ ok: true, valid: result.valid, name: result.name }); } diff --git a/app/lib/vies.ts b/app/lib/vies.ts index 7b3b7c9..fa319b4 100644 --- a/app/lib/vies.ts +++ b/app/lib/vies.ts @@ -33,7 +33,19 @@ export async function checkVatIdViaVies(vatId: string): Promise signal: AbortSignal.timeout(8000), }); if (!res.ok) return { ok: false, reason: `VIES antwortete mit ${res.status}` }; - const data: { valid?: boolean; name?: string; address?: string } = await res.json(); + const data: { actionSucceed?: boolean; valid?: boolean; name?: string; address?: string; errorWrappers?: { error?: string }[] } = await res.json(); + // VIES answers 200 even when it couldn't actually perform the check — + // `actionSucceed: false` (e.g. `MS_UNAVAILABLE`, the member state's own + // national gateway being temporarily down — Germany's in particular is + // known to do this) means "couldn't confirm", not "confirmed invalid". + // Without this check a `MS_UNAVAILABLE` response fell through to + // `Boolean(data.valid)` on a body that has no `valid` field at all, + // silently reading as `valid: false` — a real, currently-registered VAT + // ID would then look rejected instead of "VIES unavailable, try again". + if (data.actionSucceed === false) { + const reason = data.errorWrappers?.[0]?.error ?? "VIES konnte die Anfrage nicht bearbeiten."; + return { ok: false, reason: `VIES: ${reason}` }; + } return { ok: true, valid: Boolean(data.valid),