# Implementation Plan

**Spec:** 050b-everee-pay-run-plumbing
**Scope:** Phase 1b (Pay Run Plumbing) — Backend + Web slice
**Authored:** 2026-06-04
**Companion docs:** [product-requirements.md](./product-requirements.md) (Codex-reviewed) · [solution-design.md](./solution-design.md) (Codex-reviewed, ADR-1b…ADR-14b confirmed 2026-06-04) · [README.md](./README.md)

## Validation Checklist
- [x] All specification file paths are correct and exist
- [x] Context priming section is complete
- [x] All implementation phases are defined (T1–T15)
- [x] Each phase follows TDD: Prime → Test → Implement → Validate
- [x] Dependencies between phases are clear (no circular dependencies)
- [x] Parallel work is properly tagged with `[parallel: true]`
- [x] Activity hints provided for specialist selection `[activity: type]`
- [x] Every phase references relevant SDD sections/ADRs
- [x] Every test references a PRD feature / SDD test scenario
- [x] Integration & E2E tests defined in the final phase (T15)
- [x] Project commands match actual project setup
- [x] A developer could follow this plan independently

---

## Specification Compliance Guidelines
1. **Before each phase:** read the SDD/PRD refs in the phase's Prime sub-tasks. No phase begins without that read-through.
2. **During implementation:** cite the SDD ADR/section in commit messages + PR descriptions.
3. **After each task:** run the Validate sub-tasks. Quality gates are MANDATORY.
4. **Deviation Protocol:** any deviation from the SDD gets a `## Deviation Log` entry + owner approval before merge; never deviate silently (a deviation found in review without a log entry blocks the merge).

**Metadata:** `[parallel: true]` (sibling-concurrent) · `[ref: doc/section]` · `[activity: code|migration|test|review-code|run-tests|security-review|doc-write|partner-coordination|cli-tool]`

---

## Context Priming

*GATE: every implementer reads these BEFORE picking up a phase. The PLAN points at the SDD/PRD; it does not repeat them.*

- `docs/specs/050b-everee-pay-run-plumbing/solution-design.md` — the HOW (ADRs 1b–14b, 6 migrations, the pay-run algorithms, the 23 test scenarios). CRITICAL.
- `docs/specs/050b-everee-pay-run-plumbing/product-requirements.md` — the WHAT (F1–F12 + the "Resolved during PRD review" policy block). CRITICAL.
- `docs/specs/050-everee-payroll-foundations/solution-design.md` — the foundation contracts this slice reuses.

**Foundation code to study (SDD §Implementation Context ICO-2..ICO-5):**
- `src/BuyerKiosk/Payroll/Services/{PayRateService,EvereeApiClient,EvereeWebhookHandler,PayrollAuditService,SchedulingProviderGate,EvereeTokenStorage}.php`
- `src/BuyerKiosk/Payroll/Jobs/ProcessEvereeWebhookJob.php` (the 5 `DEFERRED_PHASE_1B_EVENTS`) + `Repositories/UserPayrollProfileRepository.php` (apply* helpers)
- `src/BuyerKiosk/Payroll/Controllers/PayrollAdminController.php` + `routes/api/payroll.php`
- `src/BuyerKiosk/Scheduling/{Services/OvertimeCalculator.php,Repositories/{TimesheetRepository,TimePunchRepository}.php}` (approved-hours source; reads `users.isExempt`)
- `routes/admin/quickbooks.php` + `QuickBooks/Controllers/QBPageController.php` + `templates/themes/default/quickbooks/dashboard.html` + `public_html/js/quickbooks/dashboard.js` + `templates/themes/default/menus/sidebar.html` (admin-page clone target)

**Project commands:** `./test.sh --testsuite unit|integration` · `./test.sh --stan` · `cd userfrosting && ./vendor/bin/phpunit --filter PayRunServiceTest` · `php userfrosting/conductor run` (or the `buyerkiosk-conductor-targeted-migration` skill) · `php userfrosting/bin/payroll/check-token-leaks.php` · `php userfrosting/bin/task worker:start`.

