# Spec 050 — Everee Payroll Foundations

**Scope:** Phase 0 (Prerequisites) + Phase 1a (Foundations) of the Everee white-label payroll integration.
**Source analysis:** [`docs/everee-payroll-integration-analysis.md`](../../everee-payroll-integration-analysis.md) — 32 architectural decisions locked 2026-05-18.

## Status

**Phase 1a foundations: services built, codex-reviewed, unit/integration-verified, schema live on dev — pending the human sign-offs + live-sandbox smoke (T15) before the Phase 1b decision.**

### ⚠️ Honest caveats for the sign-off reviewer (from the T17 capstone codex review)

The final sign-off codex pass found **no CRITICAL runtime security defect** — the security-critical controls (F4 token encryption/fail-closed, F6 provisioning, F7 append-only rate history, F8 HMAC/idempotency/redaction, F11 scheduling gate) all hold, and the pieces compose (DI factories, job registered in TaskCommandFactory + task_job_definitions, GuardedPdo on the rate-write path). Two honesty corrections to weigh before signing:

1. **CON-22 is a GENUINE open gate, NOT a formality.** The hard gate (no Phase-1a code mapping `users.id → evereeWorkerId` until the person-centric report is signed) was technically crossed in-branch: `ProcessEvereeWebhookJob` + `UserPayrollProfileRepository` already perform that mapping while `person-centric-account-verification.md` §"Signed-off-by" is still blank. The remediation code (assign-existing-user, CON-22) is real and correct, and the verified gap is documented — but the second-engineer sign-off should be made with full knowledge that the mapping code already exists. (The Scenario-25 guard test only scans `Payroll/Services`, so it does not catch the Jobs/Repositories mapping.)

2. **F8 async processing has an operational dependency.** A verified webhook is always durably persisted, but it is only *enqueued* when Redis/TaskEngine is reachable at dispatch time; if dispatch fails the route falls back to a no-op dispatcher and still returns 200. Re-driving those persisted-but-unqueued events depends on `bin/payroll/reprocess-webhooks.php`, which is a CLI **ops must schedule (cron)** — it is not auto-registered as a scheduled job in this branch. Install that cron before production, or the once-only-once guarantee degrades to "persisted, pending manual recovery" during a Redis outage.

Minor (LOW): the committed MySQL rate-trigger integration test asserts only "trigger exists" (it uses a 0-row WHERE), but the trigger was MANUALLY verified to block a real-row UPDATE and DELETE (rolled-back txn) during T8. The token-leak scanner PASS covers its default scope (`src/BuyerKiosk/Payroll`); do not claim `bin/` coverage. Two pattern docs live under `userfrosting/docs/` instead of repo-root `docs/` (cosmetic; both gitignored).

| Document | Status | Last updated |
|---|---|---|
| [product-requirements.md](./product-requirements.md) | Ready for SDD (Codex-reviewed) | 2026-05-22 |
| [solution-design.md](./solution-design.md) | Ready for Implementation Plan (Codex-reviewed, 11 ADRs confirmed) | 2026-05-23 |
| [implementation-plan.md](./implementation-plan.md) | Phase 1a foundations complete — T17 compliance audit filled (2026-06-02). Spec compliance: 57 ✅ / 4 ⚠️ / 7 ☐. Open: T3.4.1 second-engineer sign-off, T4.4.1 audit sign-off, F13 CS items, T15 sandbox smoke (partner-gated). | 2026-06-02 |
| [phase-0-handoff.md](./phase-0-handoff.md) | Phase 0 implementation handoff: live dev schema state + schema-drift gotcha | 2026-06-01 |

### T17 Final Test Totals (run 2026-06-02 on branch `050-everee-payroll-foundations`)

| Suite | Tests | Assertions | Result |
|---|---|---|---|
| Unit (`tests/Unit/Payroll`) | 709 | 3549 | OK |
| Integration (`tests/Integration/Payroll`) | 12 | 14 | OK |
| **Total Payroll** | **721** | **3563** | **OK** |

