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, WorkOrderNotCompletableError, WorkOrderNotStartedError, InvalidCompletionQuantityError, DuplicateBackflushRiskError, ReceiptHandoffRequiredError, } from "../lib/errors.generated"; export interface ReceiptData { itemReference?: string | null; unitOfMeasure?: string | null; siteReference?: string | null; postingDate?: Date | null; storageLocationReference?: string | null; } export interface ZeroQuantityBypassPolicy { allowZeroCompletion: boolean; reasonCode?: string | null; } export interface CompleteWorkOrderInput { id: string; completedQuantity: number; zeroQuantityBypassPolicy?: ZeroQuantityBypassPolicy | null; backflushRequired: boolean; manuallyIssuedQuantity?: number; receiptRequired: boolean; receiptData?: ReceiptData | null; notes?: string | null; } /** * Function: completeWorkOrder * * Finishes execution on an in-progress work order. Records the final completed * quantity, validates backflush and receipt-handoff obligations, creates a * COMPLETED execution event, and rolls up completion to the parent production * order when all sibling work orders are finished. */ export async function run(db: Transaction, input: CompleteWorkOrderInput, _ctx: CommandContext) { const { id, completedQuantity, zeroQuantityBypassPolicy, backflushRequired, manuallyIssuedQuantity, receiptRequired, receiptData, notes, } = 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 IN_PROGRESS const nextStatus = workOrderLifecycle.tryTransition(workOrder.status, "complete"); if (!nextStatus) { return err(new WorkOrderNotCompletableError(id)); } // 3. Validate actual start evidence exists if (!workOrder.actualStartDate) { return err(new WorkOrderNotStartedError(id)); } // 4. Validate completed quantity, allowing explicit zero-quantity bypass. const zeroQuantityBypassAllowed = completedQuantity === 0 && zeroQuantityBypassPolicy?.allowZeroCompletion === true; if (completedQuantity < 0 || (completedQuantity === 0 && !zeroQuantityBypassAllowed)) { return err(new InvalidCompletionQuantityError(id)); } // 5. Validate backflush does not duplicate manual issue if (backflushRequired && manuallyIssuedQuantity != null && manuallyIssuedQuantity > 0) { return err(new DuplicateBackflushRiskError(id)); } // 6. Validate receipt handoff data when receipt is required if (receiptRequired) { if (!receiptData) { return err(new ReceiptHandoffRequiredError(id)); } if ( !receiptData.itemReference || !receiptData.unitOfMeasure || !receiptData.siteReference || !receiptData.postingDate ) { return err(new ReceiptHandoffRequiredError(id)); } } // 7. Update work order to COMPLETE const now = new Date(); const updatedWorkOrder = await db .updateTable("WorkOrder") .set({ status: nextStatus, completedQuantity: workOrder.completedQuantity + completedQuantity, executionNotes: notes ?? workOrder.executionNotes, }) .where("id", "=", id) .returningAll() .executeTakeFirstOrThrow(); // 8. Create COMPLETED execution event await db .insertInto("WorkOrderExecutionEvent") .values({ workOrderId: id, eventType: "COMPLETED", timestamp: now, quantity: completedQuantity, timeValue: null, scrapValue: null, notes: notes ?? null, }) .execute(); const backflushHandoff = backflushRequired ? { productionOrderReference: workOrder.productionOrderId, workOrderReference: id, completedQuantity, manuallyIssuedQuantity: manuallyIssuedQuantity ?? 0, postingDate: receiptData?.postingDate ?? now, bypassReason: zeroQuantityBypassAllowed ? (zeroQuantityBypassPolicy?.reasonCode ?? null) : null, } : null; const receiptHandoff = receiptRequired && receiptData ? { productionOrderReference: workOrder.productionOrderId, workOrderReference: id, itemReference: receiptData.itemReference, quantity: completedQuantity, unitOfMeasure: receiptData.unitOfMeasure, siteReference: receiptData.siteReference, postingDate: receiptData.postingDate, storageLocationReference: receiptData.storageLocationReference ?? null, } : null; // 9. Roll up to parent production order const siblingWorkOrders = await db .selectFrom("WorkOrder") .selectAll() .where("productionOrderId", "=", workOrder.productionOrderId) .execute(); const allComplete = (siblingWorkOrders as { id: string; status: string }[]).every((wo) => { if (wo.id === id) return true; // this one was just completed return wo.status === "COMPLETE" || wo.status === "CANCELLED"; }); if (allComplete) { await db .updateTable("ProductionOrder") .set({ status: "COMPLETED", }) .where("id", "=", workOrder.productionOrderId) .execute(); } return ok({ workOrder: updatedWorkOrder, backflushHandoff, receiptHandoff }); }