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, MissingRequiredFieldError, PolicyHasNoStepsError, StepHasNoAssigneesError, } from "../lib/errors.generated"; export interface CreateApprovalPolicyAssigneeInput { userId?: string; roleId?: string; required?: boolean; roleQuorum?: ApprovalPolicyStepAssigneeRoleQuorum; } export interface CreateApprovalPolicyStepInput { stepOrder: number; name: string; minimumApprovals?: number; assignees: CreateApprovalPolicyAssigneeInput[]; } export interface CreateApprovalPolicyInput { purpose: string; name: string; description?: string; steps: CreateApprovalPolicyStepInput[]; } export async function run(db: Transaction, input: CreateApprovalPolicyInput, _ctx: CommandContext) { const { purpose, name, description, steps } = input; if (purpose.trim() === "") { return err(new MissingRequiredFieldError("purpose")); } if (name.trim() === "") { return err(new MissingRequiredFieldError("name")); } if (steps.length === 0) { return err(new PolicyHasNoStepsError("steps")); } const stepOrders = steps.map((step) => step.stepOrder); if (new Set(stepOrders).size !== stepOrders.length) { return err(new DuplicateStepOrderError(name)); } for (const step of steps) { if (step.assignees.length === 0) { return err(new StepHasNoAssigneesError(step.name)); } const minimumApprovals = step.minimumApprovals ?? 1; if (!Number.isInteger(minimumApprovals) || minimumApprovals < 1) { return err(new InvalidMinimumApprovalsError(step.name)); } const seenRoleIds = new Set(); const seenUserIds = 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(step.name)); } if (hasUser && roleQuorum !== undefined) { return err(new AssigneeFlagsIncompatibleError(step.name)); } if (hasRole && required === true) { return err(new AssigneeFlagsIncompatibleError(step.name)); } if (hasUser) { if (seenUserIds.has(userId)) { return err(new DuplicateUserAssigneeError(step.name)); } seenUserIds.add(userId); } if (hasRole) { if (seenRoleIds.has(roleId)) { return err(new DuplicateRoleAssigneeError(step.name)); } seenRoleIds.add(roleId); } } } const policy = await db .insertInto("ApprovalPolicy") .values({ purpose, name, description, status: "DRAFT", }) .returningAll() .executeTakeFirstOrThrow(); const insertedSteps = await db .insertInto("ApprovalPolicyStep") .values( steps.map((step) => ({ approvalPolicyId: policy.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: policy }); }