import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core"; import { workOrderLifecycle } from "../db/workOrder.lifecycle.generated"; import type { Transaction } from "../generated/kysely-tailordb"; import { WorkOrderNotFoundError, WorkOrderNotStartableError, ParentOrderNotExecutableError, OperationSequenceBlockedError, } from "../lib/errors.generated"; const EXECUTABLE_ORDER_STATUSES = ["RELEASED", "IN_PROGRESS"] as const; export interface StartWorkOrderInput { id: string; } /** * Function: startWorkOrder * * Begins execution on a pending work order. Records the actual start timestamp, * moves the work order to IN_PROGRESS, and transitions the parent production * order to IN_PROGRESS if it was RELEASED. */ export async function run(db: Transaction, input: StartWorkOrderInput, _ctx: CommandContext) { const { id } = input; // 1. Fetch work order with lock const workOrder = await db .selectFrom("WorkOrder") .selectAll() .where("id", "=", id) .forUpdate() .executeTakeFirst(); if (!workOrder) { return err(new WorkOrderNotFoundError(id)); } // 2. Validate status is PENDING const nextStatus = workOrderLifecycle.tryTransition(workOrder.status, "start"); if (!nextStatus) { return err(new WorkOrderNotStartableError(id)); } // 3. Check parent production order is in an execution-capable state const parentOrder = await db .selectFrom("ProductionOrder") .selectAll() .where("id", "=", workOrder.productionOrderId) .forUpdate() .executeTakeFirst(); if ( !parentOrder || !EXECUTABLE_ORDER_STATUSES.includes( parentOrder.status as (typeof EXECUTABLE_ORDER_STATUSES)[number], ) ) { return err(new ParentOrderNotExecutableError(id)); } // 4. Check preceding operations are complete (sequence order guard) const siblingWorkOrders = await db .selectFrom("WorkOrder") .selectAll() .where("productionOrderId", "=", workOrder.productionOrderId) .execute(); const hasPendingPredecessor = siblingWorkOrders.some( (wo) => wo.routingOperationSequenceNumber < workOrder.routingOperationSequenceNumber && wo.status !== "COMPLETE" && wo.status !== "CANCELLED", ); if (hasPendingPredecessor) { return err(new OperationSequenceBlockedError(id)); } // 5. Record actual start and set IN_PROGRESS const now = new Date(); const updatedWorkOrder = await db .updateTable("WorkOrder") .set({ status: nextStatus, actualStartDate: now, }) .where("id", "=", id) .returningAll() .executeTakeFirstOrThrow(); // 6. Create STARTED execution event await db .insertInto("WorkOrderExecutionEvent") .values({ workOrderId: id, eventType: "STARTED", timestamp: now, quantity: null, timeValue: null, scrapValue: null, notes: null, }) .execute(); // 7. If parent production order is RELEASED, transition to IN_PROGRESS if (parentOrder.status === "RELEASED") { await db .updateTable("ProductionOrder") .set({ status: "IN_PROGRESS", }) .where("id", "=", parentOrder.id) .execute(); } return ok({ workOrder: updatedWorkOrder }); }