import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction } from "../generated/kysely-tailordb"; import { CircularDelegationError, NotActiveAssigneeError, ParentRequestNotPendingError, SelfDecisionNotAllowedError, SelfDelegationNotAllowedError, } from "../lib/errors.generated"; export interface DelegateApprovalStepInput { approvalStepAssigneeId: string; delegatedToUserId: string; comment?: string; } export async function run(db: Transaction, input: DelegateApprovalStepInput, ctx: CommandContext) { const assignee = await db .selectFrom("ApprovalStepAssignee") .selectAll() .where("id", "=", input.approvalStepAssigneeId) .forUpdate() .executeTakeFirst(); if (!assignee || assignee.userId !== ctx.actorId || assignee.status !== "PENDING") { return err(new NotActiveAssigneeError(input.approvalStepAssigneeId)); } const step = await db .selectFrom("ApprovalStep") .selectAll() .where("id", "=", assignee.approvalStepId) .forUpdate() .executeTakeFirst(); if (!step || step.status !== "IN_PROGRESS") { return err(new NotActiveAssigneeError(input.approvalStepAssigneeId)); } const request = await db .selectFrom("ApprovalRequest") .selectAll() .where("id", "=", step.approvalRequestId) .forUpdate() .executeTakeFirst(); if (!request) { return err(new NotActiveAssigneeError(input.approvalStepAssigneeId)); } if (request.status !== "PENDING") { return err(new ParentRequestNotPendingError(request.id)); } if (ctx.actorId === request.requesterId) { return err(new SelfDecisionNotAllowedError(request.id)); } if (input.delegatedToUserId === ctx.actorId) { return err(new SelfDelegationNotAllowedError(input.delegatedToUserId)); } // Circular delegation guard: the prospective delegate must not already hold a // PENDING seat on this same step. const existingDelegate = await db .selectFrom("ApprovalStepAssignee") .selectAll() .where("approvalStepId", "=", step.id) .where("userId", "=", input.delegatedToUserId) .where("status", "=", "PENDING") .executeTakeFirst(); if (existingDelegate) { return err(new CircularDelegationError(input.delegatedToUserId)); } const now = new Date(); await db .insertInto("ApprovalDecision") .values({ approvalRequestId: request.id, approvalStepId: step.id, decision: "DELEGATE", decidedByUserId: ctx.actorId, delegatedToUserId: input.delegatedToUserId, comment: input.comment, decidedAt: now, }) .execute(); await db .updateTable("ApprovalStepAssignee") .set({ status: "DELEGATED", resolvedAt: now }) .where("id", "=", assignee.id) .execute(); await db .insertInto("ApprovalStepAssignee") .values({ approvalStepId: step.id, userId: input.delegatedToUserId, roleId: assignee.roleId, required: assignee.required, roleQuorum: assignee.roleQuorum, status: "PENDING", }) .execute(); return ok({ approvalRequest: request }); }