Skip to main content

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) set status = '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_batch already exists for its bi-monthly period with status prepared/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.
  • 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_adjustment attaches to a payment, which doesn't exist until a draft is prepared. So catch-ups are queued in a new table pending_pay_items (migration 0048; company-scoped RLS + audit). When the next draft is prepared, prepareDraft drains each contractor's unapplied items into real payment_adjustments (kind other, 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 other adjustment 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_entry trigger (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.ts updated by hand (no supabase gen types pipeline in this repo).