---

## Implementation Phases

| Tier | Phases | Gate before next tier |
|---|---|---|
| **Phase 0 (Prereqs)** | T1–T2 | **T1 (CON-10 live capture) is a HARD GATE** — no submit/onboarding/webhook impl (T7/T8/T11/T12) merges until the real Everee shapes are captured as fixtures. T2 migrations applied to a dev store. |
| **Phase 1b-core (backend)** | T3–T12 | All green on recorded fixtures; golden-fixture gross suite (T10) passes before the web tier wires submit. |
| **Phase 1b-web (admin pages)** | T13–T14 | All 6 pages permission+gate+CSRF+readiness verified. |
| **Final** | T15 | End-to-end sandbox run demonstrated; PRD ACs verified; README "ready for 050c / production-hardening" decision recorded. |

### TIER: PHASE 0 — PREREQUISITES (T1–T2)

- [ ] **T1** CON-10 live Everee sandbox capture `[ref: SDD CON-10; PRD Open Questions]` `[activity: partner-coordination]` — **HARD GATE**
  - **Reduced 2026-06-04:** the pay-run SUBMIT contract is now documented (Everee **Payables API** → `docs/interfaces/everee-payables-api.md`). Residual hard-gate = embedded-onboarding shape + the 5 webhook payload field names + sandbox confirmation of the Payables flow / W-2 regular-cycle behavior. Still gates T7/T8/T11/T12 (the trap that bit Phase 1a twice — skill `external-api-client-self-confirming-fixture-trap`).
  - [ ] T1.1 Capture the real **embedded-onboarding kickoff** request/response against the live sandbox (extends the Phase 1a T15 `createWorker` smoke); record link-delivery + status-callback shape `[activity: partner-coordination]`
  - [ ] T1.2 Capture the real **pay-run submission** endpoint: path, request payload, response (incl. the field BK reads as `evereePayRunId`), and whether it honors an `Idempotency-Key`; confirm `getPayRun(key)` is strongly consistent post-timeout (ADR-5b release-safety) `[activity: partner-coordination]`
  - [ ] T1.3 Capture the 5 real webhook payloads (`payment.paid`, `payment.deposit-returned`, `payment.updated-payment-method`, `payment-payables.status-changed`, `worker.new-tax-forms-available`) — the matching ids (`evereePayableId`/`evereeWorkerId`) `[activity: partner-coordination]`
  - [ ] T1.4 Save captures as fixtures `tests/Fixtures/Payroll/everee-payrun-*.json` + `everee-webhook-payment-*.json` (REAL responses, not authored) `[activity: test]`
  - [x] T1.5 RESOLVED 2026-06-04 — submission is **NOT portal-only**; it is the documented Payables API (`docs/interfaces/everee-payables-api.md`). The manual-submission seam is no longer the expected path. `[activity: partner-coordination]`
  - [ ] T1.6 Validate: the captured shapes are documented in `docs/interfaces/everee-api.md` (pay-run + onboarding + payment-webhook sections) `[activity: doc-write]`

