53 lines
1.8 KiB
TypeScript
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');
|
|
}
|