import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction } from "../generated/kysely-tailordb"; import { ProductionOrderNotFoundError, CostSummaryNotFoundError, CostSummaryNotCollectingError, MissingInventoryIssueReferenceError, MissingActualUnitCostError, MissingActualExtendedCostError, MissingPostingDateError, IssueOutcomeNotFinalError, DuplicateIssueOutcomeError, } from "../lib/errors.generated"; export interface RecordInventoryIssueOutcomeInput { productionOrderId: string; inventoryIssueReference: string; itemReference: string; issuedQuantity: number; unitOfMeasure: string; actualUnitCost: number; actualExtendedCost: number; currency: string; valuationMethod: string; postingDate: string; siteReference: string; isFinalValuation: boolean; } /** * Function: recordInventoryIssueOutcome * * Applies one inventory-owned valuation outcome to the manufacturing cost * summary for a production order. It is the only valid path for actual * material cost to enter manufacturing. */ export async function run( db: Transaction, input: RecordInventoryIssueOutcomeInput, _ctx: CommandContext, ) { const { productionOrderId, inventoryIssueReference, actualUnitCost, actualExtendedCost, postingDate, isFinalValuation, } = input; // 1. Validate required payload fields if (!inventoryIssueReference) { return err(new MissingInventoryIssueReferenceError(productionOrderId)); } if (actualUnitCost == null) { return err(new MissingActualUnitCostError(productionOrderId)); } if (actualExtendedCost == null) { return err(new MissingActualExtendedCostError(productionOrderId)); } if (!postingDate) { return err(new MissingPostingDateError(productionOrderId)); } // 2. Resolve production order const productionOrder = await db .selectFrom("ProductionOrder") .selectAll() .where("id", "=", productionOrderId) .forUpdate() .executeTakeFirst(); if (!productionOrder) { return err(new ProductionOrderNotFoundError(productionOrderId)); } // 3. Resolve cost summary const costSummary = await db .selectFrom("ManufacturingCostSummary") .selectAll() .where("productionOrderId", "=", productionOrderId) .forUpdate() .executeTakeFirst(); if (!costSummary) { return err(new CostSummaryNotFoundError(productionOrderId)); } // 4. Summary must be COLLECTING if (costSummary.status !== "COLLECTING") { return err(new CostSummaryNotCollectingError(productionOrderId)); } // 5. Inventory valuation must be final if (!isFinalValuation) { return err(new IssueOutcomeNotFinalError(productionOrderId)); } // 6. Check for duplicate application const existingCostLine = await db .selectFrom("ManufacturingCostLine") .selectAll() .where("costSummaryId", "=", costSummary.id) .where("costType", "=", `MATERIAL:${inventoryIssueReference}`) .executeTakeFirst(); if (existingCostLine) { return err(new DuplicateIssueOutcomeError(productionOrderId)); } // 7. Create a manufacturing cost line for this issue outcome await db .insertInto("ManufacturingCostLine") .values({ costSummaryId: costSummary.id, costType: `MATERIAL:${inventoryIssueReference}`, plannedAmount: 0, actualAmount: actualExtendedCost, }) .returningAll() .executeTakeFirst(); // 8. Update the cost summary's actual material cost const updatedSummary = await db .updateTable("ManufacturingCostSummary") .set({ actualMaterialCost: costSummary.actualMaterialCost + actualExtendedCost, }) .where("id", "=", costSummary.id) .returningAll() .executeTakeFirstOrThrow(); return ok({ manufacturingCostSummary: updatedSummary }); }