import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction } from "../generated/kysely-tailordb"; import { APPROVAL_NOT_ACTIVE_ASSIGNEE, type FindPendingApproverAssigneeFn, type GetActiveApprovalRequestFn, type SendBackApprovalStepFn, LEAVE_REQUEST_TARGET_ENTITY_TYPE, } from "../lib/_approvalDeps"; import { LeaveRequestNotFoundError, InvalidStateTransitionError, SelfApprovalError, MissingApproverCommentError, NotAssigneeError, ApprovalStepFailedError, } from "../lib/errors.generated"; export interface RejectLeaveCancelInput { id: string; resolvedBy: string; approverComment: string; } export interface RejectLeaveCancelDeps { getActiveApprovalRequest: GetActiveApprovalRequestFn; findPendingApproverAssignee: FindPendingApproverAssigneeFn; sendBackApprovalStep: SendBackApprovalStepFn; } export async function run( db: Transaction, input: RejectLeaveCancelInput, ctx: CommandContext, deps: RejectLeaveCancelDeps, ) { const leaveRequest = await db .selectFrom("LeaveRequest") .selectAll() .where("id", "=", input.id) .forUpdate() .executeTakeFirst(); if (!leaveRequest) { return err(new LeaveRequestNotFoundError(input.id)); } // resolvedBy is a user-management User id; self-rejection is detected by comparing it against // this same-module request's workerId — no cross-module lookup needed for this check. if (leaveRequest.workerId === input.resolvedBy) { return err(new SelfApprovalError(input.id)); } if (leaveRequest.status !== "CANCEL_PENDING") { return err(new InvalidStateTransitionError(input.id)); } if (!input.approverComment || input.approverComment.trim() === "") { return err(new MissingApproverCommentError(input.id)); } // Resolve the mirrored cancellation approval request as rejected (the approver sends the step // back). No active request, or the resolver owning no pending assignee, means they are not an // eligible approver. const requestResult = await deps.getActiveApprovalRequest(db, { targetEntityType: LEAVE_REQUEST_TARGET_ENTITY_TYPE, targetEntityId: input.id, }); const request = requestResult.ok ? requestResult.value.approvalRequest : null; if (!request) { return err(new ApprovalStepFailedError(input.id)); } const assignee = await deps.findPendingApproverAssignee(db, { approvalRequestId: request.id, userId: input.resolvedBy, }); if (!assignee) { return err(new NotAssigneeError(input.id)); } const decision = await deps.sendBackApprovalStep( db, { approvalStepAssigneeId: assignee.approvalStepAssigneeId, reason: input.approverComment }, ctx, ); if (!decision.ok) { return err( decision.error.code === APPROVAL_NOT_ACTIVE_ASSIGNEE ? new NotAssigneeError(input.id) : new ApprovalStepFailedError(input.id), ); } // No LeaveConsumption rows are restored and no time-tracking side effect is reverted here — // the ledger and the already-emitted day status remain exactly as they were before the // cancellation was filed (per doc). const updated = await db .updateTable("LeaveRequest") .set({ status: "APPROVED", resolvedAt: new Date(), resolvedBy: input.resolvedBy, }) .where("id", "=", input.id) .returningAll() .executeTakeFirstOrThrow(); return ok({ leaveRequest: updated }); }