import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction } from "../generated/kysely-tailordb"; import { type GetActiveApprovalRequestFn, type WithdrawApprovalRequestFn, LEAVE_REQUEST_TARGET_ENTITY_TYPE, } from "../lib/_approvalDeps"; import { LeaveRequestNotFoundError, InvalidStateTransitionError, NotRequesterError, ApprovalStepFailedError, } from "../lib/errors.generated"; export interface WithdrawLeaveInput { id: string; requestedBy: string; comment?: string; } export interface WithdrawLeaveDeps { getActiveApprovalRequest: GetActiveApprovalRequestFn; withdrawApprovalRequest: WithdrawApprovalRequestFn; } export async function run( db: Transaction, input: WithdrawLeaveInput, ctx: CommandContext, deps: WithdrawLeaveDeps, ) { const leaveRequest = await db .selectFrom("LeaveRequest") .selectAll() .where("id", "=", input.id) .forUpdate() .executeTakeFirst(); if (!leaveRequest) { return err(new LeaveRequestNotFoundError(input.id)); } // requestedBy is compared against this same-module request's own workerId field; no // cross-module lookup is needed to enforce that only the original requester may withdraw. if (leaveRequest.workerId !== input.requestedBy) { return err(new NotRequesterError(input.id)); } if (leaveRequest.status !== "PENDING") { return err(new InvalidStateTransitionError(input.id)); } // Close the mirrored approval request as withdrawn by the requester. A resolved/absent request // (nothing active) is a no-op; only a live request needs closing to keep the two records in sync. const requestResult = await deps.getActiveApprovalRequest(db, { targetEntityType: LEAVE_REQUEST_TARGET_ENTITY_TYPE, targetEntityId: input.id, }); if (!requestResult.ok) { return err(new ApprovalStepFailedError(input.id)); } const request = requestResult.value.approvalRequest; if (request) { const withdrawResult = await deps.withdrawApprovalRequest( db, { approvalRequestId: request.id, comment: input.comment }, ctx, ); if (!withdrawResult.ok) { return err(new ApprovalStepFailedError(input.id)); } } const updated = await db .updateTable("LeaveRequest") .set({ status: "CANCELLED", }) .where("id", "=", input.id) .returningAll() .executeTakeFirstOrThrow(); const consumptions = await db .selectFrom("LeaveConsumption") .selectAll() .where("leaveRequestId", "=", input.id) .where("restoredAt", "is", null) .forUpdate() .execute(); for (const consumption of consumptions) { const grant = await db .selectFrom("LeaveGrant") .selectAll() .where("id", "=", consumption.leaveGrantId) .forUpdate() .executeTakeFirst(); if (grant) { const restoredRemaining = ( Number(grant.remainingDays) + Number(consumption.daysConsumed) ).toString(); await db .updateTable("LeaveGrant") .set({ remainingDays: restoredRemaining }) .where("id", "=", grant.id) .execute(); } await db .updateTable("LeaveConsumption") .set({ restoredAt: new Date() }) .where("id", "=", consumption.id) .execute(); } return ok({ leaveRequest: updated }); }