fix: Stripe-Ratenplan bei Prüfung anhalten, doppelte Pläne verhindern
Befunde aus dem Sicherheits-Review des vorherigen Commits: - Gerät eine Buchung in "Prüfung nötig" (Rückbuchung, Doppelzahlung ...), wird der Stripe-Plan beendet und offene Rechnungen storniert. Nach Bestätigung legt der Worker einen neuen Plan für die offenen Raten an. Vorher zog Stripe auch bei Buchungen in Prüfung weiter ein. - Vor dem Anlegen wird bei Stripe nach einem laufenden Plan der Buchung gesucht; der Idempotenzschlüssel enthält Generation und Start. Verhindert einen zweiten Plan nach verlorener ID und die Rückgabe des alten, beendeten Plans nach einer Prüfung. - Start bei bereits verstrichener Fälligkeit deterministisch am nächsten 9-Uhr-Termin, letzte Rate bleibt am Stichtag. - Rückbuchungs-Event meldet die betroffene Buchung zurück. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
c1ad9ad2ca
commit
1ce7947d93
8 files changed
+168
-23
No files matched your search
+13
-3
@@ -13,7 +13,7 @@ import type Stripe from "stripe";
|
||||
import { Prisma } from "@/generated/prisma/client";
|
||||
import { STRIPE_ACTOR, type Actor } from "./audit";
|
||||
import type { Db } from "./db";
|
||||
import { ensureInstallmentSchedule, INSTALLMENT_PLAN_KIND, settleInstallmentInvoice } from "./installment-billing";
|
||||
import { ensureInstallmentSchedule, holdSchedulesInReview, INSTALLMENT_PLAN_KIND, settleInstallmentInvoice } from "./installment-billing";
|
||||
import { normalizePaymentStatus, type CheckoutSessionInfo, type PaymentGateway } from "./payment-gateway";
|
||||
import {
|
||||
applyCheckoutExpired,
|
||||
@@ -170,8 +170,10 @@ async function dispatch(db: Db, gateway: PaymentGateway, event: Stripe.Event, ct
|
||||
}
|
||||
case "charge.dispute.created": {
|
||||
const piId = idOf(event.data.object.payment_intent);
|
||||
if (piId) await db.$transaction((tx) => applyDispute(tx, piId, eventCtx(ctx)));
|
||||
return [];
|
||||
if (!piId) return [];
|
||||
await db.$transaction((tx) => applyDispute(tx, piId, eventCtx(ctx)));
|
||||
const disputed = await db.installment.findUnique({ where: { stripePaymentIntentId: piId }, select: { bookingId: true } });
|
||||
return disputed ? [disputed.bookingId] : [];
|
||||
}
|
||||
default:
|
||||
return [];
|
||||
@@ -218,6 +220,14 @@ export async function handleStripeEvent(
|
||||
try {
|
||||
const bookingIds = await dispatch(db, gateway, event, ctx);
|
||||
await db.stripeEvent.update({ where: { id: event.id }, data: { status: "PROCESSED", processedAt: new Date(), error: null } });
|
||||
// Geriet eine Buchung in Pruefung (Rueckbuchung, Doppelzahlung ...),
|
||||
// zieht Stripe bis zur Entscheidung nichts weiter ein. Scheitert das
|
||||
// hier, erledigt es der Worker.
|
||||
if (bookingIds.length > 0) {
|
||||
await holdSchedulesInReview(db, gateway, bookingIds, STRIPE_ACTOR).catch((error: unknown) => {
|
||||
console.error(`[webhook] Ratenplan nicht angehalten: ${error instanceof Error ? error.message : "unbekannt"}`);
|
||||
});
|
||||
}
|
||||
return { outcome: "processed", bookingIds };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message.slice(0, 500) : "unbekannter Fehler";
|
||||
|
||||
Reference in new issue
Block a user