18. Leave & holiday management
Date: 2026-06-06
Status
Accepted
Context
Contractors submit unpaid leave; in-house admins also have a holiday calendar with an annual cap that resets Jan 1. Approved holidays are credited as worked time by the pay engine (built earlier); unpaid leave does not reduce expected hours (the engine ignores it). Uses the existing leave_requests table — no migration needed (the reason field carries the holiday name).
Decision
- Pure accounting (
src/lib/leave, unit-tested):holidayBalance(cap − used, floored, null when uncapped) andwouldExceedCap(strict over-cap check). Keeps the cap math out of the actions. - Submit (
submitLeaveRequest, contractor): inserts apendingrequest; rejects holiday requests from non-in-house contractors and end-before-start. - Review (
reviewLeaveRequest, admin): approve/reject; approving a holiday enforces the annual cap by summing the contractor's approved holiday hours in the request's calendar year and rejecting if over. - Admin grant (
addHoliday): admin adds an already-approved holiday for an in-house contractor (cap-checked) — the "configure holiday calendar" path. - All writes go through service-role actions behind role/ownership checks. UI: contractor
/contractor/leave(submit + history + holiday balance for in-house), admin/admin/leave(pending queue with approve/reject), and an admin holiday form on the in-house contractor detail page.
Consequences
- Approved holidays now flow into the corrected pay engine's "credit as worked, ratio preserved" handling; unpaid leave correctly does not.
- The annual cap is enforced at approval/grant time, in the request's year (handles a request spanning into a new year by its start date).
- Holiday name is stored in
reasonto avoid a migration; if richer holiday metadata is needed later, add columns then.