From 5848eb6ad56d2b05a091831c1832e668fea268c5 Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Sat, 26 Sep 2026 03:12:37 +0200 Subject: [PATCH] fix: Bestand bei Storno nach erstatteter Retoure nicht doppelt zurueckbuchen Wurde eine Retoure erstattet und die Bestellung danach auf "STORNIERT" gesetzt, buchten beide Schritte die Ware zurueck. Bei einer Testbestellung stieg der Bestand so von 105 auf 106 Glaeser. - Stornieren bucht nur noch zurueck, wenn keine Retoure erstattet ist (needsRestockOnCancel in lib/inventory.ts) - Retoure einer bereits stornierten Bestellung laesst sich nicht mehr erstatten, auch nicht im Wettlauf (Bedingung im atomaren Claim) Co-Authored-By: Claude Opus 5.5 (1M context) --- src/app/admin/actions/orders.ts | 7 +++++-- src/app/admin/retouren/actions.ts | 10 +++++++++- src/lib/inventory.test.ts | 18 ++++++++++++++++++ src/lib/inventory.ts | 16 +++++++++++++++- 4 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 src/lib/inventory.test.ts diff --git a/src/app/admin/actions/orders.ts b/src/app/admin/actions/orders.ts index 110d964..5c66dff 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 { restockOrderItems } from "@/lib/inventory"; +import { needsRestockOnCancel, restockOrderItems } from "@/lib/inventory"; import { checkOrderDeletable, needsRestockOnDelete } from "@/lib/order-delete"; import { getStripeClient } from "@/lib/stripe"; import { isLocale, defaultLocale } from "@/i18n/config"; @@ -139,6 +139,7 @@ export async function updateOrderStatusAction(formData: FormData): Promise stripeCheckoutSessionId: true, payment: { select: { status: true, stripePaymentIntentId: true } }, items: { select: { productId: true, quantity: true } }, + returnRequest: { select: { status: true } }, }, }); if (!previous) return null; @@ -164,7 +165,9 @@ export async function updateOrderStatusAction(formData: FormData): Promise }); if (claim.count === 1) { cancelledNow = true; - await restockOrderItems(tx, previous.items); + if (needsRestockOnCancel(previous.returnRequest?.status ?? null)) { + await restockOrderItems(tx, previous.items); + } } } else { await tx.order.update({ where: { id: orderId }, data: { status } }); diff --git a/src/app/admin/retouren/actions.ts b/src/app/admin/retouren/actions.ts index 71f44a5..6e6bb17 100644 --- a/src/app/admin/retouren/actions.ts +++ b/src/app/admin/retouren/actions.ts @@ -87,8 +87,16 @@ export async function refundReturnRequestAction(formData: FormData): Promise { + test("bucht zurueck, wenn es keine Retoure gibt", () => { + expect(needsRestockOnCancel(null)).toBe(true); + }); + + test("bucht zurueck, solange die Retoure noch nicht erstattet ist", () => { + expect(needsRestockOnCancel("ANGEFRAGT")).toBe(true); + expect(needsRestockOnCancel("ANGENOMMEN")).toBe(true); + expect(needsRestockOnCancel("ABGELEHNT")).toBe(true); + }); + + test("bucht nicht noch einmal zurueck, wenn die Retoure die Ware schon zurueckgebucht hat", () => { + expect(needsRestockOnCancel("ERSTATTET")).toBe(false); + }); +}); diff --git a/src/lib/inventory.ts b/src/lib/inventory.ts index d63f9ee..bc82077 100644 --- a/src/lib/inventory.ts +++ b/src/lib/inventory.ts @@ -1,4 +1,18 @@ -import type { Prisma } from "@/generated/prisma/client"; +import type { Prisma, ReturnStatus } from "@/generated/prisma/client"; + +/** + * Muss beim Stornieren einer Bestellung die Ware zurueckgebucht werden? + * + * Nein, wenn eine Retoure zu dieser Bestellung bereits erstattet wurde: + * refundReturnRequestAction (admin/retouren/actions.ts) hat die Ware dann + * schon zurueckgebucht. Ein zweites Gutschreiben wuerde den Bestand um Glaeser + * erhoehen, die es nicht gibt — genau das ist bei einer Testbestellung + * passiert (Retoure erstattet, danach "STORNIERT" gesetzt: +1 Glas zu viel). + * Gegenstueck zu needsRestockOnDelete in order-delete.ts. + */ +export function needsRestockOnCancel(returnStatus: ReturnStatus | null): boolean { + return returnStatus !== "ERSTATTET"; +} /** * Bucht reservierten/verkauften Bestand zurueck — genutzt beim Stornieren