import { err, ok, type ReadonlyDB } from "@tailor-platform/erp-kit/core"; import type { DB } from "../generated/kysely-tailordb"; import { InvalidSearchFilterError } from "../lib/errors.generated"; export interface AggregateWorkedDaysInput { assignmentIds: string[]; startDate: Date; endDate: Date; } const MS_PER_DAY = 24 * 60 * 60 * 1000; // UTC-midnight of a Date's calendar day, so the SQL range bounds are day-aligned regardless of // any time-of-day component on the inputs (consistent with the UTC-day dedupe below). function startOfUtcDay(d: Date): Date { return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())); } /** * Function: aggregateWorkedDays * Description: Counts a worker's worked days over a date range, for * evaluating leave grant conditions (labour-law worked days). A worked day is a * distinct workDate on which any of the supplied Assignments has at least one * CalculatedTimeBlock — a day worked under any of the assignments counts once * (union across assignmentIds). Pure producer query (ADR-026 B1): the caller * resolves worker→assignments; this query does not depend on workforce. */ export async function run(db: ReadonlyDB, input: AggregateWorkedDaysInput) { if (Number.isNaN(input.startDate.getTime())) { return err(new InvalidSearchFilterError(`startDate=${String(input.startDate)}`)); } if (Number.isNaN(input.endDate.getTime())) { return err(new InvalidSearchFilterError(`endDate=${String(input.endDate)}`)); } if (input.endDate < input.startDate) { return err( new InvalidSearchFilterError( `${input.startDate.toISOString()}..${input.endDate.toISOString()}`, ), ); } if (input.assignmentIds.length === 0) { return ok({ workedDays: 0 }); } // Day-aligned window [winStart, winEndExclusive): covers every timestamp on the // startDate..endDate calendar days, so a non-midnight input cannot exclude a boundary day. const winStart = startOfUtcDay(input.startDate); const winEndExclusive = new Date(startOfUtcDay(input.endDate).getTime() + MS_PER_DAY); const blocks = await db .selectFrom("CalculatedTimeBlock") .select("workDate") .where("assignmentId", "in", input.assignmentIds) .where("workDate", ">=", winStart) .where("workDate", "<", winEndExclusive) .execute(); // UTC calendar-date key ("YYYY-MM-DD"): workDates are UTC-midnight today, but keying on the // calendar date keeps the dedupe robust to time-of-day components and consistent with // leave-management's countDeemedAttendanceDays date keys. const distinctWorkDates = new Set( blocks.map((block) => block.workDate.toISOString().slice(0, 10)), ); return ok({ workedDays: distinctWorkDates.size }); }