- [x] **T2** Schema migrations (6) — **DONE 2026-06-04** (applied live to `kiosk_buykiosk` + `kiosk_users` + all 7 stores; idempotent re-run = clean skip; INFORMATION_SCHEMA confirms enums APPENDED at END + every column/index/table correct; 27 unrelated pre-existing migration errors noted, not 050b) `[ref: SDD §Data Storage Changes; ADR-1b/8b/11b/12b/13b]` `[activity: migration]`
  - [ ] T2.1 Prime: read SDD §Data Storage Changes (all 6) + the existing `20260522_001/002` create migrations + `20251220_013_002` punches
  - [ ] T2.2 Test: Scenario 22-style idempotent-migration check (apply twice → zero second-pass changes); enum APPEND verified (ordinals unchanged) `[ref: SDD ADR-1b]` `[activity: test]`
  - [ ] T2.3 Implement (central, `alter_table`): `20260603_001` append run/line status enums (`pending_approval`,`partial_error`,`needs_reconciliation`,`returned`); `20260603_002` run-line `typeNum`+`positionId`+`rateType`+`rateCents`; `20260603_003` tenant `environment`+`payPeriodAnchorDate`+`semiMonthlySecondPeriodStartDay`; `20260603_004` (kiosk_users) profile `paymentMethodUpdatedAt`+`taxFormsAvailableAt`; `20260603_005` `payrollRunExclusions` table `[activity: migration]`
  - [ ] T2.4 Implement (PER-STORE): `20260603_006` `scheduleTimePunches.submittedToEvereeRunId` `[activity: migration]`
  - [ ] T2.5 Validate: `conductor run` applies all 6 cleanly on a dev store + idempotent re-run; `db-migration-check` skill scan; existing data preserved `[activity: run-tests]`

### TIER: PHASE 1b-CORE — BACKEND (T3–T12)

- [ ] **T3** `PayrollReadinessGate` (sandbox-only) `[ref: SDD ADR-11b; PRD F5]` `[parallel: true]` — depends T2
  - [ ] T3.1 Prime: SDD ADR-11b + the `EvereeApiClient` base-URL resolution
  - [ ] T3.2 Test: Scenario 23 — `environment='production'` (even with `isActive=1`) BLOCKS; `sandbox`+sandbox base-URL passes `[ref: SDD Scenario 23]` `[activity: test]` `[activity: security-review]`
  - [ ] T3.3 Implement: `assertSandbox(tenant)` asserting `environment='sandbox'` AND base-URL is the sandbox host; throws `PayrollReadinessException` `[activity: code]`
  - [ ] T3.4 Validate: unit green; the gate is a single seam (no scattered env checks) `[activity: review-code]`

- [ ] **T4** `TenantStoreResolver` (BK-native stores + full-access) `[ref: SDD ADR-10b; PRD F2 access rule]` `[parallel: true]` — depends T2
  - [ ] T4.1 Prime: SDD ADR-10b + `SchedulingProviderGate` + `stores.payrollTenantId` + `userStoreAssignments`
  - [ ] T4.2 Test: Scenario 8 — actor lacking access at ANY attached BK-native store → refused (blocking stores named); WIW/Homebase stores filtered out `[ref: SDD Scenario 8]` `[activity: test]`
  - [ ] T4.3 Implement: `bkNativeStoresFor(tenant)` + `assertFullTenantAccess(actor, tenant, permission)` `[activity: code]`
  - [ ] T4.4 Validate: unit green; never reads `wiwEnable` (source-scan test per `buyerkiosk-wiw-exclusion-source-of-truth`) `[activity: security-review]`

- [ ] **T5** `EmploymentClassificationService` (classification ↔ `isExempt` + rate-type guard) `[ref: SDD ADR-3b; PRD F3]` `[parallel: true]` — depends T2
  - [ ] T5.1 Prime: SDD ADR-3b + `OvertimeCalculator::isExemptEmployee` (reads `users.isExempt`) + `userStoreAssignments.employmentClassification` + the existing `PayrollAdminController::setRate`
  - [ ] T5.2 Test: Scenario 14 — set `w2_salaried` ⇒ `users.isExempt=1`; reject a classification conflicting with the worker's classification at a sibling store of the same tenant; **reject `salary_annual` for a `w2_hourly` worker and `hourly` for a `w2_salaried` worker (`classification_mismatch`)** `[ref: SDD Scenario 14; PRD F3]` `[activity: test]`
  - [ ] T5.3 Implement: `setClassification(userId, typeNum, classification)` (tenant-wide consistency; writes both columns) AND `assertRateTypeMatches(userId, typeNum, rateType)` — the guard the rate-set path calls `[activity: code]`
  - [ ] T5.4 Validate: unit green; a consistency test asserts no path leaves the two columns divergent `[activity: review-code]`

