import type { OrderStatus, 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 type InventoryEditPlan = | { kind: "keep" } | { kind: "set"; expected: number; quantity: number } | { kind: "reload" }; /** * Was soll das Speichern des Produkt-Formulars mit dem Lagerbestand tun? * * Das Formular schickt neben dem eingegebenen Bestand ("submitted") den * Bestand mit, der beim Oeffnen angezeigt wurde ("loaded", verstecktes Feld). * Frueher wurde der Formularwert immer fest geschrieben: wurde zwischen * Oeffnen und Speichern ein Glas verkauft, kam es durch das Speichern * wieder in den Bestand, ohne dass es je zurueckkam. * * - Feld unveraendert: Bestand nicht anfassen ("keep"). * - Feld geaendert: nur schreiben, solange der Bestand noch "loaded" ist * ("set"; die Bedingung prueft updateProductAction atomar im UPDATE). * - "loaded" fehlt oder ist ungueltig: neu laden lassen ("reload"), statt * zu raten, welchen Stand die Imkerei vor Augen hatte. */ export function planInventoryEdit(input: { loaded: string; submitted: number }): InventoryEditPlan { if (!/^\d+$/.test(input.loaded)) return { kind: "reload" }; const loaded = Number(input.loaded); if (!Number.isSafeInteger(loaded)) return { kind: "reload" }; if (loaded === input.submitted) return { kind: "keep" }; return { kind: "set", expected: loaded, quantity: input.submitted }; } export function needsRestockOnCancel(returnStatus: ReturnStatus | null): boolean { return returnStatus !== "ERSTATTET"; } /** * Eine stornierte Bestellung wieder zu oeffnen braucht eine ausdrueckliche * Bestaetigung: beim Stornieren wurde die Ware zurueckgebucht und die Zahlung * erstattet. Frueher ging das mit einem Klick — und ohne Gegenbuchung. Ein * erneutes Stornieren buchte die Ware dann ein ZWEITES Mal zurueck (so * geschehen: +2 statt +1 Glas). */ export function needsConfirmToChangeStatus(from: OrderStatus, to: OrderStatus): boolean { return from === "STORNIERT" && to !== "STORNIERT"; } /** * Wird beim Wiederoeffnen Ware vom Bestand abgezogen? Genau dann, wenn das * Stornieren sie zurueckgebucht hatte (needsRestockOnCancel) — Buchung und * Gegenbuchung bleiben so immer paarweise. */ export function needsReserveOnReopen(returnStatus: ReturnStatus | null): boolean { return needsRestockOnCancel(returnStatus); } export class InsufficientStockError extends Error { constructor(readonly productName: string) { super(`Nicht genug Bestand fuer ${productName}`); this.name = "InsufficientStockError"; } } /** * Zieht die Ware einer Bestellung vom Bestand ab — atomar je Produkt und nur, * wenn genug da ist (wie die Reservierung im Checkout). Reicht es fuer ein * Produkt nicht, wirft InsufficientStockError; die umgebende Transaktion * macht dann auch die schon abgezogenen Positionen rueckgaengig. */ export async function reserveOrderItems( tx: Prisma.TransactionClient, items: { productId: string; quantity: number; productName: string; product?: { category: string } | null }[] ): Promise { for (const item of items) { // Patenschafts-Traegerprodukte fuehren keinen Bestand (eine Urkunde ist // nie ausverkauft) — ohne diese Ausnahme scheiterte das Wiederoeffnen // jeder Bestellung mit Patenschaft. Der Honig des Pakets ist eine eigene // Position und wird normal reserviert. if (item.product?.category === "PATENSCHAFT") continue; const reserved = await tx.inventory.updateMany({ where: { productId: item.productId, quantity: { gte: item.quantity } }, data: { quantity: { decrement: item.quantity } }, }); if (reserved.count === 0) throw new InsufficientStockError(item.productName); } } /** * Bucht reservierten/verkauften Bestand zurueck — genutzt beim Stornieren * einer Bestellung (admin/actions.ts, api/checkout/webhook/route.ts) und bei * einer angenommenen Retoure (admin/retouren/actions.ts). Alle drei Stellen * hatten dieselbe Schleife dupliziert; jetzt ein gemeinsamer Aufruf. */ export async function restockOrderItems( tx: Prisma.TransactionClient, items: { productId: string; quantity: number }[] ): Promise { for (const item of items) { await tx.inventory.updateMany({ where: { productId: item.productId }, data: { quantity: { increment: item.quantity } }, }); } }