PHPStan: `[OK] No errors` on `src/BuyerKiosk/Payroll/`
SEC-6 no-DELETE scan: 0 hits
Token-leak scanner: `[PASS]`
catch(Exception) scan: 0 hits (all catches are `\Throwable`, specific typed exceptions, or `\Slim\Exception\Stop`)

### Key findings caught during implementation reviews

The following issues were caught by codex and independent review passes — not found during initial authoring — and are recorded here as evidence that the review process added real value:

1. **T1 (f429d715b):** `Core\Employee` and `Employee\Employee` had incompatible constructors; a naive alias flip would have caused silent `TypeError` on every `Buy.php` route. Fixed by adding `fromLegacyEmployeeId()` helper + rewriting Buy.php callsites (Deviation Log 2026-05-26).

2. **T2 (03f06baa9):** Two SDD schema names did not match the live database: `kiosk_buykiosk.positions` (does not exist; real table is per-store `schedulePositions`) and `display_name` column on `users` (real column is `displayName`; structured `firstName`/`lastName` already existed). Caught at live apply; corrected before branch advanced (Deviation Log 2026-05-29).

3. **T6 (c6d3d1f16):** ADR-1's "reuse `Security\Encryption`" produced unauthenticated CBC — a tampered ciphertext decrypts silently (CBC malleability). For a payroll/financial token vault this is unacceptable. Added encrypt-then-MAC envelope (`v1:<ciphertext>:<HMAC-SHA256>`); MAC key domain-separated from master key; `hash_equals` verify before any decrypt (Deviation Log 2026-06-01).

4. **T6 (e199885cf):** `check-token-leaks.php` originally used `var_export` to print code, which itself leaked the fixture token into scanner output. Fixed to use `json_encode` (commit e199885cf — "clear token-leak scanner").

5. **T7 (b546320e6):** SDD Implementation Example 2 threw `EvereeRateLimitException` immediately when 429 lacked a `Retry-After` header. PRD F5 says fall back to exponential backoff. The two specs contradicted; PRD prevailed. Added `testScenario10bRateLimitWithoutRetryAfterFallsBackToExponentialBackoff` (Deviation Log 2026-06-01b). Also: decrypted token must never be a frame argument — PHP stack traces with `zend.exception_ignore_args=Off` would log `Authorization: Basic <token>`; client was restructured to build auth headers inside the request-sending frame.

6. **T9 (567d1d0d3):** `isActive` only goes to 1 after a real `listWorkers` round-trip confirms reachability. **SUPERSEDED by F6 redefinition (T15, owner-approved):** the original "bank not yet verified → isActive=0" secondary check was removed — the real Everee worker resource has no `bankVerified`/`status` key, and gating COMPANY activation on per-WORKER bank/TIN status was the wrong model (workers verify during onboarding, after the company is live). Readiness is now "non-throwing `listWorkers` 200 = reachable". This supersedes PRD T9.2.4.

7. **T10 (aea35c21e):** First T10 build agent (R1) fabricated test names in its report; second (R2) left `ProcessEvereeWebhookJob` unregistered in the job-definition table. Both caught by codex review + independent test-count verification. Registration fixed via migration 018.

8. **T12 (a37785a9d):** CRITICAL IDOR — controller was reading `payrollTenantId` from the request body (attacker-supplied), allowing a user at store A to operate on store B's tenant. Fixed: tenant always derived from the authenticated `typeNum` route parameter via `stores.payrollTenantId`. Also caught: production-fatal `EvereeTokenStorage` had a private constructor making DI impossible; missing CSRF protection on state-mutating POST endpoints; exception-message leak in validation errors.

## Review Log

### SDD — Codex review (2026-05-23)

**Codex blockers raised (all resolved in the SDD):**

