import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core"; import { approvalPolicyLifecycle } from "../db/approvalPolicy.lifecycle.generated"; import type { Transaction } from "../generated/kysely-tailordb"; import { ActivePolicyConflictError, InvalidStatusTransitionError, PolicyNotFoundError, PolicyNotReadyToActivateError, } from "../lib/errors.generated"; export interface ActivateApprovalPolicyInput { id: string; } export async function run( db: Transaction, input: ActivateApprovalPolicyInput, _ctx: CommandContext, ) { const policy = await db .selectFrom("ApprovalPolicy") .selectAll() .where("id", "=", input.id) .forUpdate() .executeTakeFirst(); if (!policy) { return err(new PolicyNotFoundError(input.id)); } const nextStatus = approvalPolicyLifecycle.tryTransition(policy.status, "activate"); if (!nextStatus) { return err(new InvalidStatusTransitionError(input.id)); } const steps = await db .selectFrom("ApprovalPolicyStep") .selectAll() .where("approvalPolicyId", "=", input.id) .execute(); if (steps.length === 0) { return err(new PolicyNotReadyToActivateError(input.id)); } const assignees = await db .selectFrom("ApprovalPolicyStepAssignee") .select("approvalPolicyStepId") .where( "approvalPolicyStepId", "in", steps.map((s) => s.id), ) .execute(); const stepsWithAssignees = new Set(assignees.map((a) => a.approvalPolicyStepId)); if (stepsWithAssignees.size !== steps.length) { return err(new PolicyNotReadyToActivateError(input.id)); } // Application-level enforcement: TailorDB doesn't support partial unique indexes, // so concurrent activates on different DRAFTs sharing (purpose, name) can race. const conflicting = await db .selectFrom("ApprovalPolicy") .selectAll() .where("purpose", "=", policy.purpose) .where("name", "=", policy.name) .where("status", "=", "ACTIVE") .where("id", "!=", input.id) .executeTakeFirst(); if (conflicting) { return err(new ActivePolicyConflictError(input.id)); } const now = new Date(); const updated = await db .updateTable("ApprovalPolicy") .set({ status: nextStatus, activatedAt: now }) .where("id", "=", input.id) .returningAll() .executeTakeFirstOrThrow(); return ok({ approvalPolicy: updated }); }