import { randomUUID } from 'node:crypto'; import type { Prisma } from '../generated/prisma/client'; import type { Principal } from '../identity/identity.types'; import { AppError } from '../common/errors/app-error'; import { lockStock, reservedQuantity } from '../inventory/stock-lock'; import { commandHash } from '../inventory/inventory.policy'; export async function holdOrderStock( tx: Prisma.TransactionClient, actor: Principal, orderId: string, expiresAt: Date, lines: { variantId: string; quantity: number }[], ) { const stocks = await tx.stockItem.findMany({ where: { organizationId: actor.organizationId, variantId: { in: lines.map((line) => line.variantId) }, }, orderBy: { id: 'asc' }, take: 201, }); if (stocks.length > 200) throw new AppError('STOCK_ALLOCATION_LIMIT'); const now = new Date(); const remaining = new Map( lines.map((line) => [line.variantId, line.quantity]), ); for (const candidate of stocks) { const stock = await lockStock(tx, actor.organizationId, candidate.id); const needed = remaining.get(stock.variantId)!; if (!needed) continue; const available = stock.onHand - (await reservedQuantity(tx, stock.id, now)); const quantity = Math.min(needed, available); if (quantity <= 0) continue; await tx.stockReservation.create({ data: { stockItemId: stock.id, organizationId: actor.organizationId, userId: actor.userId, orderId, quantity, expiresAt, idempotencyKey: randomUUID(), requestHash: commandHash(orderId, stock.id, quantity), }, }); remaining.set(stock.variantId, needed - quantity); } if ([...remaining.values()].some((quantity) => quantity > 0)) throw new AppError('STOCK_INSUFFICIENT'); }