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

+18 -4
View File
@@ -337,10 +337,24 @@ export async function POST(request: Request) {
metadata: { orderId: order.id, orderNumber: order.orderNumber },
});
await prisma.order.update({
where: { id: order.id },
data: { stripeCheckoutSessionId: session.id },
});
// Best-effort: stripeCheckoutSessionId wird aktuell von keinem Lesepfad
// ausgewertet (fulfillOrder im Webhook identifiziert die Bestellung ueber
// session.metadata.orderId, nicht ueber dieses Feld) — schlaegt nur
// dieser Schreibzugriff fehl, waere es falsch, die bereits erstellte,
// echte und bezahlbare Stripe-Session deswegen zu verwerfen und der
// Kundschaft einen Fehler zu zeigen (Risiko: Retry legt eine zweite
// Bestellung samt zweiter Bestandsreservierung an).
try {
await prisma.order.update({
where: { id: order.id },
data: { stripeCheckoutSessionId: session.id },
});
} catch (error: unknown) {
console.error(
`Speichern von stripeCheckoutSessionId fuer Bestellung ${order.orderNumber} fehlgeschlagen (Session ${session.id} bleibt trotzdem gueltig):`,
error
);
}
return NextResponse.json({ url: session.url });
} catch (error: unknown) {