38. Owner manual time entry: auto-approve + out-of-period catch-up
Date: 2026-06-16
Status
Accepted
Context
Owner-entered manual hours previously landed submitted/pending_approval and had to be approved again
on the payroll grid — redundant, since the owner is the approver, and it left the invoicing soft-gate
(which holds a send until hours are approved) stuck. Separately, payroll pulls time entries strictly by
entry_date within a period, so hours logged for an already-closed period are never picked up by a
future run — they'd be silently logged-but-never-paid.
Decision
- Owner manual entries auto-approve.
createManualTimeEntry(ies)setstatus = 'approved'directly (the owner is the approver). They're immediately payable and clear the invoicing soft-gate. Contractor self-logged time (upsertTimeEntry) is unchanged — it still goes through approval. The week-overage reflag was removed from the manual path (moot once auto-approved). - Out-of-period detection. An entry is "out of period" when a
payroll_batchalready exists for its bi-monthly period with statusprepared/funded/closed. Open periods (no batch /draft) just auto-approve. - Disposition prompt. When a submission includes out-of-period hours, the action returns
needsDisposition(with the computed catch-up ₱) and the form prompts:- Already paid → record the day
locked+ note"Already paid (manual backfill)". Never re-paid. - Add to next run → record the day
locked+ note, and queue a catch-up pay item.
- Already paid → record the day
- Catch-up amount = marginal pay in the day's own period. Computed (
computeCatchupCentavos) by running the real pay engine for that period with vs. without the new hours:pay(existing + new) − pay(existing). This respects the bi-monthly-rate proration and the 100% weekly cap (hours in a week already at cap add ₱0), so the contractor is paid exactly the fraction those hours would have earned. - Queue, not direct adjustment. A
payment_adjustmentattaches to apayment, which doesn't exist until a draft is prepared. So catch-ups are queued in a new tablepending_pay_items(migration 0048; company-scoped RLS + audit). When the next draft is prepared,prepareDraftdrains each contractor's unapplied items into realpayment_adjustments(kindother, note carried over) on their payment and marks them applied. The drain is best-effort — a failure leaves items queued for the next prepare.
Consequences
- Catch-up hours are paid on the next run the contractor is in, as a transparent
otheradjustment with a note, never by mis-dating the hours into a different period. - The catch-up is computed once at log time against the then-current baseline; it's stored as a fixed ₱ amount (it does not re-float if the original period's hours change later).
- Locked catch-up entries are frozen by the
guard_locked_time_entrytrigger (0045) like any locked day. - Out-of-period entries that already have a locked day are skipped (can't change an already-paid day); the catch-up flow covers missing days in a closed period.
- Migration 0048 (
pending_pay_items) applied;db/types.tsupdated by hand (nosupabase gen typespipeline in this repo).