manicanldes-backend/src/checkout/stock-allocation.ts

53 lines
1.8 KiB
TypeScript

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');
}