Replace hand-duplicated VIES/VAT-ID/PLZ/tracking logic with shared package

Deletes app/lib/vies.ts, vatId.ts, tracking.ts in favor of the newly unified @einfach-produktiv/invoicing modules — fixes the actual PLZ inconsistency (this repo already validated per-country digit counts; the backend hardcoded German-only) rather than just deduplicating code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-31 15:41:01 +00:00
parent 230b8ebaad
commit b6a826dc46
10 changed files with 15 additions and 113 deletions
+3 -1
View File
@@ -1,6 +1,8 @@
// Thin client for the backend's DHL custom endpoints (src/lib/endpoints/
// dhlValidatePostNumber.ts, dhlAutocompleteAddress.ts) — own copy per repo,
// same "no shared package yet" convention as app/lib/tracking.ts.
// unlike vies.ts/vatId.ts/tracking.ts (now shared via @einfach-produktiv/invoicing),
// since this proxies to Payload endpoints specific to this repo's own DHL wiring,
// not portable logic.
const PAYLOAD_URL = process.env.PAYLOAD_URL || "https://payload.mk360.de";
const TENANT_SLUG = "einfach-produktiv";
-26
View File
@@ -1,26 +0,0 @@
// Mirrors the Payload backend's own src/lib/tracking.ts — same carrier
// set/labels/URL patterns, kept in sync by hand (two separate
// deployments, no shared package). Used to render a clickable tracking
// link on /konto/bestellungen/[orderNumber]; the backend's copy builds
// the same link for the order-shipped email.
export const CARRIER_LABELS: Record<string, string> = {
dhl: "DHL",
dpd: "DPD",
hermes: "Hermes",
ups: "UPS",
gls: "GLS",
other: "Sonstiger Versanddienstleister",
};
const CARRIER_TRACKING_URL: Record<string, (trackingNumber: string) => string> = {
dhl: (n) => `https://www.dhl.de/de/privatkunden/dhl-sendungsverfolgung.html?piececode=${encodeURIComponent(n)}`,
dpd: (n) => `https://tracking.dpd.de/status/de_DE/parcel/${encodeURIComponent(n)}`,
hermes: (n) => `https://www.myhermes.de/empfangen/sendungsverfolgung/sendungsinformation/#${encodeURIComponent(n)}`,
ups: (n) => `https://www.ups.com/track?loc=de_DE&tracknum=${encodeURIComponent(n)}`,
gls: (n) => `https://www.gls-pakete.de/sendungsverfolgung?trackingNumber=${encodeURIComponent(n)}`,
};
export function buildTrackingUrl(carrier: string | null | undefined, trackingNumber: string | null | undefined): string | null {
if (!carrier || !trackingNumber) return null;
return CARRIER_TRACKING_URL[carrier]?.(trackingNumber) ?? null;
}
-15
View File
@@ -1,15 +0,0 @@
// Mirrors the backend's own USt-IdNr. validation exactly (Orders.ts/
// Customers.ts/CompanySettings.ts in the Payload repo) — kept as a plain
// client+server-safe helper here since this repo's frontend needs the same
// check twice (checkout's instant client-side pattern + api/checkout's own
// server-side re-validation, same "never trust the client" reasoning as
// every other checkout field).
const VAT_ID_PATTERN = /^[A-Z]{2}[A-Z0-9]{2,12}$/;
export function normalizeVatId(value: string): string {
return value.toUpperCase().trim();
}
export function isValidVatId(value: string): boolean {
return VAT_ID_PATTERN.test(value);
}
-58
View File
@@ -1,58 +0,0 @@
// Server-only — calls the European Commission's public VIES REST API to
// confirm an EU VAT ID is actually registered, not just correctly
// formatted (see lib/vatId.ts's own comment: format alone is never
// enough to zero-rate an invoice). Confirmed live and working against
// the real endpoint 2026-07-23 (POST {countryCode, vatNumber} →
// {valid: boolean, ...}) — this is the Commission's own documented REST
// API, not a guess.
const VIES_URL = "https://ec.europa.eu/taxation_customs/vies/rest-api/check-vat-number";
export type ViesCheckResult =
| { ok: true; valid: boolean; name: string | null; address: string | null }
| { ok: false; reason: string };
// `vatNumber` must NOT include the country prefix (VIES wants it split
// out) — callers pass the full "DE123456789"-shaped id and this function
// does the splitting, since every call site already has the normalized
// full id (see lib/vatId.ts's normalizeVatId()) rather than the two parts
// separately.
export async function checkVatIdViaVies(vatId: string): Promise<ViesCheckResult> {
const countryCode = vatId.slice(0, 2);
const vatNumber = vatId.slice(2);
if (!countryCode || !vatNumber) return { ok: false, reason: "Ungültiges USt-IdNr.-Format." };
try {
// 8s timeout — VIES is a shared EU-wide government service with no
// uptime SLA to this shop; a slow/unreachable response must not hang
// checkout indefinitely. Callers treat `ok: false` as "couldn't
// confirm" and fail closed (no exemption), never as "confirmed invalid".
const res = await fetch(VIES_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ countryCode, vatNumber }),
signal: AbortSignal.timeout(8000),
});
if (!res.ok) return { ok: false, reason: `VIES antwortete mit ${res.status}` };
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),
name: data.name && data.name !== "---" ? data.name : null,
address: data.address && data.address !== "---" ? data.address : null,
};
} catch (err) {
return { ok: false, reason: err instanceof Error ? err.message : "VIES ist gerade nicht erreichbar." };
}
}