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 { createDhlShipmentLabel, DhlApiError, DhlNotConfiguredError, type DhlCreateShipmentResult } from "@/lib/dhl";
|
||||||
import { requireAdmin, fail, str } from "./_shared";
|
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
|
* Erstellt per DHL Parcel DE Shipping API eine Versandmarke fuer eine
|
||||||
* bezahlte Bestellung und speichert die zurueckgegebene Sendungsnummer im
|
* 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.");
|
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 totalWeightGrams = order.items.reduce((sum, item) => sum + item.weightGrams * item.quantity, 0);
|
||||||
const company = await getCompanyInfo();
|
const company = await getCompanyInfo();
|
||||||
|
|
||||||
@@ -76,6 +113,14 @@ export async function createDhlLabelAction(formData: FormData): Promise<void> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error: unknown) {
|
} 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) {
|
if (error instanceof DhlNotConfiguredError) {
|
||||||
fail(detailPath, error.message);
|
fail(detailPath, error.message);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user