1. **Webhook payload could store forbidden PII fields verbatim** (CON-11 violation risk). The raw event payload was stored byte-for-byte without redaction. Fix: added `EvereeWebhookPayloadRedactor` service that strips SSN-/bank-/W-4-/I-9-shaped keys after HMAC verification but BEFORE DB INSERT, replacing them with the sentinel `'<REDACTED-PII>'`. High-severity application log entry fires on every redaction so Everee-side drift surfaces. Captured as ADR-11.
2. **Company-instance provisioning was logically impossible as drawn.** `EvereeApiClient::_request` required a `PayrollTenant` object to build auth, but `createCompanyInstance` runs before any tenant exists. Fix: added partner-level auth (`EVEREE_PARTNER_API_TOKEN` env var) for the single Company Instance create call; added `EvereeProvisioningService::provisionManually(...)` for the portal-based seam when partner has no self-serve API. Captured as ADR-11.
3. **`payrollRunLines` / `payrollRunSnapshots` `ON DELETE CASCADE` from `payrollRuns` violated IRS 4-year retention (CON-10).** Fix: switched both to `ON DELETE RESTRICT`.

**Important issues addressed:**

- HMAC rotation acceptance required a per-tenant prior-secret column with expiry. Added `payrollTenants.webhookPriorSecretEncrypted` + `payrollTenants.webhookPriorSecretExpiresAt`; verifier accepts either current or prior while the expiry is in the future. Captured as ADR-11.
- `GuardedPdo` was ambiguous between production and test scope. Clarified that the DI container wires it in BOTH environments; the integration test asserts behavior on the production-wired guard.
- Webhook failure state was contradictory (`processedAt=NULL` in error matrix vs `markProcessed(id, err)` elsewhere). Split into `markProcessed(id)` (success) and `markFailed(id, error)` (failure leaves `processedAt=NULL`).
- Person-centric verification SQL was counting rows, not distinct typeNum. Fix: `COUNT(DISTINCT usa.typeNum) >= 2` per PRD F2.
- PRD F4's CI check for direct token logging had no test scenario. Added Scenario 13b.
- PRD F12's TaskEngine audit had no test-gating. Added Scenario 26b.
- PRD F5 fixture sign-off scenarios were incomplete. Added Scenarios 11b (GET worker), 11c (POST worker with idempotency key), 11d (4xx validation passthrough).
- `userPayrollProfiles` DB-placement contradiction (CON-2 said central, table said kiosk_users). Fix: settled on `kiosk_users` and updated CON-2 + the database-routing summary.
- Pre-flight punchType check was ordered AFTER the store-DB migration despite the "pre-flight" label. Renamed to "schema-verification" and clarified ordering is non-critical.
- Tombstone INSERT lacked `rateType` (NOT NULL in schema). Fix: `retireRate` now reads the current-active rate and copies its `rateType` onto the tombstone.
- `findByEvereeEventId` / `findById` used in flows but absent from `PayrollWebhookEventRepository` surface. Fix: added both to the repository contract.

**Enhancements addressed:**

- ADR-section intro updated from "Currently all are _Pending_" to "All confirmed 2026-05-23"; ADR-11 added and confirmed.
- "Three NEW environment variables" corrected to "Six NEW".
- Validation checklist counts updated (10 exception types instead of 9; Scenarios 1–27 + sub-scenarios instead of 1–24).
- `listHistory` return shape clarified to `map<positionId, reverse-chronological-array>` so the implicit-effectiveUntil derivation is safe within each position group.

### SDD — In-conversation reviewer pass (2026-05-23)

Prior to the Codex pass, a `feature-dev:code-reviewer` agent identified 4 blockers + 5 important issues — all resolved (PRD F2/F7/F13 deliverables added, Scenario 4 audit-count corrected, `isActiveAt` boundary clarified, `listHistory` per-position grouping documented, `EvereeEncryptionRequiredException` added to directory map, `PayRateImmutableException` audit-entry claim corrected, `ptoAccrualBalances` missing FK added).

## ADR Confirmation Log

All 11 ADRs in `solution-design.md` were confirmed by the user on 2026-05-23:

| ADR | Decision | Confirmed |
|---|---|---|
| ADR-1 | Reuse `Security\Encryption` with separate `EVEREE_ENCRYPTION_KEY` | 2026-05-23 |
| ADR-2 | Append-only via `GuardedPdo` (production + test) + regression test; DB-level follow-up | 2026-05-23 |
| ADR-3 | Per-tenant webhook secret default; global mode via env-switched config | 2026-05-23 |
| ADR-4 | TaskEngine `default` queue initially; Feature 12 audit may move to dedicated queue | 2026-05-23 |
| ADR-5 | HMAC-SHA256 with multi-candidate-secret rotation tolerance | 2026-05-23 |
| ADR-6 | Plain HTTP client + custom retry/backoff (no circuit-breaker library) | 2026-05-23 |
| ADR-7 | Idempotency-Key on POST; best-effort with documented residual risk | 2026-05-23 |
| ADR-8 | Single namespace `BuyerKiosk\Payroll\` | 2026-05-23 |
| ADR-9 | 12 permission keys in one `uf_authorize_group` migration JSON | 2026-05-23 |
| ADR-10 | PII source-of-truth labels in migration descriptions | 2026-05-23 |
| ADR-11 | EvereeWebhookPayloadRedactor + partner-level provisioning auth + per-tenant prior-secret rotation columns (added post Codex review) | 2026-05-23 |

## Ready for Implementation Plan?

- [x] All Codex blockers resolved
- [x] Design covers all PRD requirements (F1-F13 with named deliverables and test scenarios for each)
- [x] Architecture is sound and justified (layered modular monolith mirroring `BuyerKiosk\QuickBooks\` shape)
- [x] Interfaces are clearly defined (5 HTTP endpoints + 7 services + 5 repos + 1 job, all method signatures specified)
- [x] Security and error handling addressed (token encryption, append-only, HMAC + rotation, PII redaction, 23-row error matrix)
- [x] All 11 ADRs confirmed by user
- [x] README updated with review notes

**Decision: Ready to proceed to Implementation Plan (PLAN).** Open items (Everee partner-confirmation responses on §13 stack) are tracked but do not block PLAN authoring — they will refine implementation details, not architectural decisions.

---

### PLAN — Codex review (2026-05-26)

**Codex blockers raised (all resolved in the PLAN):**

1. **`UserPayrollProfile` model + repo + worker.* event handlers missing from T10.** Added T10.3.6 (model), T10.3.7 (repository with per-event-type write helpers), T10.3.12 (ProcessEvereeWebhookJobTest).
2. **T9 dependency on T13 missing.** Provisioning calls `SchedulingProviderGate::assertAllowed` before any typeNum attach. Fixed in T9 header + dependency summary.
3. **T10 dependency on T9 missing.** Webhook handler resolves tenant by evereeCompanyId via `PayrollTenantRepository`. Fixed.
4. **T7 dependency on T11 missing.** `EvereeApiClient::_request` audits every call via `PayrollAuditService`. Fixed.
5. **PRD F8 all-11-events ingestion test missing.** Added T10.2.9 (fixture matrix across all 11 event types + unknown-type) and T10.2.10 (one HMAC-signed fixture file per event type).
6. **PRD F6 partial-provisioning + multi-store attach tests missing.** Added T9.2.4 (partial-provisioning isActive=0 preservation), T9.2.5 (multi-store attach), T9.2.6 (`provisionManually` portal path).
7. **T17 dependency summary excluded T16.** T16 owns env-vars + CLAUDE.md cheat-sheet which must be in place before final acceptance. Fixed in dependency summary + T17 is now operationally marked.

**Important issues addressed:**

- Spec Compliance Audit expanded from feature-level (13 rows) to AC-level (50+ rows), each row owning a phase + test/evidence + owner + status. Now spec-traceable per the T17.7 requirement.
- `EVEREE_PARTNER_API_TOKEN` documentation added in T9 (T9.4.3) AND T16 (T16.1) explicit enumeration of all 6 env vars.
- T15 explicitly marked as OPERATIONAL phase (no TDD shape; the scripted smoke test IS the test); T15.2 expanded with concrete steps (provision tenant + create worker + verify webhook arrives + dedupe + setRate as-of).
- T17 explicitly marked as OPERATIONAL phase (no TDD shape; pure verification).
- T16 reshaped with explicit Validate sub-tasks (T16.4.1, T16.4.2).
- Concrete SDD line refs added to T7.1.2/T7.1.3, T8.1.1-T8.1.3, T10.1.1-T10.1.2, T12.1.1, T14.1.1.
- Catch-block constraint scan added: T10.4.4 (per-phase) + T17.6.5 (whole module). `grep -rn 'catch (\\Exception' userfrosting/src/BuyerKiosk/Payroll/` must return zero.
- T17 DoD significantly expanded: SEC-6 no-DELETE scan, REL-2 dedupe evidence, REL-6 token-decryption evidence, T17.9 documentation file existence checks, T17.10.2 migration_log audit, T17.13 SDD test-suite items.
- T17.5 changed from "production once-and-only-once" to "fixture/sandbox runs, production monitoring tracked post-merge" per SDD §Quality Requirements.

**Enhancements addressed:**

- T14 tagged `[parallel: true]` (can run alongside T9/T10/T12 follow-up after T8 lands).
- Explicit ownership added for `ProcessEvereeWebhookJobTest.php` (T10.3.12) and webhook fixture files (T10.2.10).

### PLAN-final-state
- Total phases: 17 (T1-T17)
- Total sub-tasks: ~140
- Spec Compliance Audit rows: 50+ (one per PRD AC, plus 3 ADR-11 verifications)
- File: 600 lines
- `NEEDS CLARIFICATION` markers: 0

## Ready for Implementation?

- [x] All blockers resolved (Codex 2026-05-26 pass)
- [x] Plan covers all SDD components (every SDD directory-map entry owned by a T-phase)
- [x] Tasks are actionable and well-sequenced
- [x] Test tasks included in each phase (Prime → Test → Implement → Validate)
- [x] Dependencies correctly mapped (T1→T2→T3→T4 sequential, T6/T11/T13 parallel after Phase 0, T7→T8→T9→T10→T12 critical path, T14 parallel after T8, T16 parallel-anywhere-but-gates-T17)
- [x] README updated with review notes

**Decision: Ready to begin Implementation (Phase 0 — T1 first).**

---

### PRD — Codex review (2026-05-22)

**Codex blockers raised (all resolved in the PRD):**

1. **Feature 7 rate history was internally inconsistent.** The "INSERT-only" claim contradicted a flow step that wrote `effectiveUntil` on the prior row, and "one day before" against an exclusive `effectiveUntil` created a boundary bug.
   - **Resolution:** Rewrote Feature 7 as a **pure append-only** model. `effectiveUntil` is only set on explicit retirement tombstones (also via INSERT). Supersession by a new rate leaves prior rows untouched; intervals are derived at read time from the next row's `effectiveFrom`. Tie-break on concurrent writes is by greatest `(effectiveFrom, id)`.

2. **Store-to-tenant schema was missing.** Feature 6 stated multiple BK stores share one `payrollTenants` row, but Feature 3's schema list never named the FK.
   - **Resolution:** Feature 3 now requires nullable `stores.payrollTenantId` FK with documented N-stores-to-1-tenant relationship and audit-logged reassignment rule.

3. **Worker PII source-of-truth was ambiguous.** Analysis said Everee owns legal name / DOB / home address canonically; PRD added them to BK `users` without saying which fields are canonical where.
   - **Resolution:** Added explicit **PII source-of-truth model** to Feature 3 with three categories: BK canonical, onboarding-kickoff prefill (write-once, then Everee canonical), and never-enters-BK. Each new column is labeled.

4. **Salary shadow store.** `users.annualSalaryCents` (added by the prior review pass) duplicated `payRateHistory` rateType=`salary_annual`, violating the "no shadow rate storage" KPI.
   - **Resolution:** Removed `annualSalaryCents` from `users`. `payRateHistory` is the canonical source for all compensation (hourly and salaried). Feature 7 ACs explicitly state this.

5. **Webhook HMAC secret storage was assumed but not required.** Feature 8 demanded HMAC verification; Feature 3 didn't store the secret.
   - **Resolution:** Feature 3 schema adds `payrollTenants.webhookSecretEncrypted`. Feature 8 ACs now include timestamp tolerance, replay defense, secret-rotation procedure documentation, and per-tenant-vs-global-secret config switch (partner answer pending).

**Important issues addressed:**

- Added 429 / `Retry-After` handling AC to Feature 5; split 401/403 (auth) from 4xx (validation) exception types.
- Fixed broken Feature 8 cross-reference (was "Could-Have Feature 13" → corrected to Feature 14 after the earlier renumber).
- Reworded the open-question on sandbox credentials to clarify it gates only the **live smoke test**, not Phase 1a sign-off (which runs on recorded fixtures).
- Added "Deliberate deviations from the source analysis" subsection to **Won't-Have**: embedded `ONBOARDING` component (analysis put in Phase 1a; this PRD defers to Phase 1b) and COA mapping UI design (analysis put in Phase 0; this PRD ships table only and defers seam to Phase 1c). Records the deviation so the SDD does not re-litigate.

**Enhancements addressed:**

- Hedged the "single largest reason existing customers churn" claim in the Problem Statement; quantitative evidence is now an owed-back item via Feature 13's pilot-candidate audit.
- Added explicit **Phase 1a exit metrics** (hard, measurable inside the phase) separate from forward-looking KPIs (measured in Phase 1b+).
- Added a **Cross-Feature Edge Cases** subsection in Detailed Feature Specifications covering Feature 4 (decryption failure, master-key rotation), Feature 5 (401, 429-Retry-After overshoot, mid-request timeout with/without idempotency key), Feature 6 (duplicate EIN, partial portal provisioning), Feature 8 (concurrent duplicates, signature during rotation, replay), Feature 11 (mixed-provider stores under one EIN).

### Prior validation cycle (in-conversation quality review, 2026-05-22)

Surfaced before the Codex pass. Resolutions captured in the prior diff:
- Added missing Phase 0 deliverables: pre-flight `punchType` enum check, `display_name` → legal-name migration helper, pilot-store rate-data audit (now Feature 13), partner-manager email kickoff (now Feature 13).
- Promoted scheduling-provider gate (Feature 11) and background-job audit (Feature 12) from Should → Must.
- Strengthened weak ACs on Feature 2 (verification report needs concrete SQL evidence), Feature 4 (token leak regression test), Feature 5 (split unit-test gate vs sandbox smoke test).

## Decisions ledger

Decisions made during the PRD authoring/review that are not in the source analysis doc:

| Decision | Date | Rationale |
|---|---|---|
| Single combined PRD for Phase 0 + Phase 1a | 2026-05-22 | Phase 0 has no merchant-visible scope; spec directory already named "foundations"; analysis §15 "one PRD per phase" guidance overridden by user instruction |
| PRD audience: engineering-led | 2026-05-22 | Phase 0/1a ship no merchant UI; primary personas are BK engineer + CS pilot operator |
| `annualSalaryCents` field lives in `payRateHistory`, not on `users` | 2026-05-22 (Codex review) | Avoids shadow rate storage; one source of truth for compensation |
| Pure append-only `payRateHistory` | 2026-05-22 (Codex review) | INSERT-only invariant + boundary-bug-free; `effectiveUntil` reserved for explicit retirement tombstones |
| Per-tenant `webhookSecretEncrypted` column added defensively | 2026-05-22 (Codex review) | Cheap insurance against partner answering "per-tenant secrets" on §13 item 16 |
| Embedded ONBOARDING component deferred from Phase 1a → Phase 1b | 2026-05-22 | Engineering-only scope for Phase 1a; embedded UI needs the Live/Team app shells from 1b |
| COA mapping seam deferred from Phase 0 → Phase 1c | 2026-05-22 | Table ships in Phase 0; design + seam land with the QBO JE generator that consumes them |

## Ready for SDD?

- [x] All Codex blockers resolved
- [x] User stories are clear and testable
- [x] Acceptance criteria are unambiguous (Features 2/4/5/7 ACs strengthened across two review passes)
- [x] Scope is well-defined; deviations from source analysis documented in Won't-Have
- [x] PII source-of-truth model defined
- [x] Rate-history storage invariant unambiguous (pure append-only)
- [x] Store-to-tenant relationship defined
- [x] Webhook secret storage defined
- [x] Phase 1a exit metrics defined and measurable in-phase

**Decision: Ready to proceed to Solution Design (SDD).** Open items (Everee partner-confirmation responses, pilot-candidate evidence) are tracked but do not block SDD authoring — they will refine implementation details, not architectural decisions.
