Skip to main content

44. Weekly-settled outsourced pay (fix bi-monthly boundary underpayment)

Date: 2026-06-21

Status

Proposed

Amends 43. Full-time pay basis, approved overage pay, and in-house holiday proration (outsourced pricing only; the in-house model and the overage/holiday rules of 0043 stand).

Context

ADR-0043 priced outsourced contractors per Sunday–Saturday week clipped to the bi-monthly period: the rate is allocated across the period's clipped weeks by day-count, and each clip pays allocation × min(1, workedInClip / (40 × clipDays/7)), capped at 1 unless that week's overage is approved. It asserted the golden invariant — "full-time attendance every week pays exactly the bi-monthly rate" — held, and that boundary proration only "shifts slightly".

That assertion is false for real schedules. A year-long simulation running the actual engine (calculateContractorPay) across all 24 bi-monthly periods × 8 realistic weekday patterns, plus a five-agent adversarial verification (all clear, high confidence), shows the current model systematically underpays a fully-compliant 40h/week contractor by ~3–6.6% per year — for essentially every weekday pattern:

ModelCompliant-worker annual payPeriods short / 24
Current (Sunday clip + per-clip cap)93.4–97.0% of salary16–20
Monday-anchored clip (the "shift")90.4–96.8% (worse for Mon–Fri & 4×10)17–20
Whole-week settle by Sunday100.00%, every schedule0
Period-level FTE proration96.6–98.0%7–13

Root cause is mechanical (src/lib/pay/index.ts:186-188): the per-clip cap discards surplus from over-FTE clips, while the short period-boundary clips are docked, with no cross-clip carry. It is strictly worse than even period-level proration — it underpays in periods where the worker rendered more than the period's full FTE-hours, so it cannot be defended as "pay for hours actually rendered."

Why this went unseen: the golden test's fullTimeEntries helper (tests/lib/pay/pay.test.ts:24-25) logs a full 40h at each clip's start — including the 2-day boundary clip — so every clip caps to 1 and pays full. No test distributes hours by real weekday across a period boundary. The "every alignment" coverage is illusory.

Real instances (June 1–15 2026, live data): Kristine (MDS, ₱37,500) paid ₱35,000; Genesis (MDS, ₱25,000) ₱24,583; Genesis (RN, ₱25,000) ₱21,583; Jay-Ar (in-house) ₱17,500 (in-house is period-level and unaffected).

