Skip to main content

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:

  1. 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.
  2. 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; clippedWeeksInPeriod gained a weekStartsOn option). Each week pays allocation × (paidHours / fullTimeHours), where paidHours = min(worked, contracted) normally, or the full worked when that week is in approvedOverageWeeks. 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 / estimateContractorPay load the company holidaysConfig and the period's approved overage weeks (approval_requests where status='approved', keyed by contractor|assignment → Sunday-week starts) and pass them to the engine. computeCatchupCentavos does the same so marginal computations stay consistent.
  • Admin waiver during the run (waiveOverage) writes an approved approval_requests row; the run then prices the overage automatically — no separate pay path.
  • Late approval (approveOverage facility link, or a post-close waiveOverage): if the week's bi-monthly period is already closed (a payroll_batches row is prepared|funded|closed), computeOverageCatchupCentavos prices the marginal premium (pay(approved) − pay(approved minus this week)) and queues a pending_pay_items row (kind:'other', with a note + a double-queue guard). The existing drain turns it into a payment_adjustments line 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.