Skip to main content

12. Corrected contractor pay-calculation engine

Date: 2026-06-06

Status

Accepted

Context

The source spec's calculateContractorPay underpaid contractors. It computed weeklyRate = bimonthlyRate / weeks.length (the count of ISO-week buckets touching the period) and then also prorated partial weeks by days/7. That double-discount means a contractor who works every contracted hour earns far less than their agreed rate — verified at ~28.6% short for a 15-day period, and varying between ~57% and ~76% for a 16-day second period depending on which weekday the 16th falls on (so pay even drifted month to month for identical work).

Two further bugs: multi-day holidays were attributed only to the week containing their start_date (days bleeding into the next week were dropped), and the "subtract holiday from both worked and contracted" rule only preserves full pay if the holiday is also logged as worked time — which it isn't for in-house admins (Hubstaff logs no activity on a day off), so it would underpay anyone taking a holiday.

Decision

Implement a pure engine in src/lib/pay (no I/O, returns Result<PayBreakdown, PayError>, operates in Centavos):

  1. Period-fraction allocation. Split the period into Monday-aligned weeks clipped to the period (src/lib/dates), then allocate the bi-monthly rate across them in proportion to each week's day-count via allocateByWeights. The allocations sum to exactly the bi-monthly rate by construction, so full-time work over any period shape pays exactly the rate.
  2. Ratio capped at 1.0. Each week pays allocation × min(1, effectiveHours / adjustedContracted). Overtime in one week never subsidizes a short week; proration only ever reduces pay.
  3. Holiday credited as worked time. Approved holiday hours (in-house admins only) are added to worked hours (capped at contracted), so working the full non-holiday schedule reaches ratio 1 ("ratio preserved"), while partial idleness is still prorated. Holiday hours are prorated per-day across the weeks a multi-day holiday overlaps (interval overlap, not start-date-in-week).

Correctness is locked by golden tests (tests/lib/pay): full-time work pays exactly the bi-monthly rate across every month, both halves, and all weekday alignments for 13/14/15/16-day periods; plus proration, holiday-spanning-weeks, outsourced-ignores-holiday, an explicit anti-regression against the old buggy value, and a property test for the sum/rounding invariant.

Consequences

  • Contractors are paid correctly and predictably; the engine is the single source of pay truth, reused by payroll, payslips, and the contractor "estimated pay" display.
  • Holiday semantics differ deliberately from the spec's literal wording; documented here and in code so the divergence is intentional and traceable.
  • The engine assumes holiday hours are not also present as time entries. When wiring real data, in-house holiday hours must come from the holiday source, not from Hubstaff time entries, to avoid double counting.
  • All time math is UTC-based calendar arithmetic (src/lib/dates), avoiding the local-timezone hazards in date-fns' interval/week helpers. The operating-timezone decision for deciding which business day a wall-clock instant belongs to is deferred to a later ADR (cron/timezone).