fix: harden payment/shipping paths against partial-failure edge cases

Dispatched a read-only silent-failure audit (silent-failure-hunter
agent) over the checkout/webhook/DHL/mail code paths, then verified
each finding by hand before fixing. Four confirmed, real gaps:

- Stripe webhook (fulfillOrder): Payment/Invoice creation and the
  confirmation email ran after the order's status flip to "BEZAHLT",
  outside any try/catch. If any of those three failed (DB blip), the
  handler threw uncaught -> Stripe retried -> the retry's
  status==="NEU" guard now found 0 rows and silently skipped the
  retry forever, leaving a paid order with no invoice, no payment
  record, and no confirmation email, with nothing logged. Fixed by
  allowing "BEZAHLT" as a valid retry entry point (idempotent
  upserts) and wrapping the post-transaction block in try/catch that
  logs loudly and re-throws (so Stripe still retries).

- DHL label creation: the already-billed tracking number's DB write
  sat outside the try/catch that protects the (paid) DHL API call.
  A failed write there lost the tracking number with zero record of
  it, while the claim sentinel permanently blocked any retry through
  the UI. Fixed by wrapping the write, logging the tracking number
  for manual reconciliation, and deliberately leaving the claim in
  place (not resettable) to avoid triggering a second, separately
  billed label.

- Checkout route: a DB write failure for the (currently unread,
  write-only) stripeCheckoutSessionId field discarded an already
  live, payable Stripe session and showed the customer a generic
  error, risking a duplicate order + stock reservation on retry.
  Made that specific write best-effort/non-fatal.

- DHL fetch calls had no request timeout, so a hung (non-erroring)
  DHL API could block an admin action indefinitely. Added a 20s
  AbortSignal.timeout to both.

Verified: tsc clean, 151/151 existing tests still pass. No dedicated
DB-integration test harness exists yet for route/webhook handlers in
this codebase (only pure-function unit tests) — building one is a
separate, larger undertaking than this fix warrants.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Sonnet 5 committed 2026-09-07 13:35:04 +02:00
1 parent 229a1dc435
commit 8b5d5d095c
4 files changed
+130 -65

No files matched your search

+8
View File
@@ -55,6 +55,12 @@ export class DhlApiError extends Error {
// Konfiguration
// ---------------------------------------------------------------------------
// Ohne Timeout wuerde eine haengende (nicht fehlerhafte, sondern einfach nie
// antwortende) DHL-API die aufrufende Admin-Server-Action unbegrenzt
// blockieren und dabei den DHL_CLAIM_SENTINEL-Claim (siehe admin/actions/dhl.ts)
// auf unbestimmte Zeit offen halten.
const DHL_REQUEST_TIMEOUT_MS = 20_000;
interface DhlConfig {
apiKey: string;
apiSecret: string;
@@ -124,6 +130,7 @@ async function fetchAccessToken(config: DhlConfig): Promise<string> {
client_id: config.apiKey,
client_secret: config.apiSecret,
}),
signal: AbortSignal.timeout(DHL_REQUEST_TIMEOUT_MS),
});
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unbekannter Netzwerkfehler";
@@ -291,6 +298,7 @@ export async function createDhlShipmentLabel(
"dhl-api-key": config.apiKey,
},
body: JSON.stringify(requestBody),
signal: AbortSignal.timeout(DHL_REQUEST_TIMEOUT_MS),
});
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unbekannter Netzwerkfehler";