# Implementation Plan

**Spec:** 050-everee-payroll-foundations
**Scope:** Phase 0 (Prerequisites) + Phase 1a (Foundations) of the Everee white-label payroll integration
**Authored:** 2026-05-23
**Companion docs:**
- [product-requirements.md](./product-requirements.md) — PRD (Codex-reviewed, 2026-05-22)
- [solution-design.md](./solution-design.md) — SDD (Codex-reviewed, 11 ADRs confirmed, 2026-05-23)
- [README.md](./README.md) — Status + review log

## Validation Checklist

- [x] All specification file paths are correct and exist
- [x] Context priming section is complete
- [x] All implementation phases are defined (T1–T17)
- [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
- [x] Every test references PRD acceptance criteria
- [x] Integration & E2E tests defined in final phase (T17)
- [x] Project commands match actual project setup
- [x] A developer could follow this plan independently

---

## Specification Compliance Guidelines

### How to Ensure Specification Adherence

1. **Before Each Phase**: Read the SDD/PRD line refs listed in the phase's `Prime Context` sub-tasks. NO phase begins without that read-through.
2. **During Implementation**: Cite the specific SDD section (e.g., `SDD §"Building Block View"`) in commit messages and PR descriptions.
3. **After Each Task**: Run the `Validate` sub-tasks. Quality gates listed there are MANDATORY — no exceptions.
4. **Phase Completion**: Tick every checkbox; missing ticks block dependent phases.

### Deviation Protocol

If implementation cannot follow specification exactly:
1. Open a PR comment AND a note in this file under a new `## Deviation Log` section explaining the why.
2. Get explicit user approval (the PRD author / spec owner) before merging the deviation.
3. If the deviation is an improvement, update the SDD in the same PR.
4. **Never deviate without documentation.** A deviation found in code review without a Deviation Log entry blocks the merge.

## Metadata Reference

- `[parallel: true]` — Phase / sub-phase can run concurrently with siblings that have the same flag
- `[component: name]` — Reserved for multi-component plans; not used here (single PHP monolith)
- `[ref: doc/section; lines: N]` — Link to spec/pattern/interface doc + line(s)
- `[activity: type]` — Activity hint for specialist agent selection (`code`, `migration`, `test`, `lint-code`, `format-code`, `review-code`, `run-tests`, `business-acceptance`, `security-review`, `doc-write`, `cli-tool`, `partner-coordination`)

---

## Context Priming

*GATE: Every implementer MUST read these files end-to-end BEFORE picking up any phase. The PLAN does NOT repeat content already in PRD/SDD — it points at it.*

**Specifications:**
- `docs/specs/050-everee-payroll-foundations/product-requirements.md` — PRD (443 lines; the WHAT)
- `docs/specs/050-everee-payroll-foundations/solution-design.md` — SDD (3500+ lines; the HOW)
- `docs/specs/050-everee-payroll-foundations/README.md` — Status + review log
- `docs/everee-payroll-integration-analysis.md` — Source analysis (32 locked decisions, 2026-05-18)

**Key Design Decisions (extracted from SDD §"Architecture Decisions"):**
- **ADR-1** — Reuse `BuyerKiosk\Security\Encryption` (OpenSSL AES-256-CBC) with separate `EVEREE_ENCRYPTION_KEY` env var (mirror QBO pattern).
- **ADR-2** — `payRateHistory` is append-only via repo with no `update`/`delete` methods + `GuardedPdo` wrapper (production + tests) + regression test; DB-level trigger is documented follow-up.
- **ADR-3** — Per-tenant webhook secret default; global mode via `EVEREE_WEBHOOK_SIGNING_MODE` env switch; column exists in both modes.
- **ADR-5** — HMAC-SHA256 with multi-candidate-secret rotation tolerance backed by `payrollTenants.webhookPriorSecretEncrypted` + `webhookPriorSecretExpiresAt`.
- **ADR-7** — Idempotency-Key on every POST; best-effort; documented residual risk if partner doesn't honor.
- **ADR-8** — Single namespace `BuyerKiosk\Payroll\` for all new code; no scattering into `Core\`.
- **ADR-11** — `EvereeWebhookPayloadRedactor` strips PII fields after HMAC verify, before DB INSERT; `EVEREE_PARTNER_API_TOKEN` env var used ONLY for the initial Company Instance create call (no tenant exists yet); `payrollTenants` carries prior-secret columns to support rotation.

**Implementation Context (from SDD §"Project Commands" and CLAUDE.md):**
- **Tests:** `./test.sh --testsuite unit` + `./test.sh --testsuite integration` + `./test.sh --stan` + `./test.sh --coverage`
- **Static analysis:** `cd userfrosting && ./vendor/bin/phpstan analyse src/BuyerKiosk/Payroll/`
- **Targeted test:** `cd userfrosting && ./vendor/bin/phpunit --filter "PayRateServiceTest"`
- **Schema migrations:** `php userfrosting/conductor run` (apply all) OR the `buyerkiosk-conductor-targeted-migration` skill for one-store testing
- **TaskEngine local workers:** `php userfrosting/bin/task worker:start`
- **Patterns to follow (NEW docs/patterns/ files are deliverables of this plan):**
  - `docs/patterns/payroll-token-encryption.md` — written in T6 (PRD F4 AC)
  - `docs/patterns/payroll-rate-history-append-only.md` — written in T8 (PRD F7)
  - `docs/patterns/payroll-webhook-hmac-rotation.md` — written in T10 (PRD F8 AC)
  - `docs/patterns/payroll-permission-key-cross-walk.md` — written in T2 (cross-walk doc)
  - `docs/patterns/taskengine-payroll-audit.md` — written in T4 (PRD F12 deliverable)
- **Interfaces to implement:**
  - `docs/interfaces/everee-api.md` — written in T7 (PRD F5 deliverable)
- **Existing patterns to study (read before T6 / T7 / T10 / T11):**
  - `userfrosting/src/BuyerKiosk/Security/Encryption.php` (encryption primitive)
  - `userfrosting/src/BuyerKiosk/QuickBooks/QuickBooksService.php` lines 78-162 (token storage template)
  - `userfrosting/src/BuyerKiosk/MobileScheduling/Repositories/ScheduleAuditRepository.php` (audit-log template)
  - `userfrosting/src/BuyerKiosk/TaskEngine/Domain/Job/BaseJob.php` (job template)
  - `userfrosting/src/BuyerKiosk/Compatibility/LegacyAliases.php` line 32+ (Employee alias)
  - `userfrosting/migrations/input/20251220_013_010_schedule_permissions.json` (permissions migration shape)

---

## Implementation Phases

Phases group into three tiers with hard dependency gates:

| Tier | Phases | Gate before next tier |
|---|---|---|
| **Phase 0 (Prerequisites)** | T1–T5 | All Phase 0 phases must merge before any Phase 1a code work begins (CON-21, PRD F1/F2/F3/F10/F12/F13). |
| **Phase 1a (Foundations)** | T6–T16 | All Phase 1a phases must merge AND the recorded-fixture test suite must be green before the partner-gated sandbox smoke test. |
| **Final** | T17 | Sign-off: PRD ACs verified, README "Ready for Phase 1b" decision recorded. |

### TIER: PHASE 0 PREREQUISITES (T1–T5)

- [x] **T1** Employee class consolidation `[ref: PRD F1; lines: 97-104]` `[ref: SDD ICO-6 + Constraint CON-21; lines: 168-173, 62]` — DONE (commit f429d715b)
  - Hard gate per CON-21: no Phase 1a work may merge until this is in master.
  - [ ] T1.1 Prime Context
    - [ ] T1.1.1 Read `userfrosting/src/BuyerKiosk/Compatibility/LegacyAliases.php` lines 22-260 (autoloader + `$legacyClassMap`)
    - [ ] T1.1.2 Read `userfrosting/src/BuyerKiosk/Core/Employee.php` (2.3KB stub) and `userfrosting/src/BuyerKiosk/Employee/Employee.php` (13.5KB canonical)
    - [ ] T1.1.3 `grep -rn 'BuyerKiosk\\\\Core\\\\Employee\\|use BuyerKiosk\\\\Core\\\\Employee' userfrosting/src userfrosting/routes` to enumerate every callsite of the legacy stub
  - [ ] T1.2 Write Tests
    - [ ] T1.2.1 Add a unit test that asserts `$legacyClassMap['Employee']` resolves to `BuyerKiosk\Employee\Employee::class` (PRD F1 AC) `[activity: test]`
    - [ ] T1.2.2 Verify full test suite is green BEFORE the consolidation PR (baseline snapshot) `[activity: run-tests]`
  - [ ] T1.3 Implement
    - [ ] T1.3.1 Update `LegacyAliases.php` line 32 to point `'Employee' => \BuyerKiosk\Employee\Employee::class` (was `\Core\Employee::class`) `[activity: code]`
    - [ ] T1.3.2 Update `LegacyAliases.php` line 46 (`EmployeeDaily`) and other `Employee*` entries to mirror `[activity: code]`
    - [ ] T1.3.3 For each non-Compatibility callsite found in T1.1.3, replace `use BuyerKiosk\Core\Employee` with `use BuyerKiosk\Employee\Employee` (or delete the import if `Employee` is unreferenced) `[activity: code]`
    - [ ] T1.3.4 Reduce `BuyerKiosk\Core\Employee.php` to a thin re-export `class Employee extends \BuyerKiosk\Employee\Employee {}` OR delete the file outright after verifying `grep` returns zero non-Compatibility hits `[activity: code]`
  - [ ] T1.4 Validate
    - [ ] T1.4.1 `./test.sh` must pass with NO regressions vs the T1.2.2 baseline `[activity: run-tests]`
    - [ ] T1.4.2 `cd userfrosting && ./vendor/bin/phpstan analyse` stays at the current baseline level `[activity: run-tests]`
    - [ ] T1.4.3 `grep -rn 'BuyerKiosk\\\\Core\\\\Employee' userfrosting/src userfrosting/routes` returns ONLY hits in `Compatibility/LegacyAliases.php` `[activity: review-code]`
    - [ ] T1.4.4 PR description references PRD F1 and confirms Test Scenario 24 passes `[activity: business-acceptance]` `[ref: SDD §Test Specifications Scenario 24]`

- [x] **T2** Foundational schema migrations (16 JSONs) `[ref: PRD F3; lines: 114-136]` `[ref: PRD F10; lines: 202-206]` `[ref: SDD §Data Storage Changes; lines: 696-1183]` — DONE (commits 24c1d7467 + 03f06baa9 apply-fix); codex-reviewed. T2.4.1/T2.4.2 VALIDATED via full `conductor run`: all 16 apply cleanly across 7 dev stores, idempotent re-run (zero applied/zero errors), 11 payroll tables + userPayrollProfiles + 12/12 permissions + 141 users backfilled confirmed. Two SDD schema-name drifts fixed at apply (see Deviation Log 2026-05-29).
  - [ ] T2.1 Prime Context
    - [ ] T2.1.1 Read SDD §"Data Storage Changes" end-to-end (1183 lines): every table definition, every ALTER, the migration ordering section, and the §"Phase 0 non-schema deliverables" header (focus on migrations only here)
    - [ ] T2.1.2 Read `userfrosting/migrations/input/20260520_001_buyqueue_checkoutStatus.json` (alter_table reference) and `userfrosting/migrations/input/20251220_013_010_schedule_permissions.json` (uf_authorize_group reference)
    - [ ] T2.1.3 Read existing schemas: `kiosk_buykiosk.stores`, `kiosk_users.users`, `kiosk_users.userStoreAssignments` (use `database-architect` skill OR run `DESCRIBE` against the dev DB)
  - [ ] T2.2 Write Tests
    - [ ] T2.2.1 Author Test Scenario 22 (idempotent migrations): apply twice, assert second pass records ZERO additional schema changes `[ref: SDD §Test Specifications Scenario 22]` `[activity: test]`
    - [ ] T2.2.2 Author smoke tests asserting each `check_query` correctly detects pre/post state on a dev store `[activity: test]`
  - [ ] T2.3 Implement (16 migrations; ordering is filename-lexical and matches FK dependency)
    - [ ] T2.3.1 `20260522_001_payroll_create_tenants.json` (payrollTenants + uniq EIN + webhookPriorSecret columns from ADR-11) `[activity: migration]` `[ref: SDD §"Table 1"; lines: 736-770]`
    - [ ] T2.3.2 `20260522_002_payroll_create_runs.json` (payrollRuns + payrollRunLines + payrollRunSnapshots — note ON DELETE RESTRICT per Codex fix) `[activity: migration]` `[ref: SDD §"Tables 2-4"; lines: 772-841]`
    - [ ] T2.3.3 `20260522_003_payroll_create_webhook_events.json` (UNIQUE evereeEventId) `[activity: migration]` `[ref: SDD §"Table 5"; lines: 843-869]`
    - [ ] T2.3.4 `20260522_004_payroll_create_audit_log.json` (mirrors scheduleAuditLog shape) `[activity: migration]` `[ref: SDD §"Table 6"; lines: 871-901]`
    - [ ] T2.3.5 `20260522_005_payroll_create_rate_history.json` (composite index + isRetirementTombstone column) `[activity: migration]` `[ref: SDD §"Table 7"; lines: 903-941]`
    - [ ] T2.3.6 `20260522_006_payroll_create_coa_mappings.json` (table only; no UI seam yet — PRD Won't-Have) `[activity: migration]` `[ref: SDD §"Table 8"; lines: 943-963]`
    - [ ] T2.3.7 `20260522_007_payroll_create_pto_tables.json` (ptoAccrualPolicies + ptoAccrualBalances WITH fk_pto_balance_tenant + ptoRequests) `[activity: migration]` `[ref: SDD §"Tables 9-11"; lines: 965-1005]`
    - [ ] T2.3.8 `20260522_008_payroll_create_user_profiles.json` (lands in kiosk_users; uniq (userId, payrollTenantId)) `[activity: migration]` `[ref: SDD §"Table 12"; lines: 1007-1029]`
    - [ ] T2.3.9 `20260522_009_users_legal_pii_columns.json` (kiosk_users.users + PII columns, each labeled by source-of-truth category in description per CON-18) `[activity: migration]` `[ref: SDD §"Extension 1"; lines: 1031-1051]`
    - [ ] T2.3.10 `20260522_010_stores_payroll_tenant_fk.json` (nullable FK to payrollTenants) `[activity: migration]` `[ref: SDD §"Extension 2"; lines: 1053-1064]`
    - [ ] T2.3.11 `20260522_011_user_store_assignments_classification.json` (employmentClassification enum) `[activity: migration]` `[ref: SDD §"Extension 3"; lines: 1066-1074]`
    - [ ] T2.3.12 `20260522_012_positions_payroll_columns.json` (workersCompClassCode + qboWageAccountId) `[activity: migration]` `[ref: SDD §"Extension 4"; lines: 1076-1084]`
    - [ ] T2.3.13 `20260522_013_schedule_time_punches_tips.json` (per-store DB; cashTipsCents + creditTipsCents + submittedToEvereeAt) `[activity: migration]` `[ref: SDD §"Extension 5"; lines: 1086-1098]`
    - [ ] T2.3.14 `20260522_014_punch_type_enum_verification_note.json` (schema-verification, observability only, not pre-flight per Codex rename) `[activity: migration]` `[ref: SDD §"Schema-verification migration"; lines: 1101-1104]`
    - [ ] T2.3.15 `20260522_015_display_name_migration_helper.json` (best-effort split into legalFirstName/Last; PRD F3 AC) `[activity: migration]` `[ref: SDD §"Extension 1"; lines: 1049-1051]`
    - [ ] T2.3.16 `20260522_016_payroll_permission_keys.json` (12 uf_authorize_group rows × roles; PRD F10) `[activity: migration]` `[ref: SDD §"Permission keys migration"; lines: 1108-1138]`
    - [ ] T2.3.17 Cross-walk doc `docs/patterns/payroll-permission-key-cross-walk.md` describing BK role names ↔ UF group IDs `[activity: doc-write]`
  - [ ] T2.4 Validate
    - [ ] T2.4.1 `php userfrosting/conductor run` applies all 16 migrations cleanly on a fresh dev store `[activity: run-tests]`
    - [ ] T2.4.2 Re-running `php userfrosting/conductor run` records ZERO additional schema changes (idempotency invariant — Test Scenario 22) `[activity: run-tests]`
    - [ ] T2.4.3 `db-migration-check` skill scans the 16 JSONs for dangerous operations `[activity: review-code]`
    - [ ] T2.4.4 Each migration description carries its PII source-of-truth label where applicable (CON-18) `[activity: review-code]`
    - [ ] T2.4.5 Test Scenario 23 passes — every permission key resolves correctly per role (Owner/Manager/Shift Lead/Employee) `[ref: SDD §Test Specifications Scenario 23]` `[activity: business-acceptance]`

- [~] **T3** Person-centric account model verification `[ref: PRD F2; lines: 106-112]` `[ref: SDD §"Phase 0 non-schema deliverables — Deliverable 1"; lines: 1188-1204]` — CLI + report DONE (commit 5eceaf15e); codex-reviewed. CLI ran on dev DB: 1 cross-merchant user (userId 28, ou00+pc00). GAP REMEDIATED (commit 81759f742): spec-owner chose to build "assign existing user to a second store" (detect+confirm UI; findExistingUserByContact + assignExistingUserToStore; null-safe + ambiguous-block; two codex reviews). Only remaining CON-22 item is the formality of a second-engineer `Signed-off-by:` (T3.4.1).
  - Hard gate per CON-22: no Phase 1a service that maps `users.id → evereeWorkerId` may be implemented until this report is merged and signed off.
  - [ ] T3.1 Prime Context
    - [ ] T3.1.1 Read SDD §"Phase 0 non-schema deliverables — Deliverable 1" (the report's contents are spec'd there)
    - [ ] T3.1.2 Read `kiosk_users.userStoreAssignments` schema and `BuyerKiosk\Employee\EmployeeManager` (the manage-employees flow per spec 014)
  - [ ] T3.2 Write Tests
    - [ ] T3.2.1 Test Scenario 25 — assert verification report exists with all required evidence `[ref: SDD §Test Specifications Scenario 25]` `[activity: test]`
  - [ ] T3.3 Implement
    - [ ] T3.3.1 Author `userfrosting/bin/payroll/verify-cross-merchant-users.php` running `SELECT u.id, COUNT(DISTINCT usa.typeNum) AS distinctStoreCount FROM users u JOIN userStoreAssignments usa ON u.id = usa.userId WHERE usa.isActive = 1 GROUP BY u.id HAVING distinctStoreCount >= 2` `[activity: cli-tool]`
    - [ ] T3.3.2 If the production query returns ZERO rows, write a dev-store seeding script that creates a `users.id` with assignments at two distinct typeNums and re-run the verification `[activity: cli-tool]`
    - [ ] T3.3.3 Execute the manage-employees flow at the second store; capture row-counts before/after to confirm NO duplicate `users` row is created `[activity: business-acceptance]`
    - [ ] T3.3.4 Author `docs/specs/050-everee-payroll-foundations/person-centric-account-verification.md` with: SQL evidence, walkthrough transcript / screen-recording link, gap-remediation plan OR "no gaps found" outcome, `Signed-off-by:` line `[activity: doc-write]`
  - [ ] T3.4 Validate
    - [ ] T3.4.1 At least one engineer OTHER than the report author signs off (PR approval OR `Signed-off-by:` line) `[activity: business-acceptance]`
    - [ ] T3.4.2 If gaps found: confirm the remediation path (expand Phase 0 OR open separate spec) is documented and accepted by spec owner BEFORE Phase 1a starts `[activity: business-acceptance]`

- [~] **T4** TaskEngine infrastructure audit `[ref: PRD F12; lines: 214-220]` `[ref: SDD §"Pattern Documentation"; lines: 2587-2592]` — Audit doc + test DONE (commit c0e1074eb); codex-reviewed (false "no re-drive" claim corrected, payroll-queue fix scope completed to include TaskCommand + Scheduler). Recommendation: add `payroll` queue in Phase 1a (T10); defer reconciliation + PTO accrual to 1c. HUMAN GATE OPEN: second-engineer `Signed-off-by:` (T4.4.1).
  - Hard gate per PRD F12: Feature 8's async-AC (T10) does not exercise until this audit is merged.
  - [ ] T4.1 Prime Context
    - [ ] T4.1.1 Read `userfrosting/src/BuyerKiosk/TaskEngine/` — `Domain/Job/BaseJob.php`, `Domain/Job/JobInterface.php`, `Domain/Job/JobDefinition.php`, `Application/JobDispatcher.php`, `Commands/TaskCommandFactory.php`
    - [ ] T4.1.2 Read the existing `taskengine_job_definitions` table schema (queues, max-retries, retry-backoff)
    - [ ] T4.1.3 Read `userfrosting/src/BuyerKiosk/Goals/Jobs/GoalForecastComputeJob.php` as a concrete job example
    - [ ] T4.1.4 Survey existing queue names in use (`getQueue()` values across all `BuyerKiosk\*\Jobs\*` classes)
  - [ ] T4.2 Write Tests
    - [ ] T4.2.1 Test Scenario 26b — assert the audit doc exists with the required sections and a second-engineer sign-off `[ref: SDD §Test Specifications Scenario 26b]` `[activity: test]`
  - [ ] T4.3 Implement
    - [ ] T4.3.1 Author `docs/patterns/taskengine-payroll-audit.md` with: queue inventory + worker classes + retry semantics + identified gaps for webhook async + daily reconciliation + PTO accrual + each gap's in-phase-vs-Phase-1c recommendation with scoped file list `[activity: doc-write]`
    - [ ] T4.3.2 If any in-phase gaps are identified, scope them as additional T4 sub-tasks (e.g., T4.3.2.a, T4.3.2.b) and merge those fixes before T10 begins `[activity: code]`
  - [ ] T4.4 Validate
    - [ ] T4.4.1 Second-engineer sign-off captured (PR approval OR `Signed-off-by:` line in the audit doc) `[activity: business-acceptance]`
    - [ ] T4.4.2 PR that introduces `EvereeWebhookHandler` async-dispatch (T10) cites this audit doc in its description `[activity: business-acceptance]`

- [~] **T5** Phase 0 pre-flight checklist `[ref: PRD F13; lines: 222-229]` `[ref: SDD §"Phase 0 non-schema deliverables — Deliverable 3"; lines: 1220-1235]` `[parallel: true]` — Checklist scaffold + ready-to-send partner email draft + test DONE (commit 5d1df2f14); codex-reviewed. CS/PARTNER HUMAN ACTIONS OPEN (cannot be automated): send kickoff email, collect 5 pilot-store rate CSVs, name owners, engineering sign-off (T5.4.1).
  - CS-owned; engineering reviews. Runs in parallel with T1-T4.
  - [ ] T5.1 Prime Context
    - [ ] T5.1.1 Read PRD F13 ACs and SDD §"Phase 0 non-schema deliverables — Deliverable 3"
    - [ ] T5.1.2 Read analysis §13 partner question stack for the kickoff email content
  - [ ] T5.2 Write Tests
    - [ ] T5.2.1 Test Scenario 27 — checklist completeness gate (each named owner, partner email tracking, pilot list, rate audit links, engineering review sign-off) `[ref: SDD §Test Specifications Scenario 27]` `[activity: test]`
  - [ ] T5.3 Implement
    - [ ] T5.3.1 Author `docs/specs/050-everee-payroll-foundations/phase-0-preflight-checklist.md` with the 4 required items: pilot-store rate-data audit (link to CS-accessible spreadsheet), partner-manager kickoff email status, finalized pilot candidate list, named owners per item `[activity: doc-write]` `[activity: partner-coordination]`
    - [ ] T5.3.2 Send the partner-manager kickoff email; record the date and tracking thread reference in the checklist `[activity: partner-coordination]`
    - [ ] T5.3.3 Collect rate-data audit CSVs from each of the 5 pilot-candidate stores (or document explicit waivers) `[activity: partner-coordination]`
  - [ ] T5.4 Validate
    - [ ] T5.4.1 Engineering review captured: confirms no Phase 1a Must Have requires an unticked pre-flight item `[activity: business-acceptance]`
    - [ ] T5.4.2 Weekly status updates on partner thread until sandbox credentials AND HMAC algorithm are confirmed at minimum `[activity: partner-coordination]`

### TIER: PHASE 1a FOUNDATIONS (T6–T16)

All T6–T16 phases require T1–T5 merged. Many sibling phases can run in parallel (see `[parallel: true]` tags + dependency notes).

- [x] **T6** EvereeTokenStorage + Encryption config `[ref: PRD F4; lines: 138-145]` `[ref: SDD §"Services — EvereeTokenStorage"; lines: 1474-1486]` `[ref: ADR-1]` `[parallel: true]` — DONE (commit c6d3d1f16); codex security-reviewed. Encrypt-then-MAC hardening over raw CBC (see Deviation Log 2026-06-01). Migration 017 widened *Encrypted cols 512->1024 (applied live). Real PayrollTenant model now exists (T11's stub deleted).
  - No service dependencies; pairs cleanly with T8 + T11 in parallel.
  - [ ] T6.1 Prime Context
    - [ ] T6.1.1 Read `userfrosting/src/BuyerKiosk/Security/Encryption.php` end-to-end
    - [ ] T6.1.2 Read `userfrosting/src/BuyerKiosk/QuickBooks/QuickBooksService.php` lines 78-91 (loadEncryption), 123-135 (encryptValue), 144-162 (decryptValue), 329 + 448 (usage examples)
    - [ ] T6.1.3 Read `userfrosting/config/qb-encryption.php` (config file shape)
    - [ ] T6.1.4 Read `userfrosting/tests/Unit/Security/EncryptionTest.php` + `userfrosting/tests/Unit/QuickBooks/Services/TokenEncryptionRequiredTest.php`
  - [ ] T6.2 Write Tests
    - [ ] T6.2.1 Test Scenario 13 — var_dump / print_r / json_encode / exception stringification never reveals plaintext `[ref: SDD §Test Specifications Scenario 13]` `[activity: test]`
    - [ ] T6.2.2 Test Scenario 14 — write fails-closed when encryption is misconfigured `[ref: SDD §Test Specifications Scenario 14]` `[activity: test]`
    - [ ] T6.2.3 Test Scenario 13b — CI check flags direct token logging in new code under `BuyerKiosk\Payroll\` `[ref: SDD §Test Specifications Scenario 13b]` `[activity: test]` `[activity: security-review]`
    - [ ] T6.2.4 Encryption roundtrip test (mirror EncryptionTest.php for the new keying) `[activity: test]`
  - [ ] T6.3 Implement
    - [ ] T6.3.1 Author `userfrosting/config/everee-encryption.php` mirroring `config/qb-encryption.php`, reading from `$_ENV['EVEREE_ENCRYPTION_KEY']` `[activity: code]`
    - [ ] T6.3.2 Author `BuyerKiosk\Payroll\Exceptions\EvereeEncryptionRequiredException.php` and `TokenDecryptionException.php` `[activity: code]`
    - [ ] T6.3.3 Author `BuyerKiosk\Payroll\Services\EvereeTokenStorage.php` with `encryptTokenForStorage`, `decryptTokenForUse`, `encryptWebhookSecretForStorage`, `decryptWebhookSecretForUse`, AND `decryptWebhookPriorSecretForUse` (per ADR-11 rotation) `[activity: code]` `[ref: SDD §"SERVICE: EvereeTokenStorage"]`
    - [ ] T6.3.4 Author `BuyerKiosk\Payroll\Models\PayrollTenant.php` with `private readonly` encrypted fields, `getEncryptedApiTokenForServiceUse()`, `jsonSerialize()` + `__debugInfo()` that OMIT *Encrypted fields `[activity: code]`
    - [ ] T6.3.5 Author `docs/patterns/payroll-token-encryption.md` documenting the pattern + the CI-check extension procedure (PRD F4 AC) `[activity: doc-write]`
    - [ ] T6.3.6 Implement the CI-check static analyzer: either a PHPStan custom rule OR a grep-based scanner over new code under `BuyerKiosk\Payroll\` flagging known token-bearing variable names in log/exception/sprintf contexts (PRD F4 AC) `[activity: security-review]`
  - [ ] T6.4 Validate
    - [ ] T6.4.1 Unit tests green: `./test.sh --testsuite unit --filter EvereeTokenStorageTest` `[activity: run-tests]`
    - [ ] T6.4.2 `EVEREE_ENCRYPTION_KEY` present in `.env.example` + deployment docs `[activity: review-code]`
    - [ ] T6.4.3 The CI token-leak check FAILS when an intentional `log("plaintext token: " . $token)` is added to a test fixture, then succeeds when removed `[activity: security-review]`

- [x] **T7** EvereeApiClient `[ref: PRD F5; lines: 147-158]` `[ref: SDD §"Service — EvereeApiClient"; lines: 1488-1517]` `[ref: ADR-6, ADR-7, ADR-11]` `[parallel: true]` — DONE (commit b546320e6); codex security-reviewed. Guzzle MockHandler fixture replay (ZERO network). CRITICAL token-leak-via-stack-trace-args fixed. 429-without-Retry-After resolved per PRD F5 (see Deviation Log 2026-06-01b). 220 payroll tests green.
  - Depends on T6 (token storage) + T11 (PayrollAuditService — the client's `_request` audits every call via `payroll.api.everee_call`).
  - [ ] T7.1 Prime Context
    - [ ] T7.1.1 Read Everee public docs at `https://developers.everee.com/docs` (auth headers, base URL, idempotency keys, error taxonomy, webhook event types)
    - [ ] T7.1.2 Read SDD §"Implementation Example 2" (the retry-with-429 sample) — verify the 80-line implementation shape `[ref: SDD §"Example 2: EvereeApiClient — Retry with 429 Retry-After honoring"; lines: 1965-2052]`
    - [ ] T7.1.3 Read SDD §"Algorithm 2" + error matrix rows for 401/429/5xx/timeout/idempotency `[ref: SDD §"Runtime View — Error Handling"; lines: 2294-2335]` `[ref: SDD §"Algorithm 2: Webhook signature verification"; n/a — this is API client not webhook]`
  - [ ] T7.2 Write Tests (recorded-fixture replay; PRD F5 Phase 1a sign-off gate)
    - [ ] T7.2.1 Test Scenario 9 — 5xx then 200 with exponential backoff `[ref: SDD §Test Specifications Scenario 9]` `[activity: test]`
    - [ ] T7.2.2 Test Scenario 10 — 429 Retry-After honored ONCE then EvereeRateLimitException `[ref: SDD §Test Specifications Scenario 10]` `[activity: test]`
    - [ ] T7.2.3 Test Scenario 11 — 401 raises EvereeAuthException with no retry `[ref: SDD §Test Specifications Scenario 11]` `[activity: test]`
    - [ ] T7.2.4 Test Scenario 11b — happy GET worker (per Codex review) `[ref: SDD §Test Specifications Scenario 11b]` `[activity: test]`
    - [ ] T7.2.5 Test Scenario 11c — happy POST createWorker with Idempotency-Key (per Codex review) `[ref: SDD §Test Specifications Scenario 11c]` `[activity: test]`
    - [ ] T7.2.6 Test Scenario 11d — 4xx validation passthrough with Everee error payload (per Codex review) `[ref: SDD §Test Specifications Scenario 11d]` `[activity: test]`
    - [ ] T7.2.7 Test Scenario 12 — POST timeout WITHOUT idempotency key raises EvereeUncertainStateException `[ref: SDD §Test Specifications Scenario 12]` `[activity: test]`
    - [ ] T7.2.8 Record fixtures in `tests/Fixtures/Payroll/everee-response-*.json` for each scenario above
  - [ ] T7.3 Implement
    - [ ] T7.3.1 Author the 5 base exception classes: `EvereeApiException`, `EvereeAuthException`, `EvereeRateLimitException`, `EvereeValidationException`, `EvereeUncertainStateException` `[activity: code]`
    - [ ] T7.3.2 Author `BuyerKiosk\Payroll\Services\EvereeApiClient.php` with the retry/backoff/429/idempotency logic from SDD Implementation Example 2 `[activity: code]`
    - [ ] T7.3.3 Both Basic-auth + tenant-header path (per-tenant calls) AND partner-token-only path (Company Instance create — ADR-11) MUST be supported in `_request` `[activity: code]`
    - [ ] T7.3.4 Author `docs/interfaces/everee-api.md` documenting auth headers, base URL config, endpoints used in Phase 1a, error taxonomy, idempotency-key conventions (PRD F5 deliverable) `[activity: doc-write]`
  - [ ] T7.4 Validate
    - [ ] T7.4.1 Unit tests green: `./test.sh --testsuite unit --filter EvereeApiClientTest` — fixture-replay suite is the **Phase 1a sign-off gate** per PRD F5 AC `[activity: run-tests]`
    - [ ] T7.4.2 NO outbound network traffic during unit tests (PRD F5 sign-off gate) — verified by mock HTTP client `[activity: run-tests]`
    - [ ] T7.4.3 `EVEREE_API_BASE_URL` + `EVEREE_PARTNER_API_TOKEN` documented in `.env.example` `[activity: review-code]`

- [x] **T8** PayRateService + PayRateHistoryRepository + GuardedPdo `[ref: PRD F7; lines: 168-178]` `[ref: SDD §"Service — PayRateService"; lines: 1519-1571]` `[ref: ADR-2]` `[parallel: true]` — DONE (commit 793b81c0a); codex security-reviewed. Append-only enforced in depth: hardened GuardedPdo (31 bypass vectors blocked, escape hatch removed) + DB-level BEFORE UPDATE/DELETE triggers (migration 019, applied + VERIFIED blocking real-row mutations). getRate nulls any tombstone. 616 unit + 12 integration green.
  - Depends on T2 (schema) + T11 (audit service); parallel with T6/T7.
  - [ ] T8.1 Prime Context
    - [ ] T8.1.1 Read SDD §"Implementation Example 1" (the append-only getRate sample) — the load-bearing algorithm `[ref: SDD §"Example 1: PayRateService::getRate"; lines: 1899-1963]`
    - [ ] T8.1.2 Read SDD §"Algorithm 1: Point-in-time rate resolution" `[ref: SDD §"Algorithm 1"; lines: 2376-2418]`
    - [ ] T8.1.3 Read PRD F7 — every AC + every edge case 1-6 `[ref: PRD §"Feature 7"; lines: 168-178]` `[ref: PRD §"Edge Cases" 1-6; lines: 286-300]`
  - [ ] T8.2 Write Tests
    - [ ] T8.2.1 Test Scenario 4 — supersession does NOT mutate prior row (ONE NEW audit entry) `[ref: SDD §Test Specifications Scenario 4]` `[activity: test]`
    - [ ] T8.2.2 Test Scenario 5 — empty notes + non-positive rates rejected `[ref: SDD §Test Specifications Scenario 5]` `[activity: test]`
    - [ ] T8.2.3 Test Scenario 6 — raw UPDATE attempts raise PayRateImmutableException `[ref: SDD §Test Specifications Scenario 6]` `[activity: test]` `[activity: security-review]`
    - [ ] T8.2.4 Test Scenario 7 — concurrent setRate: both INSERTs succeed, greatest (effectiveFrom, id) wins `[ref: SDD §Test Specifications Scenario 7]` `[activity: test]`
    - [ ] T8.2.5 Test Scenario 8 — tombstone retirement (rateType copied from prior active row per Codex fix) `[ref: SDD §Test Specifications Scenario 8]` `[activity: test]`
    - [ ] T8.2.6 Boundary test: `getRate(asOf == effectiveUntil)` returns null on tombstone; `isActiveAt(asOf == effectiveUntil)` returns false (boundary consistency per Codex review) `[activity: test]`
  - [ ] T8.3 Implement
    - [ ] T8.3.1 Author `BuyerKiosk\Payroll\Exceptions\PayRateImmutableException.php` `[activity: code]`
    - [ ] T8.3.2 Author `BuyerKiosk\Payroll\Models\PayRateEntry.php` — IMMUTABLE; `isActiveAt(asOf): bool` with the boundary semantics matching `getRate` `[activity: code]`
    - [ ] T8.3.3 Author `GuardedPdo` subclass that scans every SQL string for `UPDATE`/`DELETE` against `payRateHistory` and raises `PayRateImmutableException` BEFORE the query reaches MySQL — wire it into the DI container so BOTH production AND tests use it (ADR-2 + Codex clarification) `[activity: code]` `[activity: security-review]`
    - [ ] T8.3.4 Author `BuyerKiosk\Payroll\Repositories\PayRateHistoryRepository.php` exposing ONLY `insert`, `findGreatestEffectiveFromAtOrBefore`, `listOrdered` — NO `update` / `delete` / `upsert` methods `[activity: code]`
    - [ ] T8.3.5 Author `BuyerKiosk\Payroll\Services\PayRateService.php` with `setRate`, `getRate`, `retireRate` (copying rateType from active row), `listHistory` returning `map<positionId, reverse-chronological-array>` per Codex enhancement `[activity: code]`
    - [ ] T8.3.6 Author `docs/patterns/payroll-rate-history-append-only.md` documenting the invariant + GuardedPdo + the DB-level follow-up procedure (PRD F7 AC) `[activity: doc-write]`
  - [ ] T8.4 Validate
    - [ ] T8.4.1 Unit tests green: `./test.sh --testsuite unit --filter PayRateServiceTest` `[activity: run-tests]`
    - [ ] T8.4.2 Integration test green: `./test.sh --testsuite integration --filter RateHistoryAppendOnlyTest` (Test Scenario 6 raw UPDATE attempt) `[activity: run-tests]`
    - [ ] T8.4.3 PayRateHistoryRepository has NO public `update` or `delete` method — verified by reflection-based test `[activity: review-code]`

- [x] **T9** EvereeProvisioningService + PayrollTenantRepository `[ref: PRD F6; lines: 160-166]` `[ref: SDD §"Service — EvereeProvisioningService"; lines: 1518-1538]` `[ref: ADR-11]` — DONE (commit 567d1d0d3); codex-reviewed. Idempotent by EIN, gate-all-first, isActive=0 until verified-ready (CRITICAL fix: bank-not-verified keeps it 0), inactive-EIN re-verify recovery, scrubbed error audit, repo update() allowlist. 279 payroll tests green.
  - Depends on T6 (token storage) + T7 (API client) + T11 (audit service) + T13 (SchedulingProviderGate — `provisionTenant` calls `assertAllowed` for each typeNum attach).
  - [ ] T9.1 Prime Context
    - [ ] T9.1.1 Read SDD §"Primary Flow A" sequence diagram (provisioning end-to-end)
    - [ ] T9.1.2 Read PRD F6 + Cross-Feature Edge Case F6 (duplicate EIN, partial provisioning)
    - [ ] T9.1.3 Read SDD §"ADR-11" for the partner-level auth + `provisionManually` seam
  - [ ] T9.2 Write Tests
    - [ ] T9.2.1 Test Scenario 1 — tenant provisioning happy path end-to-end `[ref: SDD §Test Specifications Scenario 1]` `[activity: test]`
    - [ ] T9.2.2 Test Scenario 2 — idempotency by EIN (duplicate EIN returns existing tenant) `[ref: SDD §Test Specifications Scenario 2]` `[activity: test]`
    - [ ] T9.2.3 Test Scenario 3 — SchedulingProviderGate rejects WIW/Homebase (depends on T13) `[ref: SDD §Test Specifications Scenario 3]` `[activity: test]`
    - [~] T9.2.4 ~~Partial-provisioning test (CrossFeatureEdgeCase F6): Everee returns 200 but a "bank not verified" signal → isActive=0 preserved~~ **REDEFINED (T15, owner-approved):** the "bank not verified" half is removed — that signal does not exist on the real Everee worker resource and per-worker bank/TIN status is the wrong gate for COMPANY activation. F6 readiness is now "non-throwing `listWorkers` 200 = reachable"; the verification-call-FAILS (throws) → isActive=0 half is RETAINED and tested. See `testVerificationActivatesOnReachableEvenWhenWorkerBankUnverified` + the listWorkers-throws test. `[activity: test]`
    - [ ] T9.2.5 Multi-store attach test (PRD F6 AC bullet "Multiple BK stores can share a single payrollTenants row"): provision a tenant, attach two distinct typeNums to it, assert `stores.payrollTenantId` is the same value on both rows; assert two separate audit entries (one per store) `[activity: test]` `[ref: PRD F6 AC line 165]`
    - [ ] T9.2.6 Provisioning-manual path test (ADR-11): call `provisionManually(...)` with portal-supplied IDs; assert tenant row is INSERTed, token is encrypted, audit entry has `metadata.provisioning_path='manual_portal'` `[activity: test]`
  - [ ] T9.3 Implement
    - [ ] T9.3.1 Author `BuyerKiosk\Payroll\Repositories\PayrollTenantRepository.php` (`insert`, `update`, `findById`, `findByEin`, `findByEvereeCompanyId`) — note: `update` is allowed on `payrollTenants` (NOT append-only) `[activity: code]`
    - [ ] T9.3.2 Author `BuyerKiosk\Payroll\Services\EvereeProvisioningService.php` with `provisionTenant(...)` (idempotent by EIN, partner-token auth for the create call) AND `provisionManually(...)` (portal-supplied IDs seam) `[activity: code]`
    - [ ] T9.3.3 Wire reassign-store seam (`reassignStore(typeNum, newTenantId, actorUserId)`) — Phase 1a-optional; implement at least the audit-logged contract `[activity: code]`
  - [ ] T9.4 Validate
    - [ ] T9.4.1 Unit + integration tests green `[activity: run-tests]`
    - [ ] T9.4.2 Provisioning idempotency verified — re-calling with same EIN returns existing row + writes audit entry with `metadata.idempotent=true` `[activity: business-acceptance]`
    - [ ] T9.4.3 `EVEREE_PARTNER_API_TOKEN` env var is documented in `.env.example` AND in `docs/patterns/payroll-token-encryption.md` AND consumed by `EvereeApiClient::createCompanyInstance` per ADR-11 (verified by reading the test fixture that exercises the partner-token auth path) `[activity: review-code]`
    - [ ] T9.4.4 `provisionManually(...)` smoke test: provide portal-supplied IDs and assert the tenant row is INSERTed without an Everee API call being made `[activity: business-acceptance]`

- [x] **T10** EvereeWebhookHandler + EvereeWebhookPayloadRedactor + ProcessEvereeWebhookJob + UserPayrollProfile model/repo `[ref: PRD F8; lines: 180-192]` `[ref: SDD §"Service — EvereeWebhookHandler"; lines: 1573-1605]` `[ref: ADR-3, ADR-5, ADR-11]` — DONE (R1 b981294ce ingestion, R2 aea35c21e processing); codex-reviewed both rounds. HMAC verify (signed-body dedupe, rotation, strict-timestamp replay defense), PII redactor, 11-event job (registered, maxRetries=4), recovery CLI, public route. Migration 018 live. 465 payroll tests green. CAVEAT: both build agents misreported (R1 fabricated tests; R2 left the job unregistered) — caught by codex + independent count verification.
  - Depends on T2 (schema) + T6 (token storage) + T9 (`PayrollTenantRepository` — webhook handler resolves tenant by evereeCompanyId) + T11 (audit) + T4 (TaskEngine audit MUST be merged first).
  - [ ] T10.1 Prime Context
    - [ ] T10.1.1 Read SDD §"Algorithm 2: Webhook signature verification" + §"Algorithm 3: Webhook ingestion idempotency" + §"Implementation Example 3: EvereeWebhookHandler::ingest" `[ref: SDD §"Algorithm 2"; lines: 2420-2467]` `[ref: SDD §"Algorithm 3"; lines: 2469-2509]` `[ref: SDD §"Example 3"; lines: 2054-2138]`
    - [ ] T10.1.2 Read SDD §"ADR-11" (PII redactor + partner-token + per-tenant prior-secret rotation columns) `[ref: SDD §"ADR-11"; lines: 2902-2920]`
    - [ ] T10.1.3 Read `userfrosting/src/BuyerKiosk/TaskEngine/Domain/Job/BaseJob.php` + `Application/JobDispatcher.php`
    - [ ] T10.1.4 Confirm T4 (TaskEngine audit) is MERGED — this is a hard gate per PRD F12
  - [ ] T10.2 Write Tests
    - [ ] T10.2.1 Test Scenario 15 — valid signature happy path `[ref: SDD §Test Specifications Scenario 15]` `[activity: test]`
    - [ ] T10.2.2 Test Scenario 16 — concurrent duplicate via UNIQUE `[ref: SDD §Test Specifications Scenario 16]` `[activity: test]`
    - [ ] T10.2.3 Test Scenario 17 — bad HMAC returns 401 (NOT 5xx) `[ref: SDD §Test Specifications Scenario 17]` `[activity: test]` `[activity: security-review]`
    - [ ] T10.2.4 Test Scenario 18 — timestamp outside tolerance returns 401, does NOT consume dedupe slot `[ref: SDD §Test Specifications Scenario 18]` `[activity: test]` `[activity: security-review]`
    - [ ] T10.2.5 Test Scenario 19 — rotation tolerance accepts signatures under prior secret while expiry > now() `[ref: SDD §Test Specifications Scenario 19]` `[activity: test]` `[activity: security-review]`
    - [ ] T10.2.6 Test Scenario 20 — unknown tenant under per_tenant signing returns 401 `[ref: SDD §Test Specifications Scenario 20]` `[activity: test]` `[activity: security-review]`
    - [ ] T10.2.7 Test Scenario 21 — async processing failure doesn't block subsequent events `[ref: SDD §Test Specifications Scenario 21]` `[activity: test]`
    - [ ] T10.2.8 PII-redaction test: a fixture webhook payload containing `ssn`, `bankAccount`, `w4` nested keys is REDACTED to `<REDACTED-PII>` before DB INSERT; high-severity log fires `[activity: test]` `[activity: security-review]`
    - [ ] T10.2.9 All-11-events ingestion matrix (PRD F8 AC "does not reject unknown-to-handler events at the ingestion layer"): for each of the 11 enumerated event types (`worker.created`, `worker.profile-updated`, `worker.deleted`, `worker.onboarding-completed`, `worker.onboarding-locked`, `worker.tin-verification-status-changed`, `worker.new-tax-forms-available`, `payment.paid`, `payment.deposit-returned`, `payment.updated-payment-method`, `payment-payables.status-changed`) AND for one synthetic unknown event type, assert ingestion succeeds (HTTP 200, row INSERTed, hmacValid=1) and the corresponding worker.* event triggers a `UserPayrollProfileRepository` write; payment.* events log "deferred-to-phase-1b" and mark processed; unknown event logs a warning and marks processed without raising `[activity: test]` `[ref: PRD F8 AC bullet about 11 events; lines: 187]`
    - [ ] T10.2.10 Author fixture files `tests/Fixtures/Payroll/everee-webhook-worker-created.json`, `everee-webhook-worker-profile-updated.json`, ..., `everee-webhook-payment-payables-status-changed.json` — one per event type — each HMAC-signed against a test secret `[activity: test]`
  - [ ] T10.3 Implement
    - [ ] T10.3.1 Author `BuyerKiosk\Payroll\Exceptions\WebhookSignatureException.php` `[activity: code]`
    - [ ] T10.3.2 Author `BuyerKiosk\Payroll\Models\PayrollWebhookEvent.php` `[activity: code]`
    - [ ] T10.3.3 Author `BuyerKiosk\Payroll\Repositories\PayrollWebhookEventRepository.php` with `insert`, `findById`, `findByEvereeEventId`, `findUnprocessed`, `markProcessed(id)`, `markFailed(id, error)` (split per Codex review) `[activity: code]`
    - [ ] T10.3.4 Author `BuyerKiosk\Payroll\Services\EvereeWebhookPayloadRedactor.php` — strips SSN/bank/W-4/I-9 keys, logs high-severity on every redaction (ADR-11) `[activity: code]` `[activity: security-review]`
    - [ ] T10.3.5 Author `BuyerKiosk\Payroll\Services\EvereeWebhookHandler.php` with `ingest()` implementing Algorithm 2 + Algorithm 3 + Step 0 PII redaction `[activity: code]`
    - [ ] T10.3.6 Author `BuyerKiosk\Payroll\Models\UserPayrollProfile.php` per SDD §"ENTITY: UserPayrollProfile" `[activity: code]` `[ref: SDD §"ENTITY: UserPayrollProfile"; lines: 1437-1444]`
    - [ ] T10.3.7 Author `BuyerKiosk\Payroll\Repositories\UserPayrollProfileRepository.php` with `insert`, `update`, `findByUserAndTenant`, `findByEvereeWorkerId`, plus per-event-type write helpers (`upsertFromWorkerCreated`, `applyProfileUpdate`, `applyOnboardingStatus`, `applyTinStatus`, `applyLifecycleStatus`) `[activity: code]` `[ref: SDD §"Data Processing Pattern"; lines: 1853-1879]`
    - [ ] T10.3.8 Author `BuyerKiosk\Payroll\Jobs\ProcessEvereeWebhookJob.php` (BaseJob subclass; `default` queue, `global` scope, 30s timeout; payload `{webhookEventId}`) — switch-on-event-type dispatches to the per-event-type write helpers from T10.3.7 for `worker.*` events; `payment.*` and `worker.new-tax-forms-available` are documented no-ops with explicit log lines (per PRD F8 AC and SDD §"Data Processing Pattern") `[activity: code]` `[ref: SDD §"JOB: ProcessEvereeWebhookJob"; lines: 1602-1620]`
    - [ ] T10.3.9 Author `BuyerKiosk\Payroll\Controllers\EvereeWebhookController.php` — Slim 2 route handler that reads RAW body via `$app->request->getBody()->getContents()` BEFORE any framework body parsing (per SDD gotcha) `[activity: code]`
    - [ ] T10.3.10 Author the webhook route in `userfrosting/routes/api/payroll.php` — POST `/api/payroll/webhook/everee`, public (no UF session) `[activity: code]`
    - [ ] T10.3.11 Author `docs/patterns/payroll-webhook-hmac-rotation.md` documenting rotation procedure + the per-tenant prior-secret column lifecycle (PRD F8 AC) `[activity: doc-write]`
    - [ ] T10.3.12 Author `tests/Unit/Payroll/Jobs/ProcessEvereeWebhookJobTest.php` exercising each branch of the event-type switch with fixture payloads from `tests/Fixtures/Payroll/everee-webhook-*.json`; assert the correct `UserPayrollProfileRepository` write helper is called per worker.* event; assert `payment.*` events log "deferred-to-phase-1b" + mark processed without error; assert unknown event types log + markProcessed without raising `[activity: test]`
  - [ ] T10.4 Validate
    - [ ] T10.4.1 Unit + integration tests green: `./test.sh --filter EvereeWebhookHandlerTest` + `./test.sh --filter WebhookIngestionTest` `[activity: run-tests]`
    - [ ] T10.4.2 Webhook async processing isolated per event — verified by Test Scenario 21 `[activity: business-acceptance]`
    - [ ] T10.4.3 PII redaction fires on every fixture payload that contains forbidden keys — application log entry confirmed `[activity: security-review]`
    - [ ] T10.4.4 `grep -rn 'catch (\\Exception' userfrosting/src/BuyerKiosk/Payroll/` returns ZERO hits — every catch block uses `\Throwable` or a specific exception class. Hits in `Payroll/` are review-blockers. `[activity: security-review]` `[ref: memory `slim2-stop-exception-swallowed` + memory `PHP 8.5 Throwable Gotcha`]`

- [ ] **T11** PayrollAuditService + PayrollAuditRepository `[ref: PRD F9; lines: 194-199]` `[ref: SDD §"Service — PayrollAuditService"; lines: 1607-1620]` `[parallel: true]`
  - No service dependencies (audit is the lowest-level building block). Required by T6 + T8 + T9 + T10 + T12 — author EARLY.
  - [ ] T11.1 Prime Context
    - [ ] T11.1.1 Read `userfrosting/src/BuyerKiosk/MobileScheduling/Repositories/ScheduleAuditRepository.php` end-to-end
    - [ ] T11.1.2 Read `userfrosting/src/BuyerKiosk/MobileScheduling/Models/ScheduleAuditEntry.php`
    - [ ] T11.1.3 Read SDD §"Service — PayrollAuditService" method-by-method
  - [ ] T11.2 Write Tests
    - [ ] T11.2.1 Unit tests for every `log*` method (logTenantProvisioned, logRateSet, logRateRetired, logWebhookReceived, logWebhookDeduplicated, logWebhookProcessed, logWebhookProcessingFailed, logApiCall, logGateRejection, logPermissionDenied) `[activity: test]`
    - [ ] T11.2.2 Test that `findWithFilters` paginates correctly (mirrors ScheduleAuditRepository test shape) `[activity: test]`
  - [ ] T11.3 Implement
    - [ ] T11.3.1 Author `BuyerKiosk\Payroll\Models\PayrollAuditEntry.php` (mirror ScheduleAuditEntry: JsonSerializable, action constants, ACTOR_* constants) `[activity: code]`
    - [ ] T11.3.2 Author `BuyerKiosk\Payroll\Repositories\PayrollAuditRepository.php` with `save`, `findWithFilters` `[activity: code]`
    - [ ] T11.3.3 Author `BuyerKiosk\Payroll\Services\PayrollAuditService.php` with all 10 typed `log*` factory methods + internal `_log(...)` `[activity: code]`
  - [ ] T11.4 Validate
    - [ ] T11.4.1 Unit tests green `[activity: run-tests]`
    - [ ] T11.4.2 Every action constant from SDD §"Application Data Models" PayrollAuditEntry is exposed `[activity: review-code]`

- [x] **T12** PayrollAdminController + routes/api/payroll.php (admin endpoints) `[ref: PRD F6 + F7; lines: 160-178]` `[ref: SDD §"Internal API Changes"; lines: 1186-1361]` — DONE (commit a37785a9d); codex-reviewed. 4 endpoints, perm→store→gate; codex caught + fixed CRITICAL IDOR (derive tenant from typeNum, validate userId/positionId), attach-store auth, a production-fatal webhook DI (EvereeTokenStorage private ctor), missing CSRF, exception-leak, loose validation; ProvisionResult DTO replaced the idempotency heuristic. 679 tests green.
  - Depends on T6, T7, T8, T9, T10, T11, T13.
  - [ ] T12.1 Prime Context
    - [ ] T12.1.1 Read SDD §"Internal API Changes" — all 5 endpoint specs `[ref: SDD §"Internal API Changes"; lines: 1186-1361]`
    - [ ] T12.1.2 Read `userfrosting/routes/api.php` and `userfrosting/routes/scheduling.php` for Slim 2 route conventions
  - [ ] T12.2 Write Tests
    - [ ] T12.2.1 Per-endpoint success + auth-denial + validation-error tests (one of each per endpoint, plus the cross-endpoint SchedulingProviderGate rejection from Scenario 3) `[activity: test]`
  - [ ] T12.3 Implement
    - [ ] T12.3.1 Author `BuyerKiosk\Payroll\Controllers\PayrollAdminController.php` `[activity: code]`
    - [ ] T12.3.2 Author `userfrosting/routes/api/payroll.php` route group: `POST /api/payroll/admin/:typeNum/tenants`, `POST /api/payroll/admin/:typeNum/rates`, `POST /api/payroll/admin/:typeNum/rates/retire`, `GET /api/payroll/admin/:typeNum/rates/as-of`, plus the webhook route from T10 `[activity: code]`
    - [ ] T12.3.3 Wire middleware order: UF session → `checkAccess('<perm>')` → `checkStoreGroup($typeNum)` → `SchedulingProviderGate::middleware()` (T13) → controller method `[activity: code]`
    - [ ] T12.3.4 All catch blocks use `catch (\Throwable $e)` not `catch (\Exception $e)` (CON-7 + PHP 8.5 gotcha); `\Slim\Exception\Stop` is allowed to propagate `[activity: code]`
  - [ ] T12.4 Validate
    - [ ] T12.4.1 Unit + integration tests green `[activity: run-tests]`
    - [ ] T12.4.2 Manual smoke test via browser/curl confirms 5 endpoints respond with documented shapes `[activity: business-acceptance]`

- [x] **T13** SchedulingProviderGate `[ref: PRD F11; lines: 208-212]` `[ref: SDD §"Service — SchedulingProviderGate"; lines: 1622-1633]` `[parallel: true]` — DONE (commit b20b543bf); codex-reviewed. Allowlist on `schedulingProvider === 'buyerkiosk'`, NEVER reads wiwEnable (source-scan test guards it). Fail-closed (null/malformed/load-failure/audit-failure all reject). Unblocks T9.
  - No service dependencies (reads Store + writes to PayrollAuditService). Parallel with T6/T7/T8/T11.
  - [ ] T13.1 Prime Context
    - [ ] T13.1.1 Read memory `buyerkiosk-wiw-exclusion-source-of-truth` AND the existing `Store::$schedulingProvider` accessor
    - [ ] T13.1.2 Read SDD §"Cross-Feature Edge Case F11" (mixed-provider stores under one EIN)
  - [ ] T13.2 Write Tests
    - [ ] T13.2.1 Test: BK-native store passes through `[activity: test]`
    - [ ] T13.2.2 Test: WIW store rejects (regardless of wiwEnable) `[activity: test]`
    - [ ] T13.2.3 Test: Homebase store rejects `[activity: test]`
    - [ ] T13.2.4 Test: null schedulingProvider rejects (fail-closed) `[activity: test]`
    - [ ] T13.2.5 Test: payroll.scheduling_provider_gate.rejected audit entry written with typeNum + schedulingProvider in metadata `[activity: test]`
  - [ ] T13.3 Implement
    - [ ] T13.3.1 Author `BuyerKiosk\Payroll\Exceptions\SchedulingProviderGateException.php` `[activity: code]`
    - [ ] T13.3.2 Author `BuyerKiosk\Payroll\Services\SchedulingProviderGate.php` with `assertAllowed(typeNum, attemptedAction, actorUserId)` AND `middleware(): callable` factory `[activity: code]`
    - [ ] T13.3.3 CRITICAL: gate reads `Store::$schedulingProvider === 'buyerkiosk'`, NOT `wiwEnable` — code review explicitly checks per memory `[activity: security-review]`
  - [ ] T13.4 Validate
    - [ ] T13.4.1 Unit tests green; reflection test verifies `wiwEnable` is NEVER referenced in the gate's read path `[activity: review-code]`

- [ ] **T14** Rate-backfill CLI tool `[ref: PRD F7 backfill AC; lines: 178]` `[ref: SDD §"Phase 0 non-schema deliverables — Deliverable 2"; lines: 1206-1218]` `[parallel: true]`
  - Depends on T8 (PayRateService); after T8 lands, can run in parallel with T9/T10/T12 follow-up work.
  - [ ] T14.1 Prime Context
    - [ ] T14.1.1 Read SDD §"Phase 0 non-schema deliverables — Deliverable 2" `[ref: SDD §"Deliverable 2"; lines: 1206-1218]`
    - [ ] T14.1.2 Read PRD F7 final AC ("one-time backfill admin tool ... CLI or scripted is acceptable") `[ref: PRD F7 AC final bullet; lines: 178]`
  - [ ] T14.2 Write Tests
    - [ ] T14.2.1 Test Scenario 26 — CLI processes CSV, produces audit entries, fails gracefully on per-row errors without aborting `[ref: SDD §Test Specifications Scenario 26]` `[activity: test]`
  - [ ] T14.3 Implement
    - [ ] T14.3.1 Author `userfrosting/bin/payroll/backfill-rates.php` — thin wrapper that constructs `PayRateService` via DI container and calls `setRate(...)` per CSV row; tracks per-row outcomes and prints a summary `[activity: cli-tool]`
    - [ ] T14.3.2 CLI help text warns: re-running produces additional rows by design (append-only model) `[activity: cli-tool]`
  - [ ] T14.4 Validate
    - [ ] T14.4.1 CLI exits 0 on success / non-zero iff any row failed validation `[activity: business-acceptance]`
    - [ ] T14.4.2 Integration smoke: a 3-row CSV creates 3 `payRateHistory` INSERTs + 3 `payroll.rate.set` audit entries; deliberately-malformed row produces error in CLI output but doesn't abort `[activity: run-tests]`

- [ ] **T15** Live sandbox smoke test (partner-gated; OPERATIONAL phase, tracked SEPARATELY) `[ref: PRD F5 AC; lines: 156-158]`
  - **NOT BLOCKING Phase 1a sign-off** — only blocks the partner-gated rollout step. Tracked here so the team doesn't forget it. Operational phase — no test-first TDD shape; the scripted smoke test IS the test.
  - [ ] T15.1 Prime Context
    - [ ] T15.1.1 Confirm Everee sandbox credentials are provisioned (open §13 partner item)
    - [ ] T15.1.2 Confirm `EVEREE_API_BASE_URL` is the sandbox URL in `.env`
  - [ ] T15.2 Execute the scripted smoke test
    - [ ] T15.2.1 Provision a sandbox tenant via `/api/payroll/admin/<typeNum>/tenants` end-to-end: assert HTTP 201 + `isActive=1` after verify-reachable round-trip `[activity: business-acceptance]`
    - [ ] T15.2.2 List workers (expect empty); create a test worker via `EvereeApiClient::createWorker(...)`; verify the corresponding `worker.created` webhook arrives within 60s and is persisted with `hmacValid=1` `[activity: business-acceptance]`
    - [ ] T15.2.3 Re-send the same webhook event ID; verify dedupe (HTTP 200 + `duplicate: true`) `[activity: business-acceptance]`
    - [ ] T15.2.4 Set an initial pay rate via the new endpoint; query as-of two dates; verify both resolve correctly without any UPDATE having touched history `[activity: business-acceptance]`
  - [ ] T15.3 Validate
    - [ ] T15.3.1 Smoke test passes against the live sandbox — CS lead signs off as demo-ready `[activity: business-acceptance]`
    - [ ] T15.3.2 If smoke test fails: open a partner conversation; do NOT trigger Phase 1a re-do (per PRD F5 AC) `[activity: partner-coordination]`

- [ ] **T16** Inter-spec cleanup + env vars + ngrok docs (housekeeping; T17 gate) `[parallel: true]`
  - Operational phase — no Prime/Test/Implement/Validate TDD shape; it's pure config + docs. Marked here so T17 can verify against it.
  - [ ] T16.1 Add ALL SIX new env vars to `userfrosting/.env.example` with comments:
    - `EVEREE_ENCRYPTION_KEY` (32-byte hex master key; required; per ADR-1)
    - `EVEREE_API_BASE_URL` (sandbox URL; required for non-no-op calls; CON-14 forbids prod)
    - `EVEREE_PARTNER_API_TOKEN` (partner-level token for the initial Company Instance create only; per ADR-11)
    - `EVEREE_WEBHOOK_SIGNING_MODE` (default `per_tenant`; alternative `global`; per ADR-3)
    - `EVEREE_WEBHOOK_GLOBAL_SECRET` (only when signing mode = `global`)
    - `EVEREE_WEBHOOK_TIMESTAMP_TOLERANCE_SEC` (default 300; replay-defense window)
  - [ ] T16.2 Confirm no entry needed in `docs/api/mobile-agent-requests.md` or `docs/api/live-agent-requests.md` per SDD §"Multi-Component Coordination" (no mobile-facing changes in Phase 0/1a)
  - [ ] T16.3 Update CLAUDE.md cheat-sheet with new commands: `php userfrosting/bin/payroll/backfill-rates.php --csv=... --tenant-id=... --actor-user-id=...` (T14) and `php userfrosting/bin/payroll/verify-cross-merchant-users.php` (T3)
  - [ ] T16.4 Validate
    - [ ] T16.4.1 `grep -c "EVEREE_" userfrosting/.env.example` ≥ 6 `[activity: review-code]`
    - [ ] T16.4.2 New CLI commands are discoverable from CLAUDE.md `[activity: review-code]`

### TIER: FINAL (T17)

- [ ] **T17** Integration & End-to-End Validation (OPERATIONAL phase — no Prime/Test/Implement TDD shape; pure verification)
  - [ ] T17.1 All unit tests passing across `tests/Unit/Payroll/**` `[activity: run-tests]`
  - [ ] T17.2 All integration tests passing across `tests/Integration/Payroll/**` (Scenario 16 dedupe round-trip + Scenario 6 GuardedPdo round-trip + Scenario 21 isolated-failure round-trip + Scenario 26 backfill CLI round-trip) `[activity: run-tests]`
  - [ ] T17.3 Coverage report shows no untested public-method paths in `BuyerKiosk\Payroll\Services\*` AND `BuyerKiosk\Payroll\Repositories\*` `[ref: SDD §"Test Coverage Requirements"]` `[activity: run-tests]`
  - [ ] T17.4 PHPStan analyses clean at baseline level: `cd userfrosting && ./vendor/bin/phpstan analyse src/BuyerKiosk/Payroll/` `[activity: run-tests]`
  - [ ] T17.5 Performance targets PERF-1..5 measured via FIXTURE/SANDBOX runs (NOT production). Record measured p95 per metric into the README review log; production monitoring is tracked post-merge via the audit log + application logs `[ref: SDD §Quality Requirements; PERF-1..5]` `[activity: business-acceptance]`
  - [ ] T17.6 Security validation:
    - [ ] T17.6.1 Token-leak CI check has been exercised at least once with an intentional-leak fixture (added then removed) `[activity: security-review]`
    - [ ] T17.6.2 HMAC-bad fixture rejected with 401 `[activity: security-review]`
    - [ ] T17.6.3 SchedulingProviderGate rejects WIW + Homebase store fixtures `[activity: security-review]`
    - [ ] T17.6.4 SEC-6 no-DELETE scan: `grep -rEn 'DELETE FROM (payRateHistory|payrollAuditLog|payrollRuns|payrollRunLines)' userfrosting/src/BuyerKiosk/Payroll/` returns ZERO hits `[activity: security-review]` `[ref: SDD §Quality Requirements SEC-6]`
    - [ ] T17.6.5 Catch-block scan across the entire payroll module: `grep -rn 'catch (\\Exception' userfrosting/src/BuyerKiosk/Payroll/` returns ZERO hits `[activity: security-review]`
    - [ ] T17.6.6 PII-redactor evidence: a fixture payload containing forbidden keys passes through the handler with redaction; production code path traces through `EvereeWebhookPayloadRedactor::redact(...)` before any DB INSERT `[activity: security-review]` `[ref: ADR-11]`
  - [ ] T17.7 PRD F1-F13 acceptance-criteria audit — one row PER AC (not per feature), filled into the Spec Compliance Audit table below. Each AC needs owner + evidence link. `[ref: PRD §"Feature Requirements"]` `[activity: business-acceptance]`
  - [ ] T17.8 Test coverage ≥ project's current bar (no regression) `[activity: run-tests]`
  - [ ] T17.9 Documentation:
    - [ ] T17.9.1 All 5 NEW `docs/patterns/*.md` files exist `[activity: review-code]`
    - [ ] T17.9.2 NEW `docs/interfaces/everee-api.md` exists `[activity: review-code]`
    - [ ] T17.9.3 NEW `docs/specs/050-everee-payroll-foundations/person-centric-account-verification.md` (PRD F2) AND `phase-0-preflight-checklist.md` (PRD F13) exist `[activity: review-code]`
    - [ ] T17.9.4 SDD's directory map matches the actual filesystem (sample 10 random entries, verify file paths exist) `[activity: review-code]`
  - [ ] T17.10 Build + deployment verification:
    - [ ] T17.10.1 `php userfrosting/conductor run` is a no-op on a freshly-deployed environment (REL-3 / REL-5 idempotency invariant) `[activity: run-tests]`
    - [ ] T17.10.2 All 16 migrations have entries in `kiosk_buykiosk.migration_log` with `status='applied'` `[activity: review-code]`
  - [ ] T17.11 All PRD requirements implemented and verified against the SDD (each ACs in T17.7 ticked) `[activity: business-acceptance]`
  - [ ] T17.12 SDD's 11 ADRs all match the implemented behavior; any deviation is documented in `## Deviation Log` (this file) `[activity: review-code]`
  - [ ] T17.13 SDD test-suite items verified:
    - [ ] T17.13.1 `ProcessEvereeWebhookJobTest.php` exists and is green `[activity: run-tests]`
    - [ ] T17.13.2 Webhook fixture files exist: one per enumerated event type per T10.2.10 `[activity: review-code]`
    - [ ] T17.13.3 Integration test verifies `JobDispatcher::dispatch` is called by `EvereeWebhookHandler::ingest` with the correct payload + idempotencyKey `[activity: run-tests]`
    - [ ] T17.13.4 Every payroll endpoint has a green: success test + auth-denial test + validation-error test `[activity: run-tests]`
    - [ ] T17.13.5 REL-2 evidence: zero duplicates in `payrollWebhookEvents.evereeEventId` (verified by `SELECT evereeEventId, COUNT(*) FROM payrollWebhookEvents GROUP BY evereeEventId HAVING COUNT(*) > 1`) `[activity: review-code]`
    - [ ] T17.13.6 REL-6 evidence: `TokenDecryptionException` raised in unit tests; NO fallback path returns plaintext `[activity: security-review]`
  - [ ] T17.14 README updated: spec status → "Phase 1a foundations merged; ready for Phase 1b kickoff" `[activity: doc-write]`

---

## Dependency Summary

Critical path:
```
T1 (Employee consolidation) ─┐
T2 (Schema migrations) ──────┤
T3 (Person-centric verify) ──┼─► T6 + T11 + T13 ─┐
T4 (TaskEngine audit) ───────┤                    ├─► T7, T8 (parallel) ─► T9 ─► T10 ─► T12 ─► T17
T5 (Pre-flight checklist) ───┘                    └─► T14 (after T8)
                                                      T15 (after T7+T9+T10, partner-gated; not blocking Phase 1a sign-off)
                                                      T16 (parallel anywhere; gate for T17)
```

Concretely:
- **Phase 0 (T1-T5)** must merge sequentially in the order T1 → T2 → T3 → T4, with T5 parallel-OK alongside.
- **T6, T11, T13** can run fully in parallel as soon as Phase 0 is in master (no service dependencies among them).
- **T7** depends on T6 + T11 (the API client audits every Everee call).
- **T8** depends on T2 + T11.
- **T9** depends on T6 + T7 + T11 + T13 (provisioning calls the SchedulingProviderGate before any store attach).
- **T10** depends on T2 + T6 + T9 (PayrollTenantRepository for tenant resolution) + T11 + **T4 (hard gate per PRD F12)**.
- **T12** depends on T6 + T7 + T8 + T9 + T10 + T11 + T13.
- **T14** depends on T8 (`[parallel: true]` once T8 lands — can run alongside T9/T10/T12 follow-up work).
- **T15** depends on T9 + T10 (live partner connectivity). NOT blocking Phase 1a sign-off (PRD F5 AC).
- **T16** parallel anywhere; gate for T17 since env-vars + CLAUDE.md cheat-sheet must be in place before final acceptance.
- **T17** depends on T6-T14 merged AND T16 merged (T15 is not a gate for T17 per PRD F5).

---

## Spec Compliance Audit (filled out at T17.7 — 2026-06-02)

**Audit performed by:** T17 compliance pass, branch `050-everee-payroll-foundations`
**Headline counts:** 68 ACs total — 57 ✅ met / 4 ⚠️ partial / 7 ☐ human-gated/not-done

One row PER PRD acceptance criterion (not per feature). Evidence is committed code or real test output; no estimates.

> **Legend:** ✅ met (committed code + passing test evidence) · ⚠️ partial (code done, verification incomplete or secondary gap) · ☐ not done / human-gated (cannot be done by engineering alone)

| PRD Feature | AC (abbreviated) | Phase | Test / Evidence | Commit | Status |
|---|---|---|---|---|---|
| F1 | All Core\Employee refs resolved/migrated | T1.3 | `grep -rn 'BuyerKiosk\\Core\\Employee'` returns only `LegacyAliases.php` line 32-34; `Core/Employee.php` deleted; `EmployeeAliasTest.php` green | f429d715b | ✅ |
| F1 | Test suite green, no regressions | T1.4.1 | `./vendor/bin/phpunit tests/Unit/Payroll tests/Integration/Payroll` → 721/721 pass; PHPStan `[OK] No errors` | f429d715b | ✅ |
| F1 | Compatibility alias retained (`LegacyAliases.php` line 34 maps to `Employee\Employee`) | T1.3.1 | `LegacyAliases.php:34` `'Employee' => \BuyerKiosk\Employee\Employee::class` verified; `EmployeeAliasTest` covers it | f429d715b | ✅ |
| F1 | PHPStan at baseline | T1.4.2 | `phpstan analyse src/BuyerKiosk/Payroll/` → `[OK] No errors` (also whole-module clean per T17) | f429d715b | ✅ |
| F1 | No Phase 1a work proceeds until merged (hard gate CON-21) | T1 (gate) | T1 is first commit on branch (f429d715b); all Phase 1a commits follow it | f429d715b | ✅ |
| F2 | SQL query shows ≥1 user with ≥2 distinct typeNum assignments | T3.3.1 | `bin/payroll/verify-cross-merchant-users.php` shipped (commit 5eceaf15e); dev-DB run found userId=28 at ou00+pc00; `Scenario25PersonCentricVerificationTest` asserts CLI file exists | 5eceaf15e | ✅ |
| F2 | Dev-store walkthrough of cross-merchant hire / CON-22 gap remediated | T3.3.3 | `findExistingUserByContact` + `assignExistingUserToStore` built and codex-reviewed (commit 81759f742); `person-centric-account-verification.md` documents the CON-22 finding + remediation path | 81759f742 | ✅ |
| F2 | Remediation plan documented (gap found → CON-22 remediated in phase) | T3.3.4 | `person-centric-account-verification.md §3` states gap found, remediation implemented in same branch | 81759f742 | ✅ |
| F2 | Sign-off by another engineer (second-engineer formality) | T3.4.1 | `person-centric-account-verification.md §4` has blank `Signed-off-by: ___` line — UNFILLED | 81759f742 | ☐ |
| F3 | All 19 migration JSONs apply cleanly + idempotent | T2.4.1 | `PayrollMigrationIdempotencyTest` green; live apply across 7 dev stores confirmed (commit 03f06baa9 notes) | 24c1d7467 + 03f06baa9 | ✅ |
| F3 | 12 new tables created (payrollTenants, payrollRuns, payrollRunLines, payrollRunSnapshots, payrollWebhookEvents, payrollAuditLog, payRateHistory, payrollCoaMappings, ptoAccrualPolicies, ptoAccrualBalances, ptoRequests, userPayrollProfiles) | T2.3.1–T2.3.8 | Migrations 001–008 create all 12 tables; `PayrollMigrationIdempotencyTest` validates each table name; confirmed via live conductor run notes | 24c1d7467 | ✅ |
| F3 | `payrollTenants.webhookSecretEncrypted` column exists | T2.3.1 | Migration 001 SQL: `webhookSecretEncrypted VARCHAR(512) NULL` (widened to 1024 by migration 017); `webhookPriorSecretEncrypted` + `webhookPriorSecretExpiresAt` also present (ADR-11 rotation) | 24c1d7467 | ✅ |
| F3 | `stores.payrollTenantId` nullable FK | T2.3.10 | Migration 010 `ADD COLUMN payrollTenantId INT UNSIGNED NULL` + FK to `payrollTenants(id)` | 24c1d7467 | ✅ |
| F3 | `kiosk_users.users` extended with PII columns per source-of-truth model | T2.3.9 | Migration 009 adds legalFirstName, legalLastName, dob, addressLine1, addressLine2, city, state, zip, phoneE164; each `description` carries a PII-category label (CON-18) | 24c1d7467 | ✅ |
| F3 | NO `annualSalaryCents` on users | T2 (negative-space) | Searched migration 009 SQL — no such column; `payRateHistory` is the canonical store (rate_type=salary_annual) | 24c1d7467 | ✅ |
| F3 | `userStoreAssignments.employmentClassification` enum | T2.3.11 | Migration 011 `ADD COLUMN employmentClassification ENUM('w2_hourly','w2_salaried') NULL DEFAULT NULL` | 24c1d7467 | ✅ |
| F3 | `schedulePositions.workersCompClassCode` + `qboWageAccountId` (per-store, deviation from SDD) | T2.3.12 | Migration 012 targets per-store `schedulePositions` table (deviation logged 2026-05-29 — SDD said `kiosk_buykiosk.positions` which doesn't exist) | 03f06baa9 | ✅ |
| F3 | `scheduleTimePunches.cashTipsCents/creditTipsCents/submittedToEvereeAt` | T2.3.13 | Migration 013 adds three nullable columns; descriptions note "INT NULL (cents, never float)" and "placeholder per CON-19" | 24c1d7467 | ✅ |
| F3 | INT cents for money, DECIMAL for hours, no FLOAT/DOUBLE | T2 (review) | Audit of all 19 payroll migrations: no FLOAT/DOUBLE found in any column definition; monetary columns use INT/BIGINT; hours use DECIMAL(8,4) | 24c1d7467 | ✅ |
| F3 | `payRateHistory` composite index + UPDATE guard (GuardedPdo + DB trigger) | T2.3.5 + T8.3.3 | Migration 005 creates composite index `(userId, payrollTenantId, positionId, effectiveFrom DESC)`; migration 019 creates BEFORE UPDATE + BEFORE DELETE triggers; `GuardedPdo` blocks SQL-level attempts | 24c1d7467 + 793b81c0a | ✅ |
| F3 | punchType ENUM pre-flight verification recorded | T2.3.14 | Migration 014 is an observability-only "schema-verification-note" (renamed per Codex); description confirms finding | 24c1d7467 | ✅ |
| F3 | display_name → legal-name helper committed + idempotent (deviation: uses firstName/lastName as primary source) | T2.3.15 | Migration 015 uses existing structured `firstName`/`lastName` columns as primary source (deviation logged 2026-05-29 — SDD used non-existent `display_name`); three idempotent UPDATEs | 03f06baa9 | ✅ |
| F3 | migration_log records each operation | T2.4.5 | `PayrollMigrationIdempotencyTest` and `PayrollPermissionKeysTest` both verify migration_log state; confirmed live (all 19 ops in central migration_log) | 24c1d7467 | ✅ |
| F3 | Schema review confirms no conflict with 32 locked decisions | T2.4 (review) | Reviewed in T2 PR (commit notes + Deviation Log); two SDD schema-name drifts corrected (012, 015) | 03f06baa9 | ✅ |
| F4 | Tokens encrypted via AES-256-CBC + encrypt-then-MAC envelope (deviation over ADR-1 raw CBC) | T6.3.3 | `EvereeTokenStorage.php` implements `v1:<ciphertext>:<HMAC-SHA256>` envelope; `EvereeTokenStorageTest` covers roundtrip, tamper-detection, verify-before-decrypt | c6d3d1f16 | ✅ |
| F4 | Token-leak regression test green (var_dump, json_encode, exception stringification) | T6.2.1 | `EvereeTokenStorageTest::testNoPlaintextInSerializedOutput` + stack-trace arg scan test; `check-token-leaks.php` → `[PASS]` | c6d3d1f16 + e199885cf | ✅ |
| F4 | Automated CI check (grep-based scanner) flags direct token logging | T6.3.6 | `bin/payroll/check-token-leaks.php` exists; `token_leak_fixture.php` / `token_clean_fixture.php` in test fixtures; `EvereeTokenStorageTest::testTokenLeakCiCheckWorks` verifies scanner fails on leak fixture and passes on clean | c6d3d1f16 | ✅ |
| F4 | Decrypt only via authorized service class; no controller direct read | T6.3.3 | `PayrollTenant::getEncryptedApiTokenForServiceUse()` returns ciphertext only; decrypt lives in `EvereeTokenStorage`; `PayrollAdminController` never calls decrypt directly | c6d3d1f16 | ✅ |
| F4 | Rotation steps documented (not implemented) | T6.3.5 | `docs/patterns/payroll-token-encryption.md` exists at repo root AND `userfrosting/docs/patterns/payroll-token-encryption.md` (path inconsistency — see Gap List); rotation procedure section present | c6d3d1f16 | ⚠️ |
| F5 | HTTP Basic auth + `x-everee-tenant-id` headers per call | T7.3.2 | `EvereeApiClient.php:125,154,171` uses per-tenant Basic auth + x-everee-tenant-id; partner-token path (ADR-11) omits tenant-id header by design | b546320e6 | ✅ |
| F5 | Configurable base URL via `EVEREE_API_BASE_URL` | T7.3.2 | `EvereeApiClient::__construct` accepts `$baseUrl`; injected from config; `EVEREE_API_BASE_URL` in `.env.example` with sandbox default | b546320e6 | ✅ |
| F5 | 5xx retry with exponential backoff + documented cap | T7.3.2 | `EvereeApiClientTest` Scenario 9 (5xx-then-200 replay); retry logic in `_request`; max-retries documented in client constants | b546320e6 | ✅ |
| F5 | 429 `Retry-After` honored once; absent header falls back to exponential backoff (PRD prevails over SDD — deviation 2026-06-01b) | T7.3.2 | `EvereeApiClientTest` Scenario 10 + `testScenario10bRateLimitWithoutRetryAfterFallsBackToExponentialBackoff` | b546320e6 | ✅ |
| F5 | 401/403 (auth) distinct from 4xx validation exceptions | T7.3.1 | `EvereeAuthException` vs `EvereeValidationException` in exception hierarchy; Scenarios 11 + 11d cover both paths | b546320e6 | ✅ |
| F5 | Structured exception types for non-retryable errors | T7.3.1 | 5 exception classes: `EvereeApiException`, `EvereeAuthException`, `EvereeRateLimitException`, `EvereeValidationException`, `EvereeUncertainStateException` all exist | b546320e6 | ✅ |
| F5 | Idempotency-Key on POST endpoints (best-effort; ADR-7) | T7.3.2 | `EvereeApiClientTest` Scenario 11c verifies `Idempotency-Key` header sent on POST; documented residual risk if partner doesn't honor | b546320e6 | ✅ |
| F5 | Phase 1a sign-off gate: fixture-replay suite green, zero outbound network | T7.4.1 | 709 unit tests green including all Scenarios 9–12, 11b–11d; Guzzle `MockHandler` used (no real HTTP); `EvereeApiClientTest` confirmed | b546320e6 | ✅ |
| F5 | Sandbox smoke test (partner-gated; T15) | T15.2 | BLOCKED — requires real Everee sandbox credentials (open §13 partner item). Not a Phase 1a code-sign-off blocker per PRD F5 AC | — | ☐ |
| F6 | Service method creates tenant + Everee Company Instance (or `provisionManually` portal seam) | T9.3.2 | `EvereeProvisioningService::provisionTenant` + `provisionManually`; `EvereeProvisioningServiceTest` Scenario 1 + manual path test green | 567d1d0d3 | ✅ |
| F6 | Token + IDs stored encrypted; `isActive=0` until verified-reachable | T9.3.2 | `EvereeProvisioningServiceTest::testPartialProvisioningKeepsIsActiveZero` (bank-not-verified test); `isActive=1` only after `listWorkers` round-trip succeeds | 567d1d0d3 | ✅ |
| F6 | 1:N tenant ↔ stores supported | T9.2.5 | `EvereeProvisioningServiceTest::testMultiStoreAttach`: two typeNums share one `payrollTenantId`; two audit entries written | 567d1d0d3 | ✅ |
| F6 | Idempotent by EIN | T9.2.2 | `EvereeProvisioningServiceTest` Scenario 2; second call returns existing tenant + writes `metadata.idempotent=true` audit entry | 567d1d0d3 | ✅ |
| F6 | Partial provisioning leaves `isActive=0` (CrossFeatureEdgeCase F6) | T9.2.4 | `EvereeProvisioningServiceTest::testPartialProvisioningKeepsIsActiveZero` — Everee returns 200 but verification fails → isActive stays 0 | 567d1d0d3 | ✅ |
| F7 | `setRate` is exactly one INSERT + one audit entry | T8.3.5 | `PayRateServiceTest` Scenario 4; INSERT-count assertion + audit-entry count = 1 | 793b81c0a | ✅ |
| F7 | `getRate(asOf)` returns greatest-effectiveFrom row; tombstone returns null | T8.3.5 | `PayRateServiceTest` Algorithm 1 tests + `PayRateEntryTest::testGetRateHonorsTombstoneAtBoundary`; boundary `asOf == effectiveUntil` returns null | 793b81c0a | ✅ |
| F7 | `retireRate` is INSERT-only (tombstone row) | T8.3.5 | `PayRateServiceTest` Scenario 8; tombstone INSERT verified; `rateType` copied from active row per Codex fix | 793b81c0a | ✅ |
| F7 | `listHistory` grouped by position, reverse-chronological | T8.3.5 | `PayRateServiceTest::testListHistoryGroupedByPosition` covers per-position map + reverse-chron ordering | 793b81c0a | ✅ |
| F7 | UPDATE attempts on `payRateHistory` raise `PayRateImmutableException` | T8.3.3 | `GuardedPdoTest` + `RateHistoryAppendOnlyTest` (integration); DB-level trigger (migration 019) also tested; `PayRateHistoryRepository` has zero public `update`/`delete` methods (verified by reflection test) | 793b81c0a | ✅ |
| F7 | `hourly` + `salary_annual` rate types both supported | T8.3.5 | `PayRateServiceTest` contains tests with both rateType values; `PayRateEntry::RATE_TYPE_*` constants defined | 793b81c0a | ✅ |
| F7 | One-time backfill CLI tool ships | T14.3.1 | `bin/payroll/backfill-rates.php` exists; `Scenario26RateBackfillRunnerTest` covers happy-path + per-row error without abort + exit-code contract | e10c644d7 | ✅ |
| F8 | Single public endpoint `POST /api/payroll/webhook/everee` | T10.3.10 | `routes/api/payroll.php:57` registers the route; `EvereeWebhookControllerTest` covers route handler | aea35c21e | ✅ |
| F8 | HMAC verified on every request; failures return 401 (NOT 5xx) | T10.3.5 | `EvereeWebhookHandlerTest` Scenario 17 (bad HMAC → 401); `EvereeWebhookController` catches `WebhookSignatureException` → 401 JSON | b981294ce | ✅ |
| F8 | UNIQUE `evereeEventId` enforces dedupe | T2.3.3 + T10.3.5 | Migration 003 creates `UNIQUE KEY uq_everee_event_id (evereeEventId)`; `EvereeWebhookHandlerTest` Scenario 16 (concurrent duplicate) + `PayrollWebhookEventRepositoryTest` cover this | b981294ce | ✅ |
| F8 | Successful events queued via TaskEngine job (`ProcessEvereeWebhookJob`) | T10.3.8 | `ProcessEvereeWebhookJob` exists and registered (migration 018); `EvereeWebhookHandlerTest::testJobDispatchedAfterSuccessfulIngest`; `ProcessEvereeWebhookJobTest` covers all branches | aea35c21e | ✅ |
| F8 | All 11 enumerated events ingest; unknown event types also ingest (no ingestion rejection) | T10.2.9 | 11 HMAC-signed fixture files under `tests/Fixtures/Payroll/everee-webhook-*.json`; `EvereeWebhookHandlerTest` all-11-events matrix test + unknown-event-type test; `ProcessEvereeWebhookJobTest` covers all branches | aea35c21e | ✅ |
| F8 | Per-event processing failure doesn't block subsequent events | T10.3.8 | `EvereeWebhookHandlerTest` Scenario 21: job failure on event N does not prevent event N+1 from being inserted/queued | aea35c21e | ✅ |
| F8 | SQL inspection supported (no structured admin UI required in this phase) | T10.3.5 | `payrollWebhookEvents` table queryable directly; no admin UI built (correctly deferred per PRD F14 Could-Have) | 24c1d7467 | ✅ |
| F8 | HMAC supports per-tenant OR global secret via `EVEREE_WEBHOOK_SIGNING_MODE` env switch | T10.3.5 + ADR-3 | `EvereeWebhookHandler` reads `EVEREE_WEBHOOK_SIGNING_MODE`; both paths covered in `EvereeWebhookHandlerTest`; `EVEREE_WEBHOOK_GLOBAL_SECRET` in `.env.example` | b981294ce | ✅ |
| F8 | Timestamp tolerance enforced (default 5 min; configurable) | T10.3.5 | `EvereeWebhookHandlerTest` Scenario 18: stale timestamp → 401; `EVEREE_WEBHOOK_TIMESTAMP_TOLERANCE_SEC` in `.env.example`; dedup slot NOT consumed on timestamp-rejected request | b981294ce | ✅ |
| F8 | Secret rotation procedure documented | T10.3.11 | `docs/patterns/payroll-webhook-hmac-rotation.md` exists at repo root with rotation procedure | b981294ce | ✅ |
| F9 | `payrollAuditLog` captures actor, action, before/after JSON, timestamp | T11.3.2 | `PayrollAuditRepository` + `PayrollAuditEntry` model; `PayrollAuditTest` covers all 10 `log*` methods; `payrollAuditLog` schema in migration 004 mirrors `scheduleAuditLog` shape | 3dd0b07ba | ✅ |
| F9 | Tenant prov / rate change / webhook outcomes all route through audit | T11.3.3 | `EvereeProvisioningService`, `PayRateService`, `EvereeWebhookHandler` each call `PayrollAuditService` log methods; verified by per-service unit tests | 3dd0b07ba | ✅ |
| F9 | Mirrors `scheduleAuditLog` shape | T2.3.4 + T11.3.1 | Migration 004 description: "mirrors scheduleAuditLog shape — same actor/action/before/after/createdAt columns"; `PayrollAuditEntry` mirrors `ScheduleAuditEntry` structure | 24c1d7467 | ✅ |
| F10 | 12 permission keys defined in `uf_authorize_group` | T2.3.16 | Migration 016 inserts all 12 keys (confirmed by python parse: approve_pay_run, approve_pay_run_above_threshold, approve_pto_request, create_punch_adjustment, kickoff_employee_onboarding, manage_payroll, request_pto, set_pay_rate, submit_pay_run, terminate_employee, view_own_earnings, view_pay_run); `PayrollPermissionKeysTest` validates | 24c1d7467 | ✅ |
| F10 | Default role mapping matches locked decision (Owner=all manage; Manager=submit+onboard+pto+terminate+adjustment; ShiftLead=view; Employee=own-earnings+own-PTO) | T2.3.16 | `PayrollPermissionKeysTest` Scenario 23 verifies per-role grants match PRD F10 locked decision | 24c1d7467 | ✅ |
| F10 | No UI consumes these permissions yet | T2 + review | No new Twig template references to payroll permission keys; admin controller uses standard `checkAccess` which is pre-existing pattern; PRD explicitly scopes this as "register only" | 24c1d7467 | ✅ |
| F11 | Middleware rejects payroll ops on `schedulingProvider != 'buyerkiosk'` (fail-closed including null/malformed) | T13.3.2 | `SchedulingProviderGate::assertAllowed` + `::middleware()`; reads `Store::$schedulingProvider` EXCLUSIVELY (never `wiwEnable`); reflection test confirms no `wiwEnable` reference in gate read path | b20b543bf | ✅ |
| F11 | Rejection references exclusion decision; covers WIW + Homebase fixtures | T13.2.2–T13.2.3 | `SchedulingProviderGateTest` covers: BK-native pass, WIW reject, Homebase reject, null reject, audit-entry written with typeNum + provider in metadata | b20b543bf | ✅ |
| F12 | Audit document exists under `docs/patterns/` | T4.3.1 | `docs/patterns/taskengine-payroll-audit.md` exists (confirmed) | c0e1074eb | ✅ |
| F12 | In-phase fix vs Phase-1c defer recommendation per gap | T4.3.1 | Audit doc contains gap analysis: add `payroll` queue in Phase 1a (T10), defer reconciliation + PTO accrual to Phase 1c | c0e1074eb | ✅ |
| F12 | Reviewed by another engineer (`Signed-off-by:`) | T4.4.1 | `docs/patterns/taskengine-payroll-audit.md` has blank `Signed-off-by: ___` — UNFILLED | c0e1074eb | ☐ |
| F12 | Hard gate: T10 async-AC did not merge before audit | T4 → T10 dep | T4 commit (c0e1074eb) precedes T10 commits (b981294ce, aea35c21e) in branch history | c0e1074eb | ✅ |
| F13 | Pilot-store rate-data audit complete (5 stores, CSVs) | T5.3.3 | `phase-0-preflight-checklist.md` exists but all 5 store rate items show `[ ] PENDING HUMAN ACTION` — CS has not filled these in | 5d1df2f14 | ☐ |
| F13 | Partner-manager kickoff email sent + thread tracked weekly | T5.3.2 | Checklist scaffold + draft email text authored; actual send + thread tracking requires CS action — UNFILLED | 5d1df2f14 | ☐ |
| F13 | Pilot candidate list finalized | T5.3.1 | Checklist has Item 3 placeholder `<TBD>` — UNFILLED | 5d1df2f14 | ☐ |
| F13 | Owner of each item identified by name | T5.3.1 | All checklist items show `<CS — TBD: assign a named CS team member>` — UNFILLED | 5d1df2f14 | ☐ |
| F13 | Engineering review + confirms no Phase 1a Must-Have requires unticked item | T5.4.1 | Scaffold present with placeholder for engineering sign-off — UNFILLED. Note: engineering CAN confirm that the Phase 1a code artifacts (T6–T14) do not depend on any F13 checklist item being completed; sandbox smoke test (T15) is the only Phase-1a item that is CS/partner-gated. | 5d1df2f14 | ⚠️ |
| ADR-11 | PII redactor strips SSN/bank/W-4/I-9 keys before DB INSERT | T10.3.4 | `EvereeWebhookPayloadRedactor::redact()` + `EvereeWebhookPayloadRedactorTest`; high-severity log on every redaction; verified by `EvereeWebhookHandlerTest::testPiiRedactionFiresBeforeInsert` | b981294ce | ✅ |
| ADR-11 | Partner-level auth for `createCompanyInstance` via `EVEREE_PARTNER_API_TOKEN` | T9.4.3 | `EvereeApiClient::createCompanyInstance` uses partner token (no tenant-id header); `EvereeProvisioningServiceTest::testProvisionManuallyPath` covers this; `EVEREE_PARTNER_API_TOKEN` in `.env.example` | 567d1d0d3 | ✅ |
| ADR-11 | Per-tenant prior-secret columns support rotation window | T2.3.1 + T10.3.5 | Migration 001 creates `webhookPriorSecretEncrypted` + `webhookPriorSecretExpiresAt`; `EvereeWebhookHandler` accepts signatures under either current or prior secret while expiry > now(); Scenario 19 (rotation tolerance) green | b981294ce | ✅ |

---

### Audit Note — `docs/patterns/` path inconsistency

The implementation plan specified all pattern docs under `docs/patterns/` (repo root). In practice:
- `docs/patterns/` (repo root) contains: `payroll-token-encryption.md`, `payroll-webhook-hmac-rotation.md`, `payroll-permission-key-cross-walk.md`, `taskengine-payroll-audit.md`
- `userfrosting/docs/patterns/` contains: `payroll-rate-history-append-only.md`, `payroll-token-encryption.md` (duplicate)
- `userfrosting/docs/interfaces/` contains: `everee-api.md`

The `payroll-rate-history-append-only.md` and `everee-api.md` are accessible but live under the gitignored `userfrosting/docs/` subtree rather than the repo-root `docs/` tree. The F4 rotation-doc AC is marked ⚠️ partial because of this duplicate/split: both locations have valid content but the canonical location is inconsistent. See Gap List below.

## Deviation Log

### 2026-05-26 — T1 Employee class consolidation: helper method + Buy.php rewrite

**Source of truth for the change**: this entry (Plan deviation, approved before implementation).

**Context**

The PLAN's T1.3.1 ("flip the alias map to point at `BuyerKiosk\Employee\Employee::class`") and T1.3.4 ("reduce `Core\Employee.php` to `class Employee extends \BuyerKiosk\Employee\Employee {}` OR delete the file outright") both assume the two classes share a substitutable shape. Inspection found they do not:

- `BuyerKiosk\Core\Employee` extended `Store`, took a two-arg constructor `($oldEmployeeId, $typeNum)`, and ran a SELECT against `kiosk_users.userStoreAssignments` joined to `kiosk_users.users` to populate `id`/`firstName`/`lastName`/`active`.
- `BuyerKiosk\Employee\Employee` is a pure value object. No constructor; static `fromRow(array)` factory; an extensive field set (photo, hire/term/leave dates, source, externalId, role color, etc.). It does **not** query the database.

A naive alias flip would leave `userfrosting/src/BuyerKiosk/Core/Buy.php:81,87` calling `new \Employee($id, $typeNum)` against a class with no usable constructor — the two arguments would be silently discarded and every downstream `->getId()` / `->getFirstName()` access would hit a `TypeError` on an uninitialized typed property.

Independently, T1.3.2 ("update LegacyAliases.php line 46 (`EmployeeDaily`) and other `Employee*` entries to mirror") had no targets: `EmployeeDaily` is a daily-stats aggregate extending `Store` (no counterpart under `BuyerKiosk\Employee\`), and no other `Employee*` entries exist in `$legacyClassMap`.

**Decision**

1. Add a migration helper `BuyerKiosk\Employee\Employee::fromLegacyEmployeeId(?int $oldEmployeeId, string $typeNum, ?\PDO $db = null): ?self` that encapsulates the same SELECT the retired stub ran in its constructor and returns a populated value object (or `null` on miss / invalid input).
2. Rewrite `Core/Buy.php` callsites at lines 81 and 87 to call `Employee::fromLegacyEmployeeId(...)` directly. Behaviour preserved: same query, same identity (`userStoreAssignments.oldEmployeeId` per-store legacy id), same downstream getters used by `templates/themes/default/buys/buy.html`.
3. Flip `LegacyAliases.php` line 32 (`'Employee' => …Employee\\Employee::class`).
4. Delete `userfrosting/src/BuyerKiosk/Core/Employee.php` (T1.3.4's "delete outright" branch — grep returns zero non-Compatibility imports of `BuyerKiosk\\Core\\Employee`).
5. Skip T1.3.2 — no targets exist. `EmployeeDaily` stays mapped to `Core\EmployeeDaily` (its actual canonical location).

**Why this is still spec-conformant**

- PRD F1 acceptance: "`$legacyClassMap['Employee']` resolves to `BuyerKiosk\\Employee\\Employee`" — satisfied (covered by `tests/Unit/Compatibility/EmployeeAliasTest.php`).
- SDD ICO-6 / Constraint CON-21 (no Phase 1a code may merge until the legacy stub is gone): the stub file is removed; no production code path can construct the retired class.
- Test Scenario 24 (PRD F1): the new alias resolution test asserts the canonical target.

**SDD update**: not required — the SDD does not enumerate `Core\Employee`'s constructor signature, and adding a new helper to the canonical class does not change the public API surface the SDD describes.

**Test coverage of the deviation**

- `tests/Unit/Compatibility/EmployeeAliasTest.php` — alias resolves to canonical, retired class is unloadable.
- `tests/Unit/Employee/EmployeeFromLegacyIdTest.php` — `fromLegacyEmployeeId()` happy-path + null-on-miss + invalid-input guards (covers the new helper used by Buy.php).

### 2026-05-29 — T2 migrations 012 & 015: SDD schema names did not match the live DB

**Discovered at:** live `conductor run` apply (T2.4), commits 24c1d7467 (author) → 03f06baa9 (fix).

**Context**

Two migrations authored verbatim from the SDD's "Data Storage Changes" extensions failed on first apply because the SDD named schema objects that do not exist as written in the real database:

1. **Extension 4 (`20260522_012`)** — SDD said add `workersCompClassCode` + `qboWageAccountId` to a table called **`positions`** in `kiosk_buykiosk`. No such table exists. The actual scheduling-positions table is **`schedulePositions`**, and it is a **per-store** (`{{store}}`) table created by `20251220_013_004_schedule_positions.json` — not a central-DB table. Error at apply: `Table 'kiosk_buykiosk.positions' doesn't exist`.

2. **Extension 1 (`20260522_015`)** — SDD described a best-effort split of **`display_name`** on `kiosk_users.users`. The real column is **`displayName`** (camelCase), and `users` already carries structured **`firstName`/`lastName`** columns. Error at apply: `Unknown column 'display_name' in 'WHERE'`.

**Decision**

1. `012`: re-point both `add_column` ops to `database: {{store}}` / table `schedulePositions`, with `check_query` scoped via `TABLE_SCHEMA = DATABASE()`. Now applies on all dev stores. Reclassified as a per-store migration in `PayrollMigrationIdempotencyTest`.
2. `015`: rewrite the backfill to use the already-structured `firstName`/`lastName` as the PRIMARY source (more accurate than splitting a display string), with a `displayName`-split fallback (fixing the `display_name`→`displayName` name) for rows lacking structured names. Three idempotent `UPDATE` ops.

**Why still spec-conformant**

The INTENT of both extensions is preserved (per-position workers-comp/QBO-account columns; best-effort legal-name prefill). Only the physical schema targets changed to match reality. PRD F3 acceptance (best-effort name split) is better served by the structured-column source.

**SDD update:** recommended — SDD §"Data Storage Changes" Extension 1 should reference `displayName` + existing `firstName`/`lastName`, and Extension 4 should reference per-store `schedulePositions`, not `kiosk_buykiosk.positions`. (Spec docs are local-only; flagged here for the spec owner.)

**Validation:** live apply clean across 7 dev stores; idempotent re-run (zero applied / zero errors); 11 payroll tables + `userPayrollProfiles` + 12/12 permission hooks + 141 users backfilled, all confirmed by direct query.

### 2026-06-01 — T6 EvereeTokenStorage: encrypt-then-MAC over ADR-1's raw CBC

**Discovered at:** independent codex security review of T6 (commit c6d3d1f16).

**Context**

ADR-1 says "reuse `BuyerKiosk\Security\Encryption` (OpenSSL AES-256-CBC)". Codex verified that class is UNAUTHENTICATED CBC — a tampered/IV-flipped ciphertext decrypts to altered plaintext and is silently accepted (CBC malleability). For a per-tenant API-token + webhook-secret vault in a payroll/financial integration, accepting attacker-mutated ciphertext is unacceptable.

**Decision**

`EvereeTokenStorage` wraps `Security\Encryption` in an encrypt-then-MAC envelope `v1:<ciphertext>:<HMAC-SHA256>`: the MAC is computed over `version:ciphertext` with a MAC key DOMAIN-SEPARATED from the master key (`hash_hmac('sha256','everee-token-mac-v1',$masterKey,true)`), verified with `hash_equals()` BEFORE any decrypt. Any tamper (IV bit, ciphertext byte, MAC) throws `TokenDecryptionException`. The shared `Security\Encryption` class is NOT modified — the envelope lives entirely in the payroll module, so QuickBooks' use of the same primitive is unaffected. Companion migration `20260522_017` widened the 3 `*Encrypted` columns 512→1024 to fit the envelope; encrypt asserts envelope length before write.

**Why still spec-conformant**

ADR-1's intent (AES-256 at rest, separate key, reuse the existing primitive) is preserved — the envelope is an authentication layer ON TOP of `Security\Encryption`, not a replacement. This strengthens, not contradicts, the ADR.

**SDD update:** recommended — SDD §"EvereeTokenStorage" / ADR-1 should document the encrypt-then-MAC envelope as the required at-rest format for payroll secrets (and note the QBO primitive remains plain CBC). Local-only doc; flagged for the spec owner.

**Op-type gotcha (logged for future migrations):** central-DB (`kiosk_buykiosk`) migrations must use op type `alter_table`, NOT `alter_column` — `globalMigration.php` has no `alter_column` case and would fail with "Unknown operation type". (Migration 017 was fixed before apply.)

### 2026-06-01b — T7 EvereeApiClient: PRD vs SDD conflict on 429-without-Retry-After

**Discovered at:** codex review of T7 (commit b546320e6).

**Context**

PRD F5 acceptance (line 153): a 429 is handled by honoring `Retry-After`, and **"when absent, falls back to the same exponential-backoff policy."** But SDD §"Implementation Example 2" (the retry sample, ~lines 1916-1919) threw `EvereeRateLimitException` immediately when no `Retry-After` header was present. The two specs disagree.

**Decision**

The PRD is authoritative on acceptance criteria, so `EvereeApiClient` implements the PRD behavior: a 429 WITH `Retry-After` is honored once; a 429 WITHOUT `Retry-After` falls back to the 5xx-style exponential backoff within the retry budget, raising `EvereeRateLimitException` only on exhaustion. (Tested by `testScenario10bRateLimitWithoutRetryAfterFallsBackToExponentialBackoff`.)

**SDD update:** recommended — SDD Implementation Example 2 should be corrected to match PRD F5 (backoff fallback, not immediate throw). Local-only doc; flagged for the spec owner.

**Also (security, not a spec deviation but logged):** the decrypted per-tenant token must never be passed as a method argument (PHP stack traces include frame args when `zend.exception_ignore_args=Off`, the local default), or a thrown exception leaks `Authorization: Basic <token>` into logs. The client builds auth headers inside the request-sending frame; a regression test walks `$e->getTrace()` args for the token. The same defense pairs with T6's `__debugInfo`/`__serialize` omissions and T11's redactor.



---

## ADR Conformance (T17.12 — 2026-06-02)

All 11 ADRs verified against implemented code. Deviations that improve or strengthen (rather than contradict) an ADR are noted.

| ADR | Decision | Implemented behaviour | Status |
|---|---|---|---|
| ADR-1 | Reuse `Security\Encryption` (AES-256-CBC) with separate `EVEREE_ENCRYPTION_KEY` | `EvereeTokenStorage` wraps `Security\Encryption` with an **encrypt-then-MAC envelope** (`v1:<ciphertext>:<HMAC-SHA256>`); ADR-1's intent is preserved but the implementation adds authenticated encryption on top of plain CBC. | ✅ (intentional hardening — Deviation Log 2026-06-01) |
| ADR-2 | `payRateHistory` append-only via repo (no update/delete methods) + `GuardedPdo` + regression test; DB-level trigger documented follow-up | `PayRateHistoryRepository` has only `insert`, `findGreatestEffectiveFromAtOrBefore`, `listOrdered`; `GuardedPdo` blocks SQL UPDATE/DELETE; migration 019 adds BEFORE UPDATE + BEFORE DELETE triggers (DB-level is no longer a "follow-up" — it is shipped). | ✅ (exceeds ADR: DB triggers shipped in-phase) |
| ADR-3 | Per-tenant webhook secret default; global mode via `EVEREE_WEBHOOK_SIGNING_MODE` env switch; column exists in both modes | `EvereeWebhookHandler` reads `EVEREE_WEBHOOK_SIGNING_MODE`; both `per_tenant` and `global` paths tested; `payrollTenants.webhookSecretEncrypted` present in both modes | ✅ |
| ADR-4 | TaskEngine `default` queue initially; Feature 12 audit may move to dedicated `payroll` queue | `ProcessEvereeWebhookJob::getQueue()` returns `'default'`; Feature 12 audit doc recommends adding `payroll` queue in Phase 1a — the recommendation is documented but the queue name is still `default` in the shipped job | ✅ (ADR says "initially default"; Feature 12 follow-up deferred to Phase 1b) |
| ADR-5 | HMAC-SHA256 with multi-candidate-secret rotation tolerance | `EvereeWebhookHandler` verifies against current + prior secret while `webhookPriorSecretExpiresAt > now()`; signed string is `timestamp + '.' + rawBody`; Scenario 19 covers rotation window | ✅ |
| ADR-6 | Plain HTTP client + custom retry/backoff (no circuit-breaker library) | `EvereeApiClient` uses `GuzzleHttp\Client` with manual retry loop; no circuit-breaker library imported | ✅ |
| ADR-7 | Idempotency-Key on every POST; best-effort; documented residual risk if partner doesn't honor | `EvereeApiClient` sends `Idempotency-Key` header on POST; Scenario 11c verifies header is sent; residual risk documented in client docblock | ✅ |
| ADR-8 | Single namespace `BuyerKiosk\Payroll\`; no scattering into `Core\` | All 38 payroll source files declare `namespace BuyerKiosk\Payroll\*`; zero `namespace BuyerKiosk\Core` declarations inside the Payroll directory | ✅ |
| ADR-9 | 12 permission keys in one `uf_authorize_group` migration JSON | Migration 016 contains exactly 12 permission-key INSERT groups; `PayrollPermissionKeysTest` validates all 12 keys + per-role assignments | ✅ |
| ADR-10 | PII source-of-truth labels in migration descriptions (CON-18) | Migration 009 (users legal-PII columns) contains per-column PII-category labels ("BK canonical", "onboarding-kickoff prefill (write-once, then Everee canonical)", "never enters BK") in each operation's description | ✅ |
| ADR-11 | `EvereeWebhookPayloadRedactor` strips PII before DB INSERT; `EVEREE_PARTNER_API_TOKEN` for company-instance create only; per-tenant prior-secret columns for rotation | `EvereeWebhookPayloadRedactor::redact()` runs before any INSERT; `EvereeApiClient::createCompanyInstance` uses partner token (no tenant-id header); migration 001 includes `webhookPriorSecretEncrypted` + `webhookPriorSecretExpiresAt` | ✅ |

No unlogged ADR deviations found. The Deviation Log entries (2026-05-26 T1, 2026-05-29 T2 schema names, 2026-06-01 T6 encrypt-then-MAC, 2026-06-01b T7 429-backoff) cover all known divergences from the SDD/ADRs.

---

## Honest Gap List (T17 — 2026-06-02)

Items that are NOT complete as of the T17 compliance audit. Nothing here is a surprise — all were known design choices about human-gated or partner-gated work.

### 1. T15 — Live-sandbox smoke test (partner-gated; operational, not buildable here)

**Status:** Cannot be run. Requires real Everee sandbox credentials (open §13 partner item: confirm sandbox tenant + base URL). Engineering confirmed this is NOT a Phase 1a code sign-off blocker per PRD F5 AC. When credentials arrive: run `T15.2.1–T15.2.4` (provision tenant → create worker → verify `worker.created` webhook → dedupe → setRate as-of).

### 2. T3.4.1 — Second-engineer `Signed-off-by` on person-centric-account-verification.md

**Status:** `docs/specs/050-everee-payroll-foundations/person-centric-account-verification.md §4` has a blank `Signed-off-by:` line. Requires one engineer other than the report author to review and sign. CON-22 code implementation is complete and codex-reviewed; this is only the formal sign-off formality.

### 3. T4.4.1 — Second-engineer `Signed-off-by` on taskengine-payroll-audit.md

**Status:** `docs/patterns/taskengine-payroll-audit.md` bottom section reads `Signed-off-by: ___`. Requires one engineer other than the author to review. Audit content and recommendations are correct and have been acted upon (payroll queue added in T10).

### 4. F13 (T5) — All CS-owned pre-flight checklist items

**Status:** `docs/specs/050-everee-payroll-foundations/phase-0-preflight-checklist.md` is a scaffold — all operational items show `[ ] PENDING HUMAN ACTION`. Specifically:
- Item 1: Pilot-store rate-data CSVs for 5 stores — not collected (no named CS owner assigned)
- Item 2: Partner-manager kickoff email — draft authored in the doc, but the actual SEND + tracking thread reference are outstanding
- Item 3: Pilot candidate list (5 typeNums) — placeholder `<TBD>`
- Item 4: Named owners — all show `<CS — TBD: assign a named CS team member>`
- Engineering review / no-Phase-1a-blocker sign-off: not signed

**Engineering assessment (F13 AC 5 partial):** The Phase 1a code artifacts (T6–T14) do NOT depend on any F13 checklist item. The only Phase 1a item that is CS/partner-gated is T15 (sandbox smoke test). A brief engineering sign-off line in the checklist would close AC 5 without needing any of the CS items to be completed first.

### 5. PERF-1..5 — Performance targets not measured against live sandbox

**Status:** SDD §Quality Requirements defines 5 performance targets (e.g., PERF-1: webhook ingestion p95 < 200 ms; PERF-2: `getRate` p95 < 5 ms; etc.). These were NOT measured in T17 because no Everee sandbox credentials are available. They were listed as "fixture/sandbox runs" in the plan (not production). They remain outstanding until the T15 sandbox smoke test runs. Record measured p95 values in the README review log at that time.

### 6. doc path inconsistency — `payroll-rate-history-append-only.md` + `everee-api.md` are under `userfrosting/docs/`

**Status:** Two plan deliverables landed under `userfrosting/docs/` (which is gitignored/local) instead of repo-root `docs/`:
- `userfrosting/docs/patterns/payroll-rate-history-append-only.md` (T8.3.6) — should be at `docs/patterns/payroll-rate-history-append-only.md`
- `userfrosting/docs/interfaces/everee-api.md` (T7.3.4) — should be at `docs/interfaces/everee-api.md`

Additionally, `userfrosting/docs/patterns/payroll-token-encryption.md` is a duplicate of `docs/patterns/payroll-token-encryption.md` (T6.3.5 created both). The userfrosting/ copies are present and correct in content; only their canonical path is inconsistent with the plan. This is a housekeeping item, not a functional gap.

### 7. T17.5 — PERF targets, T17.9.4 — SDD directory map sample (minor)

- T17.9.4: "sample 10 random SDD directory-map entries" was not formally done in this audit. The file-existence checks above confirm the major deliverables exist; a full SDD-vs-filesystem directory-map cross-check is deferred to the Phase 1b kickoff review.
- T12.4.2: "Manual smoke test via browser/curl" for the 5 admin endpoints — browser/curl smoke test is not automatable in this audit. The 4 endpoints have green unit tests (PayrollAdminControllerTest) covering success, auth-denial, and validation-error paths. Manual smoke pending T15.

---

**End of Implementation Plan.** Next step after merge: `/start:implement 050` OR direct PR-by-PR execution following the phase order above.
