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),