import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction } from "../generated/kysely-tailordb"; import { RoutingItemNotFoundError, OperationRequiredError, DuplicateOperationSequenceError, InvalidStandardTimeError, WorkCenterNotFoundError, CrossCompanyWorkCenterError, } from "../lib/errors.generated"; import type { ItemManagementQueries } from "../module"; export interface CreateRoutingOperationInput { sequenceNumber: number; operationDescription?: string | null; workCenterId: string; standardSetupTime: number; standardRunTime: number; operatorInstructions?: string | null; } export interface CreateRoutingInput { parentItemId: string; companyId: string; siteId?: string | null; revisionNumber?: string | null; operations: CreateRoutingOperationInput[]; } /** * Function: createRouting * * Creates a new routing in DRAFT status with ordered operations. * Validates parent item existence, operation sequence uniqueness, * standard times, and work center references. */ export async function run>( db: Transaction, input: CreateRoutingInput & CF, ctx: CommandContext, itemManagementQueries: Pick, ) { const { parentItemId, companyId, siteId, revisionNumber, operations, ...customFields } = input; // 1. Validate parent item exists const { item: parentItem } = (await itemManagementQueries.getItem(db, { id: parentItemId }, ctx)) .value; if (!parentItem) { return err(new RoutingItemNotFoundError(parentItemId)); } // 2. Validate at least one operation if (!operations || operations.length === 0) { return err(new OperationRequiredError(parentItemId)); } // 3. Validate unique sequence numbers const seqNumbers = operations.map((op) => op.sequenceNumber); const uniqueSeqs = new Set(seqNumbers); if (uniqueSeqs.size !== seqNumbers.length) { return err(new DuplicateOperationSequenceError(parentItemId)); } // 4. Validate standard times >= 0 for (const op of operations) { if (op.standardSetupTime < 0 || op.standardRunTime < 0) { return err(new InvalidStandardTimeError(parentItemId)); } } // 5. Validate work centers exist and belong to same company for (const op of operations) { const workCenter = await db .selectFrom("WorkCenter") .selectAll() .where("id", "=", op.workCenterId) .executeTakeFirst(); if (!workCenter) { return err(new WorkCenterNotFoundError(op.workCenterId)); } if (workCenter.companyId !== companyId) { return err(new CrossCompanyWorkCenterError(op.workCenterId)); } } // 6. Create routing in DRAFT status const routing = await db .insertInto("Routing") .values({ ...(customFields as Record), parentItemId, companyId, siteId: siteId ?? null, revisionNumber: revisionNumber ?? null, status: "DRAFT", }) .returningAll() .executeTakeFirstOrThrow(); // 7. Create routing operations const createdOperations = await db .insertInto("RoutingOperation") .values( operations.map((op) => ({ routingId: routing.id, sequenceNumber: op.sequenceNumber, operationDescription: op.operationDescription ?? null, workCenterId: op.workCenterId, standardSetupTime: op.standardSetupTime, standardRunTime: op.standardRunTime, operatorInstructions: op.operatorInstructions ?? null, })), ) .returningAll() .execute(); return ok({ routing, routingOperations: createdOperations }); }