fix: Race Condition bei DHL-Versandmarken-Erstellung
Zwei schnell aufeinanderfolgende Klicks auf "DHL-Versandmarke erstellen" konnten beide den findUnique-Check bestehen und zwei echte, kostenpflichtige DHL-Sendungen anlegen (gleiche Race-Condition wie beim zuvor gefixten Retouren-Erstattungs-Bug). Ein reines create()-mit-Unique-Constraint waere hier nicht ausreichend gewesen, da Shipment-Datensaetze bereits ohne DHL existieren koennen (automatisch bei Statuswechsel auf VERSENDET, oder manuell erfasst). Fix: Serializable-Transaktion claimt den Datensatz atomar mit einem Sentinel-Wert, bevor der DHL-Aufruf ueberhaupt startet; bei einem fehlschlagenden DHL-Aufruf wird der Sentinel wieder entfernt. Gefunden bei eigener Nachpruefung der neuen Admin-Actions dieser Session (apicore-sync.ts war bereits sauber idempotent, kein Fix noetig). Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
154b75e0d8
commit
9e8da18875
1 file changed
+45
@@ -7,6 +7,11 @@ import { getCompanyInfo } from "@/lib/company-info";
|
||||
import { createDhlShipmentLabel, DhlApiError, DhlNotConfiguredError, type DhlCreateShipmentResult } from "@/lib/dhl";
|
||||
import { requireAdmin, fail, str } from "./_shared";
|
||||
|
||||
// Platzhalterwert, waehrend eine Versandmarken-Anfrage bei DHL laeuft — siehe
|
||||
// Kommentar bei der Serializable-Transaktion unten. Kein echtes DHL-Format,
|
||||
// daher eindeutig von jeder realen Sendungsnummer unterscheidbar.
|
||||
const DHL_CLAIM_SENTINEL = "__dhl_label_pending__";
|
||||
|
||||
/**
|
||||
* Erstellt per DHL Parcel DE Shipping API eine Versandmarke fuer eine
|
||||
* bezahlte Bestellung und speichert die zurueckgegebene Sendungsnummer im
|
||||
@@ -42,6 +47,38 @@ export async function createDhlLabelAction(formData: FormData): Promise<void> {
|
||||
fail(detailPath, "Für diese Bestellung wurde bereits eine DHL-Sendungsnummer erfasst.");
|
||||
}
|
||||
|
||||
// Atomarer Claim VOR dem (kostenpflichtigen) DHL-Aufruf: der obige
|
||||
// findUnique-Check allein ist nicht ausreichend — zwei schnell
|
||||
// aufeinanderfolgende Klicks koennten sonst beide passieren und zwei
|
||||
// echte, kostenpflichtige DHL-Sendungen anlegen (gleiche Race-Condition
|
||||
// wie beim Retouren-Erstattungs-Fix). Ein Shipment-Datensatz kann bereits
|
||||
// OHNE DHL existieren (z.B. automatisch beim Statuswechsel auf
|
||||
// "VERSENDET", siehe admin/actions/orders.ts, oder manuell erfasst) —
|
||||
// ein reines "create()" mit Unique-Constraint waere daher in diesen
|
||||
// Faellen faelschlich blockiert. Serializable-Transaktion + Sentinel-Wert
|
||||
// statt der echten Sendungsnummer loest beide Faelle korrekt atomar.
|
||||
let claimed: boolean;
|
||||
try {
|
||||
claimed = await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const existing = await tx.shipment.findUnique({ where: { orderId }, select: { trackingNumber: true } });
|
||||
if (existing?.trackingNumber) return false;
|
||||
await tx.shipment.upsert({
|
||||
where: { orderId },
|
||||
create: { orderId, carrier: "DHL", trackingNumber: DHL_CLAIM_SENTINEL },
|
||||
update: { carrier: "DHL", trackingNumber: DHL_CLAIM_SENTINEL },
|
||||
});
|
||||
return true;
|
||||
},
|
||||
{ isolationLevel: "Serializable" }
|
||||
);
|
||||
} catch {
|
||||
claimed = false;
|
||||
}
|
||||
if (!claimed) {
|
||||
fail(detailPath, "Für diese Bestellung wurde bereits eine DHL-Sendungsnummer erfasst oder eine Anfrage läuft gerade.");
|
||||
}
|
||||
|
||||
const totalWeightGrams = order.items.reduce((sum, item) => sum + item.weightGrams * item.quantity, 0);
|
||||
const company = await getCompanyInfo();
|
||||
|
||||
@@ -76,6 +113,14 @@ export async function createDhlLabelAction(formData: FormData): Promise<void> {
|
||||
},
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
// Claim zuruecksetzen, damit ein erneuter Versuch moeglich ist — nur
|
||||
// wenn der Sentinel-Wert seitdem nicht durch etwas anderes (z.B. eine
|
||||
// manuelle Tracking-Eingabe waehrend der laufenden Anfrage) ersetzt
|
||||
// wurde, um dessen Aenderung nicht versehentlich zu ueberschreiben.
|
||||
await prisma.shipment
|
||||
.updateMany({ where: { orderId, trackingNumber: DHL_CLAIM_SENTINEL }, data: { trackingNumber: null } })
|
||||
.catch(() => {});
|
||||
|
||||
if (error instanceof DhlNotConfiguredError) {
|
||||
fail(detailPath, error.message);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user