import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction } from "../generated/kysely-tailordb"; import { WorkOrderNotFoundError, WorkOrderNotReportableError, InvalidReportedQuantityError, EmptyProgressTransactionError, ScrapHandoffRequiredError, } from "../lib/errors.generated"; export interface ReportWorkOrderProgressInput { id: string; completedQuantity?: number; scrapQuantity?: number; actualSetupTime?: number; actualRunTime?: number; notes?: string | null; scrapHandoffData?: Record | null; } /** * Function: reportWorkOrderProgress * * Records partial execution evidence such as completed quantity, scrap quantity, * actual time, and exception notes. Emits ManufacturingScrapHandoff when the * report contains scrapped quantity. */ export async function run( db: Transaction, input: ReportWorkOrderProgressInput, _ctx: CommandContext, ) { const { id, completedQuantity = 0, scrapQuantity = 0, actualSetupTime = 0, actualRunTime = 0, notes = null, scrapHandoffData = null, } = input; // 1. Validate quantities are non-negative if (completedQuantity < 0 || scrapQuantity < 0) { return err(new InvalidReportedQuantityError(id)); } // 2. Validate at least one positive value was reported if (completedQuantity <= 0 && scrapQuantity <= 0 && actualSetupTime <= 0 && actualRunTime <= 0) { return err(new EmptyProgressTransactionError(id)); } // 3. Fetch work order with lock const workOrder = await db .selectFrom("WorkOrder") .selectAll() .where("id", "=", id) .forUpdate() .executeTakeFirst(); if (!workOrder) { return err(new WorkOrderNotFoundError(id)); } // 4. Validate status is IN_PROGRESS if (workOrder.status !== "IN_PROGRESS") { return err(new WorkOrderNotReportableError(id)); } // 5. Validate scrap handoff when positive scrap is reported if (scrapQuantity > 0 && !scrapHandoffData) { return err(new ScrapHandoffRequiredError(id)); } // 6. Accumulate quantities and time const now = new Date(); const updatedWorkOrder = await db .updateTable("WorkOrder") .set({ completedQuantity: workOrder.completedQuantity + completedQuantity, scrapQuantity: workOrder.scrapQuantity + scrapQuantity, actualSetupTime: workOrder.actualSetupTime + actualSetupTime, actualRunTime: workOrder.actualRunTime + actualRunTime, executionNotes: notes ?? workOrder.executionNotes, }) .where("id", "=", id) .returningAll() .executeTakeFirstOrThrow(); // 7. Create PROGRESS_REPORTED execution event await db .insertInto("WorkOrderExecutionEvent") .values({ workOrderId: id, eventType: "PROGRESS_REPORTED", timestamp: now, quantity: completedQuantity, timeValue: actualSetupTime + actualRunTime, scrapValue: scrapQuantity > 0 ? scrapQuantity : null, notes, }) .execute(); return ok({ workOrder: updatedWorkOrder }); }