import { ok, type ReadonlyDB } from "@tailor-platform/erp-kit/core"; import type { DB } from "../generated/kysely-tailordb"; export interface GetAnnualLeaveRegisterInput { workerId: string; /** Optional: restrict the register to one balance-backed leave type; omitted = all STATUTORY grants. */ leaveTypeKey?: string; } // Article 39 ⑦: a worker granted 10+ days of annual leave in a grant year must actually take at // least 5 of them within the year following that grant. const FIVE_DAY_DUTY_THRESHOLD_DAYS = 10; const FIVE_DAY_DUTY_REQUIRED_DAYS = 5; // Only actually-taken leave counts toward the register and the 5-day obligation: an APPROVED // LeaveRequest. PENDING/CANCEL_PENDING reservations are not yet "taken", and REJECTED/CANCELLED // never happened. const TAKEN_STATUS = "APPROVED"; function oneYearAfter(date: Date): Date { const deadline = new Date(date); deadline.setUTCFullYear(deadline.getUTCFullYear() + 1); return deadline; } /** * Function: getAnnualLeaveRegister * Description: The annual paid-leave compliance register for one worker (ADR-009), derived entirely * from the ledger — there is no stored register. One row per STATUTORY LeaveGrant: the grant's date, * amount, expiration, the days actually taken against it, the derived remaining, and — for a grant of * 10+ days — the 5-day mandatory-acquisition obligation (Labor Standards Act Article 39 ⑦): its * grant-year deadline (grantedDate + 1 year), how many paid-leave days were taken in that window, and * whether the duty is satisfied or the worker is at risk. "Taken" means an APPROVED LeaveRequest. */ export async function run(db: ReadonlyDB, input: GetAnnualLeaveRegisterInput) { let grantsQuery = db .selectFrom("LeaveGrant") .selectAll() .where("workerId", "=", input.workerId) .where("grantType", "=", "STATUTORY"); if (input.leaveTypeKey != null) { grantsQuery = grantsQuery.where("leaveTypeKey", "=", input.leaveTypeKey); } const grants = await grantsQuery.orderBy("grantedDate", "asc").execute(); // Every APPROVED taking for this worker's grants, with the taking's start date, so both the // per-grant taken total and the per-grant-year 5-day count can be reconstructed from the ledger. const grantIds = grants.map((grant) => grant.id); const takings = grantIds.length ? await db .selectFrom("LeaveConsumption") .innerJoin("LeaveRequest", "LeaveRequest.id", "LeaveConsumption.leaveRequestId") .select([ "LeaveConsumption.leaveGrantId", "LeaveConsumption.daysConsumed", "LeaveRequest.startDate", ]) .where("LeaveConsumption.leaveGrantId", "in", grantIds) .where("LeaveRequest.status", "=", TAKEN_STATUS) .execute() : []; const takenByGrant = new Map(); for (const taking of takings) { takenByGrant.set( taking.leaveGrantId, (takenByGrant.get(taking.leaveGrantId) ?? 0) + Number(taking.daysConsumed), ); } const register = grants.map((grant) => { const grantedDays = Number(grant.grantedDays); const takenDays = takenByGrant.get(grant.id) ?? 0; const subjectToFiveDayDuty = grantedDays >= FIVE_DAY_DUTY_THRESHOLD_DAYS; const fiveDayDeadline = subjectToFiveDayDuty ? oneYearAfter(grant.grantedDate) : null; // The 5-day obligation counts paid leave taken anywhere in the grant year, not only what drew // from this specific grant. let fiveDayTakenDays = 0; if (fiveDayDeadline) { for (const taking of takings) { if ( taking.startDate.getTime() >= grant.grantedDate.getTime() && taking.startDate.getTime() <= fiveDayDeadline.getTime() ) { fiveDayTakenDays += Number(taking.daysConsumed); } } } const fiveDayDutyMet = subjectToFiveDayDuty ? fiveDayTakenDays >= FIVE_DAY_DUTY_REQUIRED_DAYS : false; return { leaveGrantId: grant.id, leaveTypeKey: grant.leaveTypeKey, grantedDate: grant.grantedDate, grantedDays, expirationDate: grant.expirationDate, takenDays, remainingDays: grantedDays - takenDays, subjectToFiveDayDuty, fiveDayDeadline, fiveDayTakenDays, fiveDayDutyMet, fiveDayAtRisk: subjectToFiveDayDuty && !fiveDayDutyMet, }; }); return ok({ register }); }