diff --git a/src/app/admin/actions/dhl.ts b/src/app/admin/actions/dhl.ts index ece5bf3..4c39f0d 100644 --- a/src/app/admin/actions/dhl.ts +++ b/src/app/admin/actions/dhl.ts @@ -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 { 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 { }, }); } 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); }