import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { ApprovalPolicyStepAssigneeRoleQuorum } from "../generated/enums"; import type { Transaction } from "../generated/kysely-tailordb"; import { AssigneeFlagsIncompatibleError, AssigneeUserRoleXorViolatedError, DuplicateRoleAssigneeError, DuplicateStepOrderError, DuplicateUserAssigneeError, InvalidMinimumApprovalsError, InvalidStatusTransitionError, MissingRequiredFieldError, PolicyHasNoStepsError, PolicyNotFoundError, StepHasNoAssigneesError, } from "../lib/errors.generated"; export interface UpdateApprovalPolicyAssigneeInput { userId?: string; roleId?: string; required?: boolean; roleQuorum?: ApprovalPolicyStepAssigneeRoleQuorum; } export interface UpdateApprovalPolicyStepInput { stepOrder: number; name: string; minimumApprovals?: number; assignees: UpdateApprovalPolicyAssigneeInput[]; } export interface UpdateApprovalPolicyInput { id: string; name?: string; description?: string | null; steps?: UpdateApprovalPolicyStepInput[]; } export async function run(db: Transaction, input: UpdateApprovalPolicyInput, _ctx: CommandContext) { const { id, name, description, steps } = input; if (name !== undefined && name.trim() === "") { return err(new MissingRequiredFieldError("name")); } const policy = await db .selectFrom("ApprovalPolicy") .selectAll() .where("id", "=", id) .forUpdate() .executeTakeFirst(); if (!policy) { return err(new PolicyNotFoundError(id)); } if (policy.status !== "DRAFT") { return err(new InvalidStatusTransitionError(id)); } if (steps !== undefined) { if (steps.length === 0) { return err(new PolicyHasNoStepsError(id)); } const stepOrders = steps.map((step) => step.stepOrder); if (new Set(stepOrders).size !== stepOrders.length) { return err(new DuplicateStepOrderError(id)); } for (const step of steps) { if (step.assignees.length === 0) { return err(new StepHasNoAssigneesError(id)); } const minimumApprovals = step.minimumApprovals ?? 1; if (!Number.isInteger(minimumApprovals) || minimumApprovals < 1) { return err(new InvalidMinimumApprovalsError(id)); } const seenUserIds = new Set(); const seenRoleIds = new Set(); for (const { userId, roleId, required, roleQuorum } of step.assignees) { const hasUser = userId !== undefined; const hasRole = roleId !== undefined; if (hasUser === hasRole) { return err(new AssigneeUserRoleXorViolatedError(id)); } if (hasUser && roleQuorum !== undefined) { return err(new AssigneeFlagsIncompatibleError(id)); } if (hasRole && required === true) { return err(new AssigneeFlagsIncompatibleError(id)); } if (userId !== undefined) { if (seenUserIds.has(userId)) { return err(new DuplicateUserAssigneeError(id)); } seenUserIds.add(userId); } if (roleId !== undefined) { if (seenRoleIds.has(roleId)) { return err(new DuplicateRoleAssigneeError(id)); } seenRoleIds.add(roleId); } } } } const updateBody = { ...(name !== undefined ? { name } : {}), ...(description !== undefined ? { description } : {}), }; const updatedPolicy = Object.keys(updateBody).length === 0 ? policy : await db .updateTable("ApprovalPolicy") .set(updateBody) .where("id", "=", id) .returningAll() .executeTakeFirstOrThrow(); if (steps !== undefined) { // Delete existing assignees first to respect FK ordering, then steps const existingSteps = await db .selectFrom("ApprovalPolicyStep") .select("id") .where("approvalPolicyId", "=", id) .execute(); const existingStepIds = existingSteps.map((row) => row.id); if (existingStepIds.length > 0) { await db .deleteFrom("ApprovalPolicyStepAssignee") .where("approvalPolicyStepId", "in", existingStepIds) .execute(); } await db.deleteFrom("ApprovalPolicyStep").where("approvalPolicyId", "=", id).execute(); const insertedSteps = await db .insertInto("ApprovalPolicyStep") .values( steps.map((step) => ({ approvalPolicyId: id, stepOrder: step.stepOrder, name: step.name, minimumApprovals: step.minimumApprovals ?? 1, })), ) .returningAll() .execute(); const assigneeRows = steps.flatMap((step, idx) => { const insertedStep = insertedSteps[idx]; return step.assignees.map((assignee) => ({ approvalPolicyStepId: insertedStep.id, userId: assignee.userId, roleId: assignee.roleId, required: assignee.required ?? false, roleQuorum: assignee.roleQuorum, })); }); if (assigneeRows.length > 0) { await db.insertInto("ApprovalPolicyStepAssignee").values(assigneeRows).execute(); } } return ok({ approvalPolicy: updatedPolicy }); }