import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction } from "../generated/kysely-tailordb"; import { APPROVAL_NOT_ACTIVE_ASSIGNEE, APPROVAL_SELF_DECISION_NOT_ALLOWED, type ApproveApprovalStepFn, type FindPendingApproverAssigneeFn, type GetActiveApprovalRequestFn, LEAVE_REQUEST_TARGET_ENTITY_TYPE, } from "../lib/_approvalDeps"; import { LeaveRequestNotFoundError, InvalidStateTransitionError, SelfApprovalError, NotAssigneeError, ApprovalStepFailedError, } from "../lib/errors.generated"; export interface ApproveLeaveInput { id: string; resolvedBy: string; approverComment?: string; } export interface ApproveLeaveDeps { getActiveApprovalRequest: GetActiveApprovalRequestFn; findPendingApproverAssignee: FindPendingApproverAssigneeFn; approveApprovalStep: ApproveApprovalStepFn; } /** * Function: approveLeave * Description: Approves a PENDING LeaveRequest by delegating the decision to the bundled approval * module's approveApprovalStep (ADR-003 wrapper), then syncing the LeaveRequest to APPROVED in the * same transaction with resolvedAt/resolvedBy = the approver. Wrapper error mapping (ADR-003): * NOT_ACTIVE_ASSIGNEE -> NOT_ASSIGNEE, SELF_DECISION_NOT_ALLOWED -> SELF_APPROVAL, any other non-ok * result -> APPROVAL_STEP_FAILED. * * By design, approveLeave does NOT write into time-tracking. Time-tracking calculates actual * worked time from punches; a leave day is simply the absence of worked blocks, and time-tracking * has no reverse dependency on leave-management. Attendance-rate / leave-usage is a leave-management * or payroll concern that reads the approved LeaveRequest (joining time-tracking's aggregateWorkedDays * if needed). LeaveType.timeEntryCodeKey is the mapping a downstream payroll/reporting consumer uses * to render/pay the leave — leave-management never emits into the attendance record, keeping the * module dependency one-directional (leave-management → time-tracking, pull-based). */ export async function run( db: Transaction, input: ApproveLeaveInput, ctx: CommandContext, deps: ApproveLeaveDeps, ) { 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-approval is detected by comparing it against // this same-module request's workerId (ADR-003) — no cross-module lookup needed for this check. if (leaveRequest.workerId === input.resolvedBy) { return err(new SelfApprovalError(input.id)); } if (leaveRequest.status !== "PENDING") { return err(new InvalidStateTransitionError(input.id)); } // Locate the mirrored approval request by this LeaveRequest's id, then the assignee row the // approver (resolvedBy) owns on its active step. No active request or no owned assignee means // the resolver is not an eligible approver for this request. 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.approveApprovalStep( db, { approvalStepAssigneeId: assignee.approvalStepAssigneeId, comment: input.approverComment }, ctx, ); if (!decision.ok) { switch (decision.error.code) { case APPROVAL_NOT_ACTIVE_ASSIGNEE: return err(new NotAssigneeError(input.id)); case APPROVAL_SELF_DECISION_NOT_ALLOWED: return err(new SelfApprovalError(input.id)); default: return err(new ApprovalStepFailedError(input.id)); } } const updated = await db .updateTable("LeaveRequest") .set({ status: "APPROVED", resolvedAt: new Date(), resolvedBy: input.resolvedBy, }) .where("id", "=", input.id) .returningAll() .executeTakeFirstOrThrow(); // Reserved LeaveConsumption rows are intentionally left unchanged on approval (per doc). return ok({ leaveRequest: updated }); }