import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction } from "../generated/kysely-tailordb"; import { MissingRequiredFieldError, NotActiveAssigneeError, ParentRequestNotPendingError, SelfDecisionNotAllowedError, } from "../lib/errors.generated"; export interface RejectApprovalStepInput { approvalStepAssigneeId: string; reason: string; } /** * Function: rejectApprovalStep * * Records a reject decision against a single ApprovalStepAssignee row that * the calling actor owns and cascades the rejection to the parent step and * request immediately. Trailing PENDING steps remain PENDING (no SKIPPED * state). The supplied reason is written verbatim to ApprovalDecision.comment * and ApprovalRequest.rejectionReason in the same transaction. */ export async function run(db: Transaction, input: RejectApprovalStepInput, ctx: CommandContext) { const assignee = await db .selectFrom("ApprovalStepAssignee") .selectAll() .where("id", "=", input.approvalStepAssigneeId) .forUpdate() .executeTakeFirst(); if (!assignee) return err(new NotActiveAssigneeError(input.approvalStepAssigneeId)); if (assignee.status !== "PENDING" || assignee.userId !== ctx.actorId) { 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.reason.trim() === "") { return err(new MissingRequiredFieldError("reason")); } const now = new Date(); await db .insertInto("ApprovalDecision") .values({ approvalRequestId: request.id, approvalStepId: step.id, decision: "REJECT", decidedByUserId: ctx.actorId, comment: input.reason, decidedAt: now, }) .execute(); await db .updateTable("ApprovalStepAssignee") .set({ status: "REJECTED", resolvedAt: now }) .where("id", "=", assignee.id) .execute(); await db .updateTable("ApprovalStep") .set({ status: "REJECTED", resolvedAt: now }) .where("id", "=", step.id) .execute(); const updated = await db .updateTable("ApprovalRequest") .set({ status: "REJECTED", rejectionReason: input.reason, resolvedAt: now, }) .where("id", "=", request.id) .returningAll() .executeTakeFirstOrThrow(); return ok({ approvalRequest: updated }); }