- [x] **T6** `PayPeriodCalculator` (anchored period generation) — **DONE 2026-06-04** (`Models/PayPeriod` + `Services/PayPeriodCalculator` + 18 golden tests / 40 assertions green, PHPStan clean; weekly/biweekly-anchored/semi-monthly/monthly + `periodsInPayrollYear` = 52/26/24/12 for salaried division) `[ref: SDD ADR-8b, Example 2; PRD F2]` `[parallel: true]` — depends T2
  - [ ] T6.1 Prime: SDD ADR-8b + Example 2 (GeneratePeriods) + `payrollTenants.payFrequency/payPeriodEndsOnDayOfWeek/payPeriodAnchorDate/semiMonthlySecondPeriodStartDay`
  - [ ] T6.2 Test: Scenario 16 (biweekly anchored) + weekly/semi-monthly/monthly boundary generation + the payroll-year ACTUAL-period-count used by salaried division (52/53, 26/27) `[ref: SDD Scenario 16/17]` `[activity: test]`
  - [ ] T6.3 Implement: `generate(tenant, year|window)` + `periodsInPayrollYear(tenant, year)` (for ADR-13b salaried division) `[activity: code]`
  - [ ] T6.4 Validate: unit green; biweekly without an anchor fails loud `[activity: review-code]`

- [ ] **T7** `EvereeApiClient` Payables methods (`createPayablesBulk` / `createPayablePaymentRequest` / `getPayable` / `deletePayable`) `[ref: SDD ADR-5b, Example 3; docs/interfaces/everee-payables-api.md]` `[activity: code]` — **depends T1** (sandbox confirmation + webhook shapes)
  - [ ] T7.1 Prime: `docs/interfaces/everee-payables-api.md` (the real two-step flow) + SDD ADR-5b/Example 3 + the existing `_request` auth/retry frame
  - [ ] T7.2 Test: happy `createPayablesBulk` (atomic, echoes `externalIds`) → `createPayablePaymentRequest` (→ `PayablePaymentRequestDTO.id`); timeout throws `EvereeUncertainStateException`; `getPayable(externalId)` happy + not-found (the authoritative probe); `deletePayable` unpaid-only `[ref: SDD Scenario 7; everee-payables-api.md]` `[activity: test]`
  - [ ] T7.3 Implement: the 4 Payables methods on `/api/v2/payables*`; deterministic per-payable `externalId` (`bk-payrun-{runId}-line-{lineId}`); build auth inside the send frame (no token in frame args — Phase 1a security lesson) `[activity: code]` `[activity: security-review]`
  - [ ] T7.4 Validate: ZERO network in unit tests (MockHandler); token-leak scanner clean over the new methods `[activity: security-review]`

- [ ] **T8** `EvereeOnboardingService` (kickoff) `[ref: PRD F1; SDD Internal API kickoff contract; ADR-3b]` `[activity: code]` — **depends T1, T3, T5**
  - [ ] T8.1 Prime: PRD F1 + the SDD kickoff precondition (classification+rate required; payType/payRate derived from the set rate) + the existing `EvereeApiClient::createWorker` (embedded) + `UserPayrollProfileRepository.upsertFromWorkerCreated`
  - [ ] T8.2 Test: kickoff with classification+rate present → worker created (embedded), profile linked, idempotent re-kickoff returns existing; missing classification/rate → 422 precondition-unmet; gate + `kickoff_employee_onboarding` + readiness enforced `[ref: PRD F1]` `[activity: test]`
  - [ ] T8.3 Implement: `kickoff(userId, typeNum, typicalWeeklyHours)` — derive `payType` from `rateType`, `payRate` from the resolved `rateCents`; prefill BK-canonical name/DOB/address; audit `[activity: code]`
  - [ ] T8.4 Validate: unit green; NO SSN/bank ever sent from BK (embedded); audit on success + failure `[activity: security-review]`

