import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core"; import { manufacturingCostSummaryLifecycle } from "../db/manufacturingCostSummary.lifecycle.generated"; import { CostVarianceLineVarianceType } from "../generated/enums"; import type { Transaction } from "../generated/kysely-tailordb"; import { CostSummaryNotFoundError, CostSummaryNotReviewableError, ParentOrderNotTechnicallyCompleteError, IncompleteVarianceBreakdownError, ReviewerRequiredError, VarianceCalculationFailedError, } from "../lib/errors.generated"; export interface VarianceBreakdownEntry { varianceType: CostVarianceLineVarianceType; amount: number; accountReference?: string | null; variancePercentage?: number | null; } export interface ReviewManufacturingCostSummaryInput { costSummaryId: string; reviewerId?: string | null; reviewerNotes?: string | null; varianceBreakdown?: VarianceBreakdownEntry[] | null; } /** * Function: reviewManufacturingCostSummary * * Recalculates the final planned-versus-actual result for a technically * complete order and freezes the variance classification through an * explicit reviewer approval step. */ export async function run( db: Transaction, input: ReviewManufacturingCostSummaryInput, _ctx: CommandContext, ) { const { costSummaryId, reviewerId, reviewerNotes, varianceBreakdown } = input; // 1. Validate reviewer identity if (!reviewerId) { return err(new ReviewerRequiredError(costSummaryId)); } // 2. Resolve cost summary const costSummary = await db .selectFrom("ManufacturingCostSummary") .selectAll() .where("id", "=", costSummaryId) .forUpdate() .executeTakeFirst(); if (!costSummary) { return err(new CostSummaryNotFoundError(costSummaryId)); } // 3. Summary must be PENDING_VARIANCE_REVIEW const nextStatus = manufacturingCostSummaryLifecycle.tryTransition(costSummary.status, "review"); if (!nextStatus) { return err(new CostSummaryNotReviewableError(costSummaryId)); } // 4. Parent production order must be TECHNICALLY_COMPLETE const productionOrder = await db .selectFrom("ProductionOrder") .selectAll() .where("id", "=", costSummary.productionOrderId) .forUpdate() .executeTakeFirst(); if (productionOrder?.status !== "TECHNICALLY_COMPLETE") { return err(new ParentOrderNotTechnicallyCompleteError(costSummaryId)); } // 5. Validate variance breakdown covers all required categories if (!varianceBreakdown || !Array.isArray(varianceBreakdown)) { return err(new IncompleteVarianceBreakdownError(costSummaryId)); } const providedTypes = new Set(varianceBreakdown.map((v) => v.varianceType)); const missingTypes = Object.values(CostVarianceLineVarianceType).filter( (t) => !providedTypes.has(t), ); if (missingTypes.length > 0) { return err(new IncompleteVarianceBreakdownError(costSummaryId)); } // 6. Validate that variance amounts are consistent (no NaN or undefined) for (const entry of varianceBreakdown) { if (typeof entry.amount !== "number" || isNaN(entry.amount)) { return err(new VarianceCalculationFailedError(costSummaryId)); } } // 7. Create cost variance lines for each breakdown entry (bulk insert) if (varianceBreakdown.length > 0) { await db .insertInto("CostVarianceLine") .values( varianceBreakdown.map((entry) => ({ costSummaryId: costSummary.id, varianceType: entry.varianceType, amount: entry.amount, accountReference: entry.accountReference ?? null, variancePercentage: entry.variancePercentage ?? null, })), ) .execute(); } // 8. Update summary to VARIANCE_REVIEWED const updatedSummary = await db .updateTable("ManufacturingCostSummary") .set({ status: nextStatus, reviewedDate: new Date(), reviewerNotes: reviewerNotes ?? null, }) .where("id", "=", costSummary.id) .returningAll() .executeTakeFirstOrThrow(); return ok({ manufacturingCostSummary: updatedSummary }); }