fix(orders): cancelled orders cannot be reopened (stock restocked twice)
After refund + cancellation the stock rose by 2 instead of 1. Cause: the status form allowed moving an order out of STORNIERT. Cancelling restocks the goods and refunds the payment; reopening did neither in reverse, and the next cancellation passed the "not already cancelled" claim again and restocked a second time (the refund was not repeated because the payment was already REFUNDED, so only the stock showed it). STORNIERT is now a final state: - isOrderStatusChangeAllowed() in lib/inventory.ts next to the other cancel rules, with tests - updateOrderStatusAction rejects the change up front with a clear message and moves non-cancel statuses with an atomic "status <> STORNIERT" update, so a cancellation from a second tab cannot be undone in between - the order list and detail page no longer offer the status select for cancelled orders Existing stock that was restocked twice has to be corrected by hand. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
de82ad0233
commit
dc1523aafd
5 files changed
+62
-5
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import { needsRestockOnCancel, planInventoryEdit } from "./inventory";
|
||||
import { isOrderStatusChangeAllowed, needsRestockOnCancel, planInventoryEdit } from "./inventory";
|
||||
|
||||
describe("planInventoryEdit", () => {
|
||||
test("laesst den Bestand unberuehrt, wenn das Feld nicht geaendert wurde", () => {
|
||||
@@ -43,3 +43,21 @@ describe("needsRestockOnCancel", () => {
|
||||
expect(needsRestockOnCancel("ERSTATTET")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isOrderStatusChangeAllowed", () => {
|
||||
test("a cancelled order cannot be reopened (else a second cancel restocks again: +2 instead of +1)", () => {
|
||||
expect(isOrderStatusChangeAllowed("STORNIERT", "BEZAHLT")).toBe(false);
|
||||
expect(isOrderStatusChangeAllowed("STORNIERT", "NEU")).toBe(false);
|
||||
expect(isOrderStatusChangeAllowed("STORNIERT", "VERSENDET")).toBe(false);
|
||||
});
|
||||
|
||||
test("re-selecting the cancelled status is a harmless no-op", () => {
|
||||
expect(isOrderStatusChangeAllowed("STORNIERT", "STORNIERT")).toBe(true);
|
||||
});
|
||||
|
||||
test("active orders can move between statuses and be cancelled", () => {
|
||||
expect(isOrderStatusChangeAllowed("BEZAHLT", "VERSENDET")).toBe(true);
|
||||
expect(isOrderStatusChangeAllowed("VERSENDET", "STORNIERT")).toBe(true);
|
||||
expect(isOrderStatusChangeAllowed("NEU", "BEZAHLT")).toBe(true);
|
||||
});
|
||||
});
|
||||
+13
-1
@@ -1,4 +1,4 @@
|
||||
import type { Prisma, ReturnStatus } from "@/generated/prisma/client";
|
||||
import type { OrderStatus, Prisma, ReturnStatus } from "@/generated/prisma/client";
|
||||
|
||||
/**
|
||||
* Muss beim Stornieren einer Bestellung die Ware zurueckgebucht werden?
|
||||
@@ -42,6 +42,18 @@ export function needsRestockOnCancel(returnStatus: ReturnStatus | null): boolean
|
||||
return returnStatus !== "ERSTATTET";
|
||||
}
|
||||
|
||||
/**
|
||||
* "STORNIERT" ist ein Endzustand: beim Stornieren wurde die Ware
|
||||
* zurueckgebucht und die Zahlung erstattet. Liesse sich eine stornierte
|
||||
* Bestellung wieder auf einen aktiven Status setzen, stuende sie ohne
|
||||
* reservierte Ware und ohne Geld da — und ein erneutes Stornieren buchte die
|
||||
* Ware ein ZWEITES Mal zurueck (so geschehen: +2 statt +1 Glas). Wer die
|
||||
* Bestellung doch ausliefern will, legt eine neue an.
|
||||
*/
|
||||
export function isOrderStatusChangeAllowed(from: OrderStatus, to: OrderStatus): boolean {
|
||||
return from !== "STORNIERT" || to === "STORNIERT";
|
||||
}
|
||||
|
||||
/**
|
||||
* Bucht reservierten/verkauften Bestand zurueck — genutzt beim Stornieren
|
||||
* einer Bestellung (admin/actions.ts, api/checkout/webhook/route.ts) und bei
|
||||
|
||||
Reference in new issue
Block a user