- [ ] **T9** Pay-run repositories `[ref: SDD ADR-12b/13b/14b; §Data Models]` `[parallel: true]` — depends T2
  - [ ] T9.1 Prime: SDD §Application Data Models + ADR-12b (exclusions) + ADR-13b (line grain) + ADR-14b (serializing-lock create)
  - [ ] T9.2 Test: Scenario 21 — concurrent create yields ONE draft via the serializing lock (advisory `GET_LOCK`/parent-row); cancel→recreate works; exclusion insert/read (Scenario 20); line grain `(userId,typeNum,positionId,rate-segment)` `[ref: SDD Scenario 20/21]` `[activity: test]`
  - [ ] T9.3 Implement: `PayRunRepository` (`createOrGetDraft` with the serializing lock), `PayRunLineRepository` (+typeNum/positionId/rateType/rateCents), `PayRunSnapshotRepository`, `PayRunExclusionRepository` `[activity: code]`
  - [ ] T9.4 Validate: unit green; NO update/delete that violates IRS retention (no hard-delete on runs/lines/snapshots) `[activity: review-code]` `[activity: security-review]`

- [ ] **T10** `PayRunCalculator` (THE MONEY MATH) `[ref: SDD ADR-2b/13b, Example 1/2; PRD F2/F3]` `[activity: code]` — **depends T4, T6, T9**
  - The single highest-stakes phase. Golden fixtures are the correctness gate.
  - [ ] T10.1 Prime: SDD Example 1 (integer-only gross + salaried remainder) + Example 2 (authoritative-totals allocation, weekly-OT, blockers) + `TimesheetRepository`/`TimePunchRepository`/`PayRateService.getRate`
  - [ ] T10.2 Test (GOLDEN FIXTURES, hand-verified cents): Scenario 1 (hourly+OT), Scenario 4 (salaried-exempt payroll-year proration), Scenario 17 (27th-period exactness), Scenario 18 (semi-monthly straddle: regular by punch-day, OT to weekEndDate period), Scenario 19 (multi-position/mid-period rate split + `multi_rate_overtime` block), `position_ambiguous` + `timesheet_reconciliation` blockers, integer-only (no float) assertion, **empty-period (no approved hours → empty preview)** `[ref: SDD Scenarios 1/4/17/18/19 + §Test Coverage edge cases]` `[activity: test]`
  - [ ] T10.3 Implement: `buildPreview(tenant, period, exclusions)` — authoritative approved-timesheet totals; punches allocate by largest-remainder ten-thousandths; rate per (store,position,workDate); blockers; integer-only gross `[activity: code]`
  - [ ] T10.4 Validate: golden suite green; run total reconciles EXACTLY to approved totals (no drift); `grep` no float in the gross path `[activity: review-code]`

- [ ] **T11** `PayRunService` (lifecycle/state machine/submit/recovery) `[ref: SDD ADR-1b/4b/5b/6b/14b, Complex Logic; PRD F2/F5]` `[activity: code]` — **depends T1, T2, T3, T4, T7, T9, T10**
  - [ ] T11.1 Prime: SDD Complex Logic (submit algorithm + RECOVERY) + ADR-4b (approval) + ADR-6b (lock-before-submit + pre-call marker) + ADR-5b (uncertain release)
  - [ ] T11.2 Test: Scenario 5 (approval state machine), 6 (above-threshold gate), 7 (idempotent submit/uncertain), 9 (stale preview recomputed), 11 (**per-payable-status partial failure → `partial_error`: a payable going ERROR/UNPAYABLE_WORKER drives its line to `failed` + run roll-up; NOT a sync bulk-create partial**), 15 (cancel semantics), 20 (**exclusion end-to-end: regenerated preview omits the worker, submit payload omits them, excluded punches NOT locked**), 22 (cross-DB recovery: lock-before-submit, pre-call `needs_reconciliation` marker, proof-gated release), 2/3 (not-ready blocks + audited exclude); **edge cases: all-excluded run, backdated-rate-after-submit immutability (submitted snapshot unchanged)** `[ref: SDD Scenarios 2/3/5/6/7/9/11/15/20/22 + §Test Coverage edge cases]` `[activity: test]` `[activity: security-review]`
  - [ ] T11.3 Implement: `createDraft`, `preview`, `excludeWorker`, `submit` (snapshot → lock → `needs_reconciliation` marker → Everee → status → recovery; a synchronous submit-response partial rejection → `partial_error`), `approve`, `cancel`; run-aggregate status roll-up; all behind readiness+gate+full-tenant-access; audit every transition `[activity: code]`
  - [ ] T11.4 Validate: unit+integration green; NO double-pay under simulated timeout; submitted run immutable except via webhooks; `grep catch(\Exception` returns zero in new code `[activity: security-review]`
  - [ ] T11.5 Doc: author `docs/patterns/payroll-pay-run-lifecycle.md` (the SDD-named deliverable) — the state machine + lock-before-submit/recovery + weekly-OT allocation `[activity: doc-write]`

