43. Full-time pay basis, approved overage pay, and in-house holiday proration
Date: 2026-06-19
Status
Accepted
Context
The bi-monthly rate is a salary, but the engine treated it as "pay in full once contracted hours are met, capped there" — so overage (hours over contract) was never paid, and a part-timer earned the same as a full-timer. Two business rules forced a model change:
- The rate is a full-time (40h/week) amount. Pay should be the worked fraction of full-time, not of contract: a 30h-contracted contractor at perfect attendance earns 30/40 of the rate. Hours over the contract are still overage that needs approval, even when the contract is below 40h.
- Approved overage is paid. When an over-contracted week is approved — by an admin waiver during the run, or by the facility after the run — the contractor is paid the full worked hours for that week (50/40 of the week's share for a 50h week against a 40h contract). A waiver during the current run pays in that run; a facility approval after the run is paid as a catch-up on the next run, with a note.
In-house admins (employed by Nightingale, not deployed to a facility) are salaried with no overage. Their expectation is the period's working days × 8h, reduced by company-observed holidays (a closed-office day shouldn't prorate them down), with approved PTO credited as worked. Holidays are configurable in the Setup Menu and apply to in-house admins only.
Decision
Pay engine (src/lib/pay/index.ts, pure)
Branch by contractor type; the bi-monthly rate is always a full-time (40h/week → 8h/day) salary.
- Outsourced — priced per Sunday–Saturday week (the boundary overage is detected and approved
on;
clippedWeeksInPeriodgained aweekStartsOnoption). Each week paysallocation × (paidHours / fullTimeHours), wherepaidHours = min(worked, contracted)normally, or the fullworkedwhen that week is inapprovedOverageWeeks. Without approval the ratio is capped at 1.0 — only approved overage exceeds the full rate. - In-house admin — one period-level ratio, capped at 1.0:
(worked + PTO) / ((workingDays − holidays) × 8h). No overage. Emitted as a single period-spanning breakdown row.
The golden invariant holds: full-time attendance every week pays exactly the bi-monthly rate.
Holidays (src/lib/calendar/holidays.ts, pure; configurable)
Ten US federal holidays as code rules (fixed dates observed on the nearest working day — Sat→Fri,
Sun→Mon; nth-weekday; Good Friday via the Easter algorithm; day-after-Thanksgiving). Which are observed
is a company holidays setting (disabledStandard ids) edited in Setup → Observed holidays.
Holidays reduce only the in-house denominator.
Wiring
computePayRun/estimateContractorPayload the companyholidaysConfigand the period's approved overage weeks (approval_requestswherestatus='approved', keyed bycontractor|assignment→ Sunday-week starts) and pass them to the engine.computeCatchupCentavosdoes the same so marginal computations stay consistent.- Admin waiver during the run (
waiveOverage) writes an approvedapproval_requestsrow; the run then prices the overage automatically — no separate pay path. - Late approval (
approveOveragefacility link, or a post-closewaiveOverage): if the week's bi-monthly period is already closed (apayroll_batchesrow isprepared|funded|closed),computeOverageCatchupCentavosprices the marginal premium (pay(approved) − pay(approved minus this week)) and queues apending_pay_itemsrow (kind:'other', with a note + a double-queue guard). The existing drain turns it into apayment_adjustmentsline on the next run. If the period is still open, the next Prepare pays it directly.
Consequences
- A part-time (sub-40h) contractor now earns the worked fraction of full-time, not of contract (e.g. 30h → 75%). No current contractor is affected (all 10 are 40h); only future part-timers and overage change. The documented part-time invariant was updated and the golden tests rewritten.
- Outsourced pay is now priced on Sunday weeks (was Monday/ISO), aligning pay with overage detection, the weekly-hours matrix, and approvals. Boundary-week proration shifts slightly; the full-period invariant is unchanged.
- Approved overage can push a week (and the period total) above the bi-monthly rate — the first time pay can exceed the rate. The property test was narrowed to "no approved overage".
- In-house holidays/PTO are decoupled: holidays cut the expectation, PTO fills the numerator. A new Setup section governs which holidays apply. A future "custom holidays" extension can add to the same setting without a schema change.
- Limitation: the in-house day is a fixed 8h (40/5); a sub-40h in-house contract isn't modelled (none exist). The full-time basis (40h) is a constant, liftable to a setting later.
- Workflow order: waive overage before preparing the draft (the natural flow — the overage page is
where pending weeks are reviewed). A waiver between Prepare and Close lands in neither the draft (its
amounts are frozen at Prepare; the close-time TOCTOU only re-checks hours/entry-count, which a
status-only flip doesn't change) nor the late-approval catch-up (which fires only once a period is
prepared|funded|closed). Safeguard: the Calculate page recomputes every draft and shows a "This draft is out of date" warning when the fresh base total differs from the frozen amounts (the recomputed and prepared totals are shown), telling the owner to Delete and Calculate again before Locking. So the stale-close window is surfaced rather than silent.