# Spec 050b — Everee Pay Run Plumbing (Phase 1b, Backend + Web)

**Scope:** Phase 1b ("Pay Run Plumbing") of the Everee white-label payroll integration — **Backend + Web slice only**.
**Builds on:** [`050-everee-payroll-foundations`](../050-everee-payroll-foundations/README.md) (Phase 0 + 1a — schema, services, sandbox channel, identity model; PR #75).
**Source analysis:** [`docs/everee-payroll-integration-analysis.md`](../../everee-payroll-integration-analysis.md) §7 "Phase 1b — Pay Run Plumbing" (lines 503–517).
**Sibling slice (deferred):** `050c-everee-pay-run-mobile` — Flutter Live app screens + Team app pay-stub viewer (analysis §7 mobile surfaces).

## Status

**ALL THREE GATES CONVERGED (Codex-reviewed) — IMPLEMENTATION STARTED (2026-06-04). ADR-1b…ADR-14b signed off.**

**Implementation progress (2026-06-04):**
- ✅ **T2 migrations (6)** authored + applied live (central `kiosk_buykiosk`/`kiosk_users` + all 7 stores), idempotent + schema-verified via INFORMATION_SCHEMA.
- ✅ **T6 `PayPeriodCalculator`** — `Models/PayPeriod` + `Services/PayPeriodCalculator`, 18 golden tests / 40 assertions green, PHPStan clean. Anchored weekly/biweekly/semi-monthly/monthly generation + `periodsInPayrollYear` (52/26/24/12) for salaried division.
- 🔎 **CON-10 / T1 reduced:** the pay-run submit is the documented Everee **Payables API** (two-step `payables/bulk` → `payables/payment-request`; externalId idempotency; atomic; Everee admin-approval layer), NOT portal-only. Captured in `userfrosting/docs/interfaces/everee-payables-api.md`; SDD ADR-4b/5b/9b + Example 3 revised. T1 residual = webhook payload field names + embedded-onboarding shape + sandbox confirmation.
- ▶️ Next: remaining core services (T3 readiness gate, T4 resolver, T5 classification, T9 repos) → T10 the money-math calculator. All unblocked (T1 only gates T7/T8/T11/T12).

The slice carves the analysis's Phase 1b into a **web-first** deliverable so a store owner can complete an end-to-end sandbox pay run without waiting on the Flutter app shells. The mobile surfaces (Live screens, Team pay-stub viewer) are explicitly Won't-Have here and move to `050c`.

| Document | Status | Last updated |
|---|---|---|
| [product-requirements.md](./product-requirements.md) | Codex-reviewed (3 rounds), clean | 2026-06-03 |
| [solution-design.md](./solution-design.md) | Codex-reviewed (5 rounds), ADRs signed off | 2026-06-04 |
| [implementation-plan.md](./implementation-plan.md) | **Codex-reviewed (2 rounds), ready to implement** | 2026-06-04 |

## Slice shape at a glance

**Backend (5 features):**
1. Employee onboarding kickoff service (embedded onboarding; `users.id → evereeWorkerId`; no PII in BK)
2. Pay run lifecycle service (draft from approved punches → preview → idempotent submit → cancel)
3. Salaried / exempt employee model (wire `userStoreAssignments.employmentClassification`, which Phase 1a created but reads nowhere)
4. Complete all 11 Everee webhook handlers (the 5 `payment.*` / `worker.new-tax-forms-available` events stubbed as no-ops in Phase 1a)
5. Pre-launch readiness gate (single seam enforcing sandbox-only)

**Web admin (6 pages):**
6. Payroll landing + nav
7. `payroll-setup` provisioning wizard
8. `payroll-employees` roster + onboarding kickoff
9. `payroll-rates` rate management
10. `payroll-runs` list (Syncfusion EJ2 Grid)
11. `payroll-run-detail` line items + dry-run preview + submit

**Capability deliverable:** an owner provisions → onboards ≥1 worker to "ready to pay" → sets a rate → creates a run → dry-run previews → submits → sees a `payment.paid` webhook update the line, all on sandbox.

## Key foundations this slice reuses (no shadow infrastructure)

From `BuyerKiosk\Payroll\` (Phase 1a, PR #75):
- `EvereeProvisioningService::{provisionTenant, provisionManually, reassignStore}`
- `PayRateService::{setRate, getRate, retireRate}` (append-only `payRateHistory`)
- `EvereeApiClient::{listWorkers, createWorker (embedded), createCompanyInstance}`
- `EvereeWebhookHandler::ingest` + `ProcessEvereeWebhookJob` (5 events stubbed via `DEFERRED_PHASE_1B_EVENTS`)
- `PayrollAuditService`, `SchedulingProviderGate::assertAllowed`, `EvereeTokenStorage`
- `PayrollAdminController` + `routes/api/payroll.php` (provision/setRate/retire/as-of already wired)
- 12 payroll permission keys (`manage_payroll`, `submit_pay_run`, `approve_pay_run`, `set_pay_rate`, `kickoff_employee_onboarding`, …)
- Empty-but-ready tables: `payrollRuns`, `payrollRunLines`, `payrollRunSnapshots`

## Decisions ledger

Decisions made during PRD authoring not already in the source analysis or the 050 PRD:

| Decision | Date | Rationale |
|---|---|---|
| Split analysis Phase 1b into a **web slice (050b)** + **mobile slice (050c)** | 2026-06-03 | The "complete sandbox pay run" deliverable is achievable on web alone; carving mobile keeps 050b shippable at the low end of the 4–6 wk estimate and de-risks pilot start. Mobile surfaces are additive, not blocking. |
| **Primary persona shifts to the Pilot Store Owner** | 2026-06-03 | First merchant-visible phase; the engineer/CS personas from 050 become secondary. |
| Pre-launch **readiness gate built as a single seam** in this slice | 2026-06-03 | Production credentials stay un-wired; the later production phase opens one deliberate gate rather than touching scattered code. PRD Feature 5. |
| Salaried model **stays in `payRateHistory`** (no `annualSalaryCents` shadow field) | 2026-06-03 | Carries the 050 locked decision; this slice only *wires* the existing `employmentClassification` + `salary_annual` rateType, adds no shadow storage. |
| All 11 webhook handlers **completed here**, not deferred further | 2026-06-03 | A run's "submitted → paid" UX depends on the `payment.*` events that Phase 1a stubbed; pay-run plumbing is incomplete without them. |

## PRD Review Log

### Codex `/prd-review` — 2026-06-03 (iterative loop to convergence)

**Round 1 — 4 blockers + 7 important/enhancement findings:**
1. Pay-period source unresolved (arbitrary dates vs pay frequency). → Resolved: tenant-level pay frequency generates periods; arbitrary ranges unsupported (off-cycle = MVP-Launch-Ready).
2. Approval/submit semantics ambiguous (manager approval, pending state, threshold, possible Everee deep-link). → Resolved: one **BK-side** state machine `draft → pending_approval → submitted → paid|partial_error|error|cancelled`; no dependency on an Everee approval endpoint; above-threshold gate wired-but-inert.
3. Tenant-wide runs vs `typeNum`-scoped permissions unreconciled. → Resolved (see round 2).
4. Not-ready worker handling conflicted ("excluded" vs "blocked" vs "may exclude"). → Resolved: **blocks** the run by default; only an explicit, audited worker-level exclusion proceeds.
- Important/enhancement: gross-vs-net wording, salaried-exempt-only scope, production-deferral normalization, Feature 12 + landing-page scope, `payment.updated-payment-method` AC, unmeasurable metrics, EJ2/modal specifics in ACs, missing edge cases, the `未` typo. All addressed.

**Round 2 — 3 of 4 blockers confirmed resolved; 1 remained + 3 important:**
- Blocker: the round-1 "access rule" introduced a tenant-wide-run-identity contradiction (excluding a store the actor can't access leaves it unpayable under the one `(tenant, period)` key). → Resolved: **no partial tenant-wide run** — actor must hold permission at *every* BK-native store under the tenant, else create/submit/approve is refused. (Worker-level exclusion is distinct and retained.)
- Important: `partial_error` not in the state taxonomy → added to state machine + runs grid. Portal-only submission undercut the submit contract → reframed as an explicit partner-gated implementation blocker (not a silent fallback). Pay frequency should be tenant-level → normalized.

**Round 3 + final confirmation — clean:**
- Codex: *"No product blockers remain… The PRD is clean and ready for SDD."* Two stale-wording cleanups (resolved-log line + pay-schedule phrasing) and one typo (`audibly`→`auditably`) corrected.

**Net product decisions made during review** (also in the PRD's "Resolved during PRD review" block): tenant-level pay frequency · BK-side approval state machine · tenant-wide runs require full-tenant access (no partial runs) · not-ready workers block (audited exclusion only) · hourly + salaried-exempt only · gross-only preview · landing page in scope · Feature 12 fast-follow · portal-only submission = SDD-finalization blocker.

## Ready for SDD?

- [x] Codex `/prd-review` pass complete (3 rounds + confirmation), all blockers resolved
- [x] Personas/journeys cover the merchant-facing flow end to end
- [x] Every feature has testable acceptance criteria
- [x] MoSCoW complete; mobile + 1c + MVP-launch deferrals named explicitly
- [x] No technical implementation details / API specs (relocated to constraints / deferred to SDD)
- [x] Open questions triaged (3 partner-gated/owed items flagged, none block SDD authoring)

**Decision: READY FOR SDD.** Partner-gated caveat carried forward: SDD *finalization* (not authoring) is gated on confirming the Everee pay-run submission API shape, the embedded-onboarding flow, and the payment-webhook payloads — or explicitly designing the manual-submission seam.

## SDD Review Log

### Codex `/sdd-review` — 2026-06-03 (5 rounds + 2 touch-ups to convergence)

The SDD review was the deepest of the three gates — payroll is money, so Codex pushed hard on the gross-pay math and the submit/recovery state machine.

**Round 1 — 6 blockers + 9 important.** Position/date rate resolution (timesheets are week aggregates with no positionId) · pay-period generation not derivable (biweekly needs an anchor) · salaried proration undefined (periodsPerYear/rounding) · sandbox gate had no real signal (only `isActive`) · `UserPayrollProfile` mirror fields not in schema · worker exclusion not durably modeled. Plus float in the gross example, onboarding-needs-rate contract, enum mid-insert, webhook unmatched-vs-zero-row, cross-DB ordering, tenant-wide classification consistency.
**Round 2 — 4 blockers + 6 important** (two introduced by my round-1 edits). The lock-after-submit order created an editable-after-accept window · `SELECT FOR UPDATE` doesn't gap-lock absent rows · cross-store `positionId` collision (per-store local ids vs central run lines) · 53rd/27th-pay-period breaks the salaried "sums exactly" claim. Plus OT-blocker breadth, semi-monthly OT period, regular-hours drift, cancel-readiness.
**Round 3 — 0 blockers, 8 consistency cleanups.** Stale cross-references between edited sections; the uncertain-submit release-safety tightening.
**Round 4 — 1 blocker (recovery predicate).** Releasing locks on run-status alone could unlock an accepted-but-not-yet-written run → added a durable pre-call `needs_reconciliation` marker so recovery only releases on authoritative proof or "submit never started."
**Round 5 + touch-ups — clean.** Codex: *"the SDD is ready for the implementation plan, with CON-10 live sandbox capture as the hard phase-0 gate."*

**Net new design decisions (ADRs 1b–14b)** — schema-reconciled against the as-built Phase-0 tables:
- Append-only enum extend (`pending_approval`/`partial_error`/`needs_reconciliation`; PRD "error"=`failed`) [1b]
- Authoritative approved-timesheet totals; punches only allocate by largest-remainder; weekly OT to the `weekEndDate` period; multi-rate-OT weeks blocked [2b]
- Tenant-wide classification consistency keeping `users.isExempt` in sync [3b]
- BK-side approval state machine [4b] · idempotent submit, uncertain-even-with-key, portal-only seam [5b]
- Lock-before-submit + pre-call marker + proof-gated recovery [6b] · QuickBooks-clone web pages [7b]
- Anchored pay-period generation (+anchorDate, +semiMonthly split day) [8b] · 3-case webhook matching [9b]
- Tenant-wide full-access via `TenantStoreResolver` [10b] · real `environment` sandbox signal [11b]
- Durable `payrollRunExclusions` [12b] · `(userId,typeNum,positionId,rate-segment)` line grain + payroll-year salaried division [13b] · serializing-lock draft idempotency [14b]

**6 migrations** (5 central + 1 per-store): enum extend · run-line positionId/typeNum · tenant environment+anchor · profile mirror cols · exclusions table · per-store run-attributed lock.

## Ready for Implementation Plan?

- [ ] **Owner signs off ADR-1b…ADR-14b** (the SDD validation checklist gates on this)
- [x] Codex `/sdd-review` converged (5 rounds), every PRD feature has a component + interface + test
- [x] Schema reconciled with the as-built Phase-0 tables (no contradictions); 6 migrations specified
- [x] The money-critical paths (gross math, salaried division, submit/recovery state machine) are integer-exact and proof-gated
- [ ] **CON-10 phase-0 gate:** live Everee sandbox capture of submit API + embedded-onboarding + payment webhooks (the implementation plan must place this FIRST)

**Decision: ready to write the implementation plan once the ADRs are signed off.** The plan's phase 0 is the CON-10 live capture.

## Implementation Plan Review Log

### Codex `/plan-review` — 2026-06-04 (2 rounds to convergence)

**Round 1 — 1 blocker + 5 important + 1 enhancement.** The classification/rate-type mismatch guard (PRD F3 / SDD Scenario 14) had no owning task → added `assertRateTypeMatches` to T5 + wired it into the T13 rate-set path with the `payroll.classification.mismatch_rejected` audit. Tightened deps (T8←T3, T10←T4, explicit `depends T2`). Closed test-scenario gaps in T10/T11 (exclusion end-to-end, synchronous submit-response partial rejection, empty-period, all-excluded, backdated-rate-after-submit immutability). Added the SDD-named `docs/patterns/payroll-pay-run-lifecycle.md` deliverable (T11.5).
**Round 2 — clean.** Codex: *"The revised plan is complete enough to begin implementation. No remaining blockers."* (One trivial `depends T2` on T4 applied.)

**Plan shape:** 15 tasks (T1–T15), ~70 sub-tasks, TDD (Prime→Test→Implement→Validate) throughout, every PRD feature + SDD component owned, every task referencing the SDD's 23 test scenarios.

## Ready to Implement?

- [x] PRD, SDD, Plan all Codex-converged
- [x] ADR-1b…ADR-14b owner-signed (2026-06-04)
- [x] Every SDD component + PRD feature has an owning task; deps mapped, no cycles
- [x] T1 (CON-10 live capture) is the hard gate before submit/onboarding/webhook impl
- [x] The money-critical work (T10 golden-fixture gross) precedes web submit wiring

**Decision: READY TO IMPLEMENT.** Begin with **T1 — CON-10 live Everee sandbox capture** (the hard partner-gated gate), then T2 migrations, then the parallel core services (T3/T4/T5/T6/T9). Implementation can be driven with `/start:implement` or task-by-task.