Week-anchor inconsistency. Pre-0044 pay keyed weeks on Sunday-start (sundayWeekStart, Sun–Sat), but the live approval_requests row is Mon–Sun ISO (week_start = Mon 2026-06-08, a pre-0043 leftover). For a Sunday-working contractor the anchor is not cosmetic: the same hours form different 7-day windows, so a worker compliant in Mon–Sun weeks can be docked under Sun–Sat (verified — Genesis RN's June 1–15 pay was ₱19,687 under Sun–Sat vs ₱23,125 under Mon–Sun, the latter matching her 38h/36h weekly compliance). Pay and overage must use the same window, so unifying on ISO Mon–Sun is required, not optional.

Owner decision (2026-06-21): pay whole Mon–Sun weeks (the "M2" model). Do not adopt a partial shift — and (after a verified misstep) not Sun–Sat, which underpays Sunday-workers.

As-built note (2026-06-22): Shipped as the Mon–Sun plan in §1–4 below, with one refinement: no RPC migration. Whole-week settlement means a boundary week's days can sit in the prior period, but rather than widen the close/lock + discard RPCs to the settling-week range (which conflicts with the period-aligned in-house model), the lock/TOCTOU stay period-aligned — every calendar day is locked by exactly one period — and the close-time snapshot counts only the in-period subset while pay settles whole weeks. The anchor flip touched pay, weekly-hours, overage detection/approval, manual entry, the contractor calendar, and performance scoring; facility invoicing keeps its own Sunday weeks (a separate billing domain). An interim Sun–Sat implementation was built and reverted after the Genesis-RN underpayment above surfaced in real-data validation.

Decision

1. Canonical pay week — ISO Mon–Sun, settled by its Sunday

Define one week everywhere — pay settlement, the weekly-hours matrix, overage detection, and approval keys: the ISO Mon–Sun week, which settles in the bi-monthly period containing its Sunday (the week's last day). This reverses 0043's Sunday-start choice; the existing approval data is already Mon–Sun, so it becomes consistent rather than conflicting. (0043 chose Sunday-start to align pay with overage; we preserve that alignment goal by moving overage to the same ISO week, and pick ISO because settling by the Sunday-end attributes a week's pay to the period its work mostly falls in — e.g. June P1 pays the weeks ending Jun 7 and Jun 14, i.e. work Jun 1–14 — which Sunday-start does not.)

2. Outsourced engine (calculateOutsourcedPay, pure)

Replace clip-by-day-allocation + per-clip cap with whole-week settlement:

weeks = ISO Mon–Sun weeks whose Sunday ∈ [period.start, period.end]
ratio(w) = approvedOverage(w) ? worked(w)/40 : min(1, worked(w)/40) // worked = the week's FULL Mon–Sun hours
pay = rate × ( Σ ratio(w) / weeks.length ) // rate split equally across settling weeks

Properties (proven by simulation): a worker who renders 40h in every week earns exactly the bi-monthly rate, for every period length and weekday alignment; hours over 40/week never inflate base unless approved (cap preserved per-week, not per-clip); a part-timer still earns the worked fraction of full-time. The in-house admin path (calculateInHousePay, period-level, holiday/PTO) is unchanged.

3. Data fetch (the real ripple — computePayRun, estimateContractorPay, computeCatchupCentavos)

These currently fetch time_entries with entry_date BETWEEN periodStart AND periodEnd. Whole-week settlement needs each settling week's full hours, and a settling week's Mon–Sat days can fall in the prior period. Therefore:

  • Widen the entry query to cover the settling weeks' span (≈ periodStart − 6 days … periodEnd), then group by ISO week and keep only weeks whose Sunday is in the period.
  • No double-pay: each week has exactly one Sunday → one period (the bi-monthly halves partition the calendar). A week's Mon–Sat days that sit in an adjacent period are paid by this period (the one holding its Sunday), never twice.
  • Keep the close-time TOCTOU check (pricedEntryCount / pricedHours) consistent with the widened set between preview and close.

4. Overage coupling

Detect overage and key approval_requests on the same ISO Mon–Sun week; the engine's approvedOverageWeeks lookup must use the identical key. Audit/repair existing rows (the lone live row is already Mon–Sun). This closes the latent "approved overage never matches" gap.

5. Offboarding / termination — IMPLEMENTED (owner chose day-proration, 2026-06-22)

Without this, a mid-week offboard could lose the contractor's last days: their final week settles by its Sunday, which may fall in the next period — but the next run excludes the ended assignment (end_date < periodStart), so the week is paid by neither period.

Decision: when an assignment's end_date falls inside a period, that final period switches from whole-week settlement to day-proration:

active window = [max(period.start, effective_date), end_date]
pay = rate × (activeDays / periodDays) × min(1, worked / (40 × activeDays/7))

The time-fraction prorates by how much of the period the contractor was active; the capped fulfillment is their worked share of full-time over that window. A contractor active the whole period still earns the full rate; no day is dropped; no week-boundary over/under-pay. Ongoing assignments (no in-period end_date) are untouched (whole-week settlement). Implemented in calculateContractorPay via calculateTerminatingPay; end_date/effective_date are threaded from the assignment through computePayRun / estimateContractorPay / the catch-up service. Rejected alternative: re-homing the final week into the equal split (dilutes full weeks or over-pays by ~a week-share at the boundary).

6. Tests

Add a permanent test that distributes hours per real weekday (a genuine 40h/week schedule) across a period boundary and asserts full pay — the gap the current golden test cannot see. Fix/augment fullTimeEntries so the suite exercises realistic daily distributions, not 40h-at-clip-start.

7. Migration of already-paid periods

Historical pay has not been imported yet (the timesheet/payroll reconstruction is still pending), so there are effectively no M0-priced closed periods in the system to true up. M2 therefore applies cleanly to all open and future runs, and the historical true-up question is deferred:

  • Open periods (e.g. June 1–15 2026, not yet closed) close under M2 directly.
  • When historical pay is imported/reconstructed, reconstruct under M2 from the start so those periods are correct rather than re-broken.
  • Only if some periods were already closed under the 0043 clip+cap before this change ships: true-up the per-assignment delta (M2 − paid) via the existing catch-up → pending_pay_itemspayment_adjustments machinery over an owner-chosen look-back, after sign-off on the reconciliation report (the simulation's Query-4-style diff). Owner to decide the look-back at that time.

Consequences

  • A fully-compliant contractor is paid exactly their salary regardless of weekday pattern; the documented golden invariant becomes true for real schedules, not just the test fixture.
  • Per-period lumpiness for sub-compliant workers: a period settles 1–3 whole weeks and pay is the average of their fulfillment, so a light week weighs more in a 2-week period than a 3-week one. This is inherent to whole-week settlement and is annually neutral for compliant workers.
  • Boundary attribution changes (a week's pay follows its Sunday), and the entry-fetch now reaches one partial week before the period — a behavioural change to verify against the close TOCTOU.
  • The week anchor is unified on ISO Mon–Sun across pay, weekly-hours, overage, and approvals, fixing the approved-overage key mismatch.
  • Supersedes ADR-0043's outsourced pricing only; 0043's full-time basis, approved-overage payment, catch-up flow, and in-house holiday/PTO proration are retained.
  • Back-pay exposure if historical true-up is chosen (bounded by the look-back window); the per-period amounts are small (≈₱14.6k–₱31.5k/yr per ₱480k-salary contractor) but real.

Evidence / reproduction

Year-long, multi-model simulation against the real engine + a 5-agent adversarial verification (engine-faithfulness, M2-correctness, methodology-bias, golden-test-masking, and a steelman that the current engine is correct-by-design — all failed to refute). Throwaway harnesses (tests/tmp-*.test.ts) were used and removed; the permanent regression test in §6 replaces them.