import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core"; import { productionOrderLifecycle } from "../db/productionOrder.lifecycle.generated"; import type { Transaction } from "../generated/kysely-tailordb"; import { ProductionOrderNotFoundError, ProductionOrderNotUnreleasableError, ExecutionAlreadyStartedError, InventoryHandoffExistsError, } from "../lib/errors.generated"; export interface UnreleaseProductionOrderInput { id: string; } /** * Function: unreleaseProductionOrder * * Returns a released order to DRAFT when execution has not meaningfully * started. Removes release artifacts so the planner can safely revise * the order. */ export async function run( db: Transaction, input: UnreleaseProductionOrderInput, _ctx: CommandContext, ) { const { id } = input; // 1. Fetch production order with lock const order = await db .selectFrom("ProductionOrder") .selectAll() .where("id", "=", id) .forUpdate() .executeTakeFirst(); if (!order) { return err(new ProductionOrderNotFoundError(id)); } // 2. Validate status is unreleasable const nextStatus = productionOrderLifecycle.tryTransition(order.status, "unrelease"); if (!nextStatus) { return err(new ProductionOrderNotUnreleasableError(id)); } // 3. Check no work order has execution evidence const workOrders = await db .selectFrom("WorkOrder") .selectAll() .where("productionOrderId", "=", id) .execute(); const hasExecution = workOrders.some( (wo) => wo.status === "IN_PROGRESS" || wo.status === "PAUSED" || wo.status === "COMPLETE" || wo.completedQuantity > 0 || wo.actualStartDate != null, ); if (hasExecution) { return err(new ExecutionAlreadyStartedError(id)); } // 4. Check no irreversible inventory handoff const _materialRequirements = await db .selectFrom("ProductionOrderMaterialRequirement") .selectAll() .where("productionOrderId", "=", id) .execute(); // Check if any cost summary has actual costs recorded (inventory handoff evidence) const costSummary = await db .selectFrom("ManufacturingCostSummary") .selectAll() .where("productionOrderId", "=", id) .executeTakeFirst(); if ( costSummary && (costSummary.actualMaterialCost > 0 || costSummary.actualLaborCost > 0 || costSummary.actualMachineCost > 0 || costSummary.actualOverheadCost > 0) ) { return err(new InventoryHandoffExistsError(id)); } // 5. Remove release artifacts - delete work orders await db.deleteFrom("WorkOrder").where("productionOrderId", "=", id).execute(); // 6. Remove material requirements await db .deleteFrom("ProductionOrderMaterialRequirement") .where("productionOrderId", "=", id) .execute(); // 7. Remove BOM snapshot await db.deleteFrom("ProductionOrderBomSnapshot").where("productionOrderId", "=", id).execute(); // 8. Remove routing snapshot await db .deleteFrom("ProductionOrderRoutingSnapshot") .where("productionOrderId", "=", id) .execute(); // 9. Remove cost baseline await db.deleteFrom("ProductionOrderCostBaseline").where("productionOrderId", "=", id).execute(); // 10. Remove cost summary await db.deleteFrom("ManufacturingCostSummary").where("productionOrderId", "=", id).execute(); // 11. Set status to DRAFT const draftOrder = await db .updateTable("ProductionOrder") .set({ selectedBomVersionId: null, selectedRoutingRevisionId: null, status: nextStatus, }) .where("id", "=", id) .returningAll() .executeTakeFirstOrThrow(); return ok({ productionOrder: draftOrder }); }