diff --git a/src/app/admin/actions/orders.ts b/src/app/admin/actions/orders.ts index 5c66dff..2d84210 100644 --- a/src/app/admin/actions/orders.ts +++ b/src/app/admin/actions/orders.ts @@ -6,7 +6,7 @@ import { logAdminAction } from "@/lib/audit-log"; import { sendMail } from "@/lib/mail"; import { shipmentEmail, pickupReadyEmail } from "@/lib/email-templates"; import { getCarrierTrackingUrl } from "@/lib/shipment-tracking"; -import { needsRestockOnCancel, restockOrderItems } from "@/lib/inventory"; +import { isOrderStatusChangeAllowed, needsRestockOnCancel, restockOrderItems } from "@/lib/inventory"; import { checkOrderDeletable, needsRestockOnDelete } from "@/lib/order-delete"; import { getStripeClient } from "@/lib/stripe"; import { isLocale, defaultLocale } from "@/i18n/config"; @@ -144,6 +144,13 @@ export async function updateOrderStatusAction(formData: FormData): Promise }); if (!previous) return null; + // Stornierte Bestellungen bleiben storniert (siehe + // isOrderStatusChangeAllowed) — sonst bucht ein erneutes Stornieren die + // Ware ein zweites Mal zurueck. + if (!isOrderStatusChangeAllowed(previous.status, status)) { + return { ...previous, shipment: null, cancelledNow: false, blocked: true }; + } + // Nur wahr, wenn DIESER Aufruf die Bestellung tatsaechlich storniert hat. // Steuert weiter unten Erstattung und Schliessen der Zahlungssitzung — // beides darf bei einem zweiten Klick auf eine bereits stornierte @@ -170,7 +177,15 @@ export async function updateOrderStatusAction(formData: FormData): Promise } } } else { - await tx.order.update({ where: { id: orderId }, data: { status } }); + // Atomar gegen den aktuellen Zeilenstand: hat ein zweiter Tab die + // Bestellung zwischen Lesen und Schreiben storniert, bleibt sie das. + const moved = await tx.order.updateMany({ + where: { id: orderId, status: { not: "STORNIERT" } }, + data: { status }, + }); + if (moved.count === 0) { + return { ...previous, shipment: null, cancelledNow: false, blocked: true }; + } } let shipment: { carrier: string | null; trackingNumber: string | null } | null = null; @@ -197,12 +212,18 @@ export async function updateOrderStatusAction(formData: FormData): Promise }); } - return { ...previous, shipment, cancelledNow }; + return { ...previous, shipment, cancelledNow, blocked: false }; }); if (!result) { return fail(detailPath, "Die Bestellung wurde nicht gefunden — möglicherweise wurde sie zwischenzeitlich gelöscht."); } + if (result.blocked) { + return fail( + detailPath, + "Diese Bestellung ist storniert: die Ware ist zurückgebucht und die Zahlung erstattet. Sie lässt sich nicht wieder öffnen — soll die Ware doch raus, bitte eine neue Bestellung anlegen." + ); + } // undefined = keine E-Mail faellig; false = faellig, aber nicht versendet. let mailSent: boolean | undefined; diff --git a/src/app/admin/bestellungen/[id]/page.tsx b/src/app/admin/bestellungen/[id]/page.tsx index 3e4e791..2c6785d 100644 --- a/src/app/admin/bestellungen/[id]/page.tsx +++ b/src/app/admin/bestellungen/[id]/page.tsx @@ -57,6 +57,8 @@ export default async function AdminOrderDetailPage(props: PageProps<"/admin/best

{formatDate(order.createdAt)}

+ {/* Storniert ist ein Endzustand (isOrderStatusChangeAllowed) — keine Auswahl anbieten, die der Server ablehnt. */} + {order.status !== "STORNIERT" ? (