fix: Race Conditions bei Gutscheincode-Einloesung und Retouren-Erstattung
- Checkout: Gutscheincode-Nutzungslimit wird jetzt innerhalb derselben Serializable-Transaktion wie die Bestellanlage frisch nachgeprueft, statt nur einmalig vorab. Zwei gleichzeitige Checkouts mit demselben maxUses:1-Code konnten sich sonst beide durchsetzen. - Admin/Retouren: Erstattungsstatus wird jetzt atomar (bedingtes Update, ANGENOMMEN -> ERSTATTET) VOR dem Stripe-Aufruf gesetzt statt danach. Ein Doppelklick auf "Erstatten" konnte sonst zwei echte Stripe-Erstattungen ausloesen. Bei einem fehlschlagenden Stripe-Aufruf wird der Status wieder zurueckgesetzt, damit erneut versucht werden kann. Beide Luecken kamen aus dem Security-Assessment dieser Session. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
b6db9254a9
commit
c59913b75b
2 files changed
+107
-38
No files matched your search
@@ -68,8 +68,6 @@ export async function refundReturnRequestAction(formData: FormData): Promise<voi
|
||||
},
|
||||
},
|
||||
});
|
||||
// Nur eine angenommene Retoure kann erstattet werden — verhindert
|
||||
// Doppel-Erstattung, falls dieselbe Aktion mehrfach ausgeloest wird.
|
||||
if (!returnRequest || returnRequest.status !== "ANGENOMMEN") return;
|
||||
|
||||
const paymentIntentId = returnRequest.order.payment?.stripePaymentIntentId;
|
||||
@@ -77,11 +75,29 @@ export async function refundReturnRequestAction(formData: FormData): Promise<voi
|
||||
fail("/admin/retouren", "Keine Stripe-Zahlung zu dieser Bestellung hinterlegt — bitte manuell erstatten.");
|
||||
}
|
||||
|
||||
// Status ATOMAR und VOR dem Stripe-Aufruf auf ERSTATTET setzen (bedingt auf
|
||||
// den noch erwarteten "ANGENOMMEN"-Stand) statt erst danach: der
|
||||
// findUnique-Check allein ist nicht atomar — bei zwei schnell
|
||||
// aufeinanderfolgenden Klicks (Doppelklick, zwei offene Tabs) koennten
|
||||
// sonst beide Aufrufe die Pruefung bestehen und zwei echte
|
||||
// Stripe-Erstattungen ausloesen. Schlaegt der Stripe-Aufruf unten fehl,
|
||||
// wird der Status wieder zurueckgesetzt, damit die Erstattung erneut
|
||||
// versucht werden kann.
|
||||
const claimed = await prisma.returnRequest.updateMany({
|
||||
where: { id, status: "ANGENOMMEN" },
|
||||
data: { status: "ERSTATTET" },
|
||||
});
|
||||
if (claimed.count === 0) return;
|
||||
|
||||
const stripe = getStripeClient();
|
||||
await stripe.refunds.create({ payment_intent: paymentIntentId });
|
||||
try {
|
||||
await stripe.refunds.create({ payment_intent: paymentIntentId });
|
||||
} catch (error: unknown) {
|
||||
await prisma.returnRequest.update({ where: { id }, data: { status: "ANGENOMMEN" } });
|
||||
throw error;
|
||||
}
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.returnRequest.update({ where: { id }, data: { status: "ERSTATTET" } });
|
||||
await tx.payment.update({ where: { orderId: returnRequest.orderId }, data: { status: "REFUNDED" } });
|
||||
// Zurueckgesendete Ware wieder als verfuegbaren Bestand fuehren, analog
|
||||
// zur Stornierung im Checkout-Webhook.
|
||||
|
||||
Reference in new issue
Block a user