- [ ] **T12** Webhook handlers (5 deferred events) `[ref: SDD ADR-9b; PRD F4]` `[activity: code]` — **depends T1, T9**
  - [ ] T12.1 Prime: SDD ADR-9b (3-case matching) + the existing `ProcessEvereeWebhookJob` switch + `UserPayrollProfileRepository` apply* shape + the T1 payment fixtures
  - [ ] T12.2 Test: Scenario 10 (payment.paid/deposit-returned drive line status), 11 (partial rejection → `partial_error`), 12 (unmatched → audited, no raise), `payment.updated-payment-method` + `worker.new-tax-forms-available` profile mirror; zero-row-already-applied is an idempotent no-op (NOT markFailed); isolation (one event's failure doesn't block others) `[ref: SDD Scenarios 10/11/12]` `[activity: test]` `[activity: security-review]`
  - [ ] T12.3 Implement: remove the 5 from `DEFERRED_PHASE_1B_EVENTS`; add handlers matching line by `evereePayableId`/`evereeWorkerId` (3-case); `PayRunLineRepository.applyPaymentStatus` + profile mirror helpers returning rowCount; run roll-up `[activity: code]`
  - [ ] T12.4 Validate: unit green; redaction + idempotency + claim/dedupe preserved; the job stays registered (independent count + wiring check — the Phase 1a T10 lesson) `[activity: security-review]`

### TIER: PHASE 1b-WEB — ADMIN PAGES (T13–T14)

- [ ] **T13** API endpoints (extend controller + routes) `[ref: SDD §Internal API Changes; ADR-10b]` `[activity: code]` — **depends T8, T11, T12**
  - [ ] T13.1 Prime: SDD §Internal API Changes (all endpoints) + the existing `PayrollAdminController` (verifyCsrf + strict-parse + gate pattern) + `routes/api/payroll.php`
  - [ ] T13.2 Test: each endpoint's auth chain (permission + store-group + CSRF + gate + readiness for mutations; read endpoints drop CSRF+gate); IDOR — tenant derived from `typeNum`, never request body (the Phase 1a T12 lesson); **the rate-set endpoint rejects a classification/rate-type mismatch (422) and audits `payroll.classification.mismatch_rejected`**; 403/422/409 shapes `[activity: test]` `[activity: security-review]`
  - [ ] T13.3 Implement: roster, onboarding/kickoff, classification, pay-periods, runs (create/list/detail-preview/exclude/submit/approve/cancel) on `PayrollAdminController`; **wire `EmploymentClassificationService::assertRateTypeMatches` into the rate-set path (audit `payroll.classification.mismatch_rejected` on rejection)**; register routes BEFORE any `/:id` shadowing route (Slim 2) `[activity: code]`
  - [ ] T13.4 Validate: route/middleware tests green; `$app->halt($status, json_encode($x))` always passes a body (the 050 halt() gotcha) `[activity: review-code]`

- [ ] **T14** Web pages (6) + nav `[ref: PRD F6–F11; SDD ADR-7b]` `[activity: code]` — **depends T13**
  - [ ] T14.1 Prime: SDD ADR-7b + the QuickBooks admin clone target (route/controller/Twig/JS/CSS) + `sidebar.html` nav pattern + the modal/EJ2 skills
  - [ ] T14.2 Test: each page renders permission-gated + `typeNum`-scoped; the runs EJ2 grid loads via the data endpoint; CSRF token refresh on the long-lived run-detail page (csrf-token-refresh skill) `[activity: test]`
  - [ ] T14.3 Implement: `routes/admin/payroll.php` (6 GET routes) + `PayrollPageController` (6 display*); `templates/themes/default/payroll/{index,setup,employees,rates,runs,run-detail}.html`; `public_html/js/payroll/*.js` (IIFE) + `css/admin/modules/payroll-*.css`; Payroll nav in `sidebar.html` (gated `view_pay_run`/`manage_payroll`) `[activity: code]`
  - [ ] T14.4 Apply the MANDATORY skills: `bootstrap5-modal-backdrop-stacking` on every modal; `syncfusion-grid-hidden-tab-raf-defer` on the runs grid `[activity: code]`
  - [ ] T14.5 Validate: pages render on a dev store; modals clickable; grid populates; no console errors `[activity: review-code]`

### TIER: FINAL (T15)

- [ ] **T15** Integration + E2E + sign-off `[ref: SDD §Test Specifications; PRD §Success Metrics]` `[activity: run-tests]`
  - [ ] T15.1 End-to-end sandbox run (the slice deliverable): provision → onboard ≥1 worker to "ready to pay" → set rate → create run → dry-run preview → submit → observe a `payment.paid` webhook update the line `[ref: PRD slice-exit metric]` `[activity: business-acceptance]`
  - [ ] T15.2 Full golden-fixture gross suite green (hourly/OT/salaried-exempt/straddle/multi-position) `[activity: run-tests]`
  - [ ] T15.3 Security sweep: every mutating route gated+CSRF+readiness; token-leak scanner clean; `grep -rn 'catch (\\Exception' src/BuyerKiosk/Payroll/` returns zero; no hard-delete on retained tables `[activity: security-review]`
  - [ ] T15.4 `./test.sh --stan` clean on `src/BuyerKiosk/Payroll/`; independent test-count verification (the Phase 1a misreporting lesson — run the suite yourself, verify the count rose, grep production wiring) `[activity: run-tests]`
  - [ ] T15.5 README "Decision: ready for `050c` mobile slice / production-hardening" recorded; Spec Compliance Audit filled `[activity: doc-write]`

---

## Spec Compliance Audit (PRD feature → phase)

| PRD | Feature | Phase(s) |
|---|---|---|
| F1 | Onboarding kickoff service | T8 |
| F2 | Pay-run lifecycle | T6, T9, T10, T11 |
| F3 | Salaried/exempt model | T5, T10 |
| F4 | Complete 11 webhook handlers | T12 |
| F5 | Pre-launch readiness gate | T3, T11 |
| F6 | Payroll landing + nav | T14 |
| F7 | Provisioning wizard page | T13, T14 |
| F8 | Roster + onboarding page | T13, T14 |
| F9 | Rate management page | T13, T14 |
| F10 | Pay-run list page | T13, T14 |
| F11 | Run detail + preview + submit page | T13, T14 |
| F12 | Webhook listing (Should — fast-follow) | not in this plan (SDD-excluded) |
| — | CON-10 partner capture (hard gate) | **T1** |
| — | 6 migrations | T2 |

**Dependency summary:** T1 gates T7/T8/T11/T12. T2 gates all of core (explicit on T3/T5/T6/T9/T11). T3/T4/T5/T6/T9 are parallel (after T2). T8 ← T1,T3,T5. T10 ← T4,T6,T9. T11 ← T1,T2,T3,T4,T7,T9,T10. T12 ← T1,T9. T13 ← T8,T11,T12. T14 ← T13. T15 ← all.
