import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core"; import { billOfMaterialLifecycle } from "../db/billOfMaterial.lifecycle.generated"; import type { Transaction } from "../generated/kysely-tailordb"; import { BomNotFoundError, BomNotDeactivatableError, ReplacementRequiredError, } from "../lib/errors.generated"; export interface DeactivateBillOfMaterialInput { id: string; } /** * Function: deactivateBillOfMaterial * * Removes an active BOM version from future selection. Preserves audit * history and any released production-order snapshots that already depend * on the version. */ export async function run( db: Transaction, input: DeactivateBillOfMaterialInput, _ctx: CommandContext, ) { const { id } = input; // 1. Fetch BOM with lock const bom = await db .selectFrom("BillOfMaterial") .selectAll() .where("id", "=", id) .forUpdate() .executeTakeFirst(); if (!bom) { return err(new BomNotFoundError(id)); } // 2. Validate status is deactivatable const nextStatus = billOfMaterialLifecycle.tryTransition(bom.status, "deactivate"); if (!nextStatus) { return err(new BomNotDeactivatableError(id)); } // 3. Check replacement policy — if this is the only active BOM for this // parent item in scope and it is the default selection, a replacement // active version must exist before deactivation if (bom.defaultSelection) { const replacementBom = await db .selectFrom("BillOfMaterial") .selectAll() .where("parentItemId", "=", bom.parentItemId) .where("companyId", "=", bom.companyId) .where("id", "!=", id) .where("status", "=", "ACTIVE") .executeTakeFirst(); if (!replacementBom) { return err(new ReplacementRequiredError(id)); } } // 4. Set status to INACTIVE const updatedBom = await db .updateTable("BillOfMaterial") .set({ status: nextStatus, }) .where("id", "=", id) .returningAll() .executeTakeFirstOrThrow(); return ok({ billOfMaterial: updatedBom }); }