import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction } from "../generated/kysely-tailordb"; import { isPositiveDecimal } from "../internal/transferOrderProgress"; import { CrossCompanyTransferNotSupportedError, EmptyTransferLinesError, InvalidQuantityError, InvalidTransferLocationsError, ItemNotFoundError, SiteNotFoundError, } from "../lib/errors.generated"; import type { ItemManagementQueries, OrganizationQueries } from "../module"; export interface TransferOrderLineInput { itemId: string; orderedQuantity: string; } export interface TransferOrderHeaderInput { sourceSiteId: string; destinationSiteId: string; plannedShipmentDate: Date; expectedReceiptDate: Date; } export interface CreateTransferOrderInput { header: TransferOrderHeaderInput; lines: TransferOrderLineInput[]; } export async function run( db: Transaction, input: { header: TransferOrderHeaderInput & Partial; lines: (TransferOrderLineInput & Partial)[]; }, ctx: CommandContext, itemManagementQueries: Pick, organizationQueries: Pick, ) { const { header, lines } = input; const { sourceSiteId, destinationSiteId, plannedShipmentDate, expectedReceiptDate } = header; if (sourceSiteId === destinationSiteId) { return err(new InvalidTransferLocationsError(sourceSiteId)); } if (lines.length === 0) { return err(new EmptyTransferLinesError(sourceSiteId)); } const siteCompanyIds: string[] = []; for (const siteId of [sourceSiteId, destinationSiteId]) { const { site } = (await organizationQueries.getSite(db, { id: siteId }, ctx)).value; if (!site) { return err(new SiteNotFoundError(siteId)); } siteCompanyIds.push(site.companyId); } // Cross-company transfers are out of scope; stock moves only within one company. if (siteCompanyIds[0] !== siteCompanyIds[1]) { return err( new CrossCompanyTransferNotSupportedError(`${sourceSiteId} -> ${destinationSiteId}`), ); } const validatedLines: { line: TransferOrderLineInput & Partial; unitId: string }[] = []; for (const line of lines) { if (!isPositiveDecimal(line.orderedQuantity)) { return err(new InvalidQuantityError(line.orderedQuantity)); } const { item } = (await itemManagementQueries.getItem(db, { id: line.itemId }, ctx)).value; if (!item) { return err(new ItemNotFoundError(line.itemId)); } validatedLines.push({ line, unitId: item.unitId }); } const transferOrder = await db .insertInto("TransferOrder") .values({ // Header input first (incl. custom fields) so command-set columns always win. ...(header as Record), sourceSiteId, destinationSiteId, plannedShipmentDate, expectedReceiptDate, status: "DRAFT", closedAt: null, }) .returningAll() .executeTakeFirstOrThrow(); await db .insertInto("TransferOrderLine") .values( validatedLines.map(({ line, unitId }) => { const { itemId, orderedQuantity, ...lineCustomFields } = line; return { ...lineCustomFields, transferOrderId: transferOrder.id, itemId, unitId, orderedQuantity, shippedQuantity: "0", receivedQuantity: "0", }; }), ) .execute(); return ok({ transferOrder }); }