Skip to main content

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) and wouldExceedCap (strict over-cap check). Keeps the cap math out of the actions.
  • Submit (submitLeaveRequest, contractor): inserts a pending request; 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 reason to avoid a migration; if richer holiday metadata is needed later, add columns then.