import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core"; import { approvalRequestLifecycle } from "../db/approvalRequest.lifecycle.generated"; import { approvalStepLifecycle } from "../db/approvalStep.lifecycle.generated"; import { approvalStepAssigneeLifecycle } from "../db/approvalStepAssignee.lifecycle.generated"; import type { Transaction } from "../generated/kysely-tailordb"; import { InvalidStatusTransitionError, NotRequesterError, RequestNotFoundError, } from "../lib/errors.generated"; export interface ResubmitApprovalRequestInput { approvalRequestId: string; comment?: string; } /** * Function: resubmitApprovalRequest * * The requester's reply to a send-back. Returns a REVISION_REQUESTED request to * PENDING and restarts approval from the first step: every ApprovalStep is reset * to PENDING, every APPROVED ApprovalStepAssignee is reset to PENDING, and the * lowest-stepOrder step is re-activated to IN_PROGRESS. The restart reuses the * already-frozen assignee set — it does not re-expand roles or re-read the * source policy — and never deletes prior ApprovalDecision rows, so the full * send-back/resubmit history is preserved. */ export async function run( db: Transaction, input: ResubmitApprovalRequestInput, ctx: CommandContext, ) { const request = await db .selectFrom("ApprovalRequest") .selectAll() .where("id", "=", input.approvalRequestId) .forUpdate() .executeTakeFirst(); if (!request) return err(new RequestNotFoundError(input.approvalRequestId)); const nextStatus = approvalRequestLifecycle.tryTransition(request.status, "resubmit"); if (!nextStatus) return err(new InvalidStatusTransitionError(input.approvalRequestId)); if (ctx.actorId !== request.requesterId) { return err(new NotRequesterError(input.approvalRequestId)); } const now = new Date(); await db .insertInto("ApprovalDecision") .values({ approvalRequestId: request.id, decision: "RESUBMIT", decidedByUserId: ctx.actorId, comment: input.comment, decidedAt: now, }) .execute(); const steps = await db .selectFrom("ApprovalStep") .select(["id", "stepOrder"]) .where("approvalRequestId", "=", request.id) .orderBy("stepOrder", "asc") .execute(); const stepIds = steps.map((s) => s.id); await db .updateTable("ApprovalStep") .set({ status: approvalStepLifecycle.transitions.reset.to, resolvedAt: null }) .where("approvalRequestId", "=", request.id) .execute(); // DELEGATED rows stay: reviving the delegator would seat them alongside the delegatee's row. if (stepIds.length > 0) { await db .updateTable("ApprovalStepAssignee") .set({ status: approvalStepAssigneeLifecycle.transitions.reset.to, resolvedAt: null }) .where("approvalStepId", "in", stepIds) .where("status", "in", approvalStepAssigneeLifecycle.transitions.reset.from) .execute(); } const firstStep = steps[0]; if (firstStep) { await db .updateTable("ApprovalStep") .set({ status: approvalStepLifecycle.transitions.activate.to }) .where("id", "=", firstStep.id) .execute(); } const updated = await db .updateTable("ApprovalRequest") .set({ status: nextStatus }) .where("id", "=", request.id) .returningAll() .executeTakeFirstOrThrow(); return ok({ approvalRequest: updated }); }