import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core"; import { Decimal } from "decimal.js"; import { transferOrderLifecycle } from "../db/transferOrder.lifecycle.generated"; import type { Transaction } from "../generated/kysely-tailordb"; import { getOpenStockReservationQuantity } from "../internal/stockReservation"; import { EmptyTransferLinesError, InsufficientAvailableStockError, InvalidStateTransitionError, TransferOrderNotFoundError, } from "../lib/errors.generated"; export interface OpenTransferOrderInput { id: string; } export async function run(db: Transaction, input: OpenTransferOrderInput, _ctx: CommandContext) { const existing = await db .selectFrom("TransferOrder") .selectAll() .where("id", "=", input.id) .forUpdate() .executeTakeFirst(); if (!existing) { return err(new TransferOrderNotFoundError(input.id)); } const nextStatus = transferOrderLifecycle.tryTransition(existing.status, "open"); if (!nextStatus) { return err(new InvalidStateTransitionError(input.id)); } const lines = await db .selectFrom("TransferOrderLine") .selectAll() .where("transferOrderId", "=", input.id) .orderBy("id", "asc") .execute(); if (lines.length === 0) { return err(new EmptyTransferLinesError(input.id)); } const demandByItem = new Map(); for (const line of lines) { demandByItem.set( line.itemId, (demandByItem.get(line.itemId) ?? new Decimal(0)).plus(line.orderedQuantity), ); } const itemIds = [...demandByItem.keys()]; const locationIds = await db .selectFrom("StorageLocation") .select("id") .where("siteId", "=", existing.sourceSiteId) .where("status", "=", "ACTIVE") .execute(); if (locationIds.length === 0) { return err(new InsufficientAvailableStockError(existing.sourceSiteId)); } const sourceSiteStockLevels = await db .selectFrom("StockLevel") .selectAll() .where("itemId", "in", itemIds) .where( "storageLocationId", "in", locationIds.map((location) => location.id), ) .where("stockType", "=", "AVAILABLE") .orderBy("storageLocationId", "asc") .forUpdate() .execute(); const openReservations = await db .selectFrom("StockReservation") .selectAll() .where("itemId", "in", itemIds) .where("siteId", "=", existing.sourceSiteId) .where("status", "=", "OPEN") .execute(); const availableByItem = new Map(); for (const stockLevel of sourceSiteStockLevels) { availableByItem.set( stockLevel.itemId, (availableByItem.get(stockLevel.itemId) ?? new Decimal(0)).plus(stockLevel.quantity), ); } for (const reservation of openReservations) { availableByItem.set( reservation.itemId, (availableByItem.get(reservation.itemId) ?? new Decimal(0)).minus( getOpenStockReservationQuantity(reservation), ), ); } for (const [itemId, demandQuantity] of demandByItem.entries()) { const availableQuantity = availableByItem.get(itemId); if (!availableQuantity || availableQuantity.lt(demandQuantity)) { return err(new InsufficientAvailableStockError(`${itemId}:${existing.sourceSiteId}`)); } } const transferOrder = await db .updateTable("TransferOrder") .set({ status: nextStatus }) .where("id", "=", input.id) .returningAll() .executeTakeFirstOrThrow(); const stockReservations = await db .insertInto("StockReservation") .values( lines.map((line) => ({ sourceType: "TRANSFER_ORDER" as const, sourceId: existing.id, sourceLineId: line.id, itemId: line.itemId, siteId: existing.sourceSiteId, storageLocationId: null, reservedQuantity: line.orderedQuantity, consumedQuantity: "0", status: "OPEN" as const, requiredDate: existing.plannedShipmentDate, })), ) .returningAll() .execute(); const supplyPlans = await db .insertInto("InventorySupplyPlan") .values( lines.map((line) => ({ sourceType: "TRANSFER_ORDER" as const, sourceId: existing.id, sourceLineId: line.id, itemId: line.itemId, siteId: existing.destinationSiteId, expectedQuantity: line.orderedQuantity, receivedQuantity: "0", status: "OPEN" as const, expectedDate: existing.expectedReceiptDate, })), ) .returningAll() .execute(); return ok({ transferOrder, stockReservations, supplyPlans }); }