# Implementation Plan: POS Data Catchup System (Spec 043)

## Validation Checklist

- [x] All specification file paths are correct and exist
- [x] Context priming section is complete
- [x] All implementation phases are defined
- [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
- [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**: Complete the Pre-Implementation Specification Gate
2. **During Implementation**: Reference specific SDD sections in each task
3. **After Each Task**: Run Specification Compliance checks
4. **Phase Completion**: Verify all specification requirements are met

### Deviation Protocol

If implementation cannot follow specification exactly:
1. Document the deviation and reason
2. Get approval before proceeding
3. Update SDD if the deviation is an improvement
4. Never deviate without documentation

## Metadata Reference

- `[parallel: true]` - Tasks that can run concurrently
- `[component: component-name]` - For multi-component features
- `[ref: document/section; lines: 1, 2-3]` - Links to specifications, patterns, or interfaces and (if applicable) line(s)
- `[activity: type]` - Activity hint for specialist agent selection

---

## Context Priming

*GATE: You MUST fully read all files mentioned in this section before starting any implementation.*

**Specification**:

- `docs/specs/043-pos-data-catchup/product-requirements.md` - Product Requirements (all features, business rules, edge cases)
- `docs/specs/043-pos-data-catchup/solution-design.md` - Solution Design (architecture, schemas, contracts, ADRs)

**Key Design Decisions** (all confirmed):

- **ADR-1**: Per-store job scope (not global) — scheduler handles store iteration automatically
- **ADR-2**: Dedicated `posDataGaps` table in `kiosk_buykiosk` (not TaskEngine execution data) — persistent across runs
- **ADR-3**: Ably action prefix `drs:catchup:*` — prevents collision with existing ~20 action types
- **ADR-4**: HTTP POST for boundary reporting (not Ably reverse publish) — follows existing DRS endpoint pattern
- **ADR-5**: 30-day batch chunking for backfills — prevents overwhelming desktop app
- **ADR-6**: Multi-source DB queries (central + store + kiosk_sales) — data lives across databases

**Implementation Context**:

- Commands to run:
  - `./test.sh --testsuite unit` — Run all unit tests
  - `cd userfrosting && ./vendor/bin/phpunit --filter "PosDataCatchup"` — Targeted tests
  - `php userfrosting/conductor run` — Run pending migrations
  - `cd userfrosting && ./vendor/bin/phpstan analyse src/BuyerKiosk/PosDataCatchup/` — Static analysis
  - `php userfrosting/bin/task job:list | grep catchup` — Verify job registration
  - `php userfrosting/bin/task job:dispatch pos-data-catchup --store=pc00` — Manual dispatch test

- Patterns to follow:
  - `userfrosting/src/BuyerKiosk/TaskEngine/Domain/Job/BaseJob.php` — Job base class with progress(), checkpoint(), info(), getStore(), getStoreDb(), config(), payload()
  - `userfrosting/src/BuyerKiosk/TaskEngine/Jobs/BackfillMockDataJob.php` — Reference backfill job (lookback, chunking, abort checks)
  - `userfrosting/src/BuyerKiosk/Workbook/WorkbookAbly.php` — Ably publisher pattern (constructor init, throttle integration)
  - `userfrosting/src/BuyerKiosk/Core/AblyPublishThrottle.php` — Throttle integration (shouldPublish())
  - `userfrosting/src/BuyerKiosk/TaskEngine/Domain/Job/JobResult.php` — Success/failure result pattern
  - `userfrosting/routes/groups/daily-close.php` — DRS API route pattern with X-API-Key validation
  - `userfrosting/migrations/input/20260103_001_backfill_mock_data_job.json` — Migration format for job definition
  - `userfrosting/migrations/input/20260210_036_005_billing_store_columns.json` — Migration format for ALTER TABLE

- Interfaces to implement:
  - `BuyerKiosk\TaskEngine\Domain\Job\JobInterface` — getName(), getDisplayName(), getQueue(), getScope(), getTimeout(), handle()

**Critical Gotchas** (from MEMORY.md and SDD):

- PDO named param reuse: Cannot reuse `:param` name — use unique names `:typeNum1`, `:typeNum2`
- Store buys table: Named `buyQueue` NOT `buys` (per CLAUDE.md)
- Daily close in per-store DB: `drsDailySFileData` is in `kiosk_{typeNum}`, NOT `kiosk_sales`
- Sales/buys in central DB: `kiosk_sales.sales` and `kiosk_sales.buys`
- INT column comparisons: Use `> 0` not `<> ''` for INT columns
- Migration `check_query`: Use `SHOW COLUMNS` for ALTER TABLE, `SELECT id FROM...` for INSERT
- Store timezone: Use `stores.timezone` for all DateTime operations
- Migration backslashes: Double-escape in JSON: `BuyerKiosk\\\\PosDataCatchup\\\\Jobs\\\\PosDataCatchupJob`

---

## Implementation Phases

### Phase 1: Database Foundation (Migrations)

> **Goal**: Create the database schema and job definition. All subsequent phases depend on this.

- [ ] T1 Phase 1: Database Migrations `[component: migrations]`

    - [ ] T1.1 Prime Context
        - [ ] T1.1.1 Read SDD §Interface Specifications → Data Storage Changes `[ref: solution-design.md; lines: 301-350]`
        - [ ] T1.1.2 Read existing migration format examples `[ref: userfrosting/migrations/input/20260103_001_backfill_mock_data_job.json]` `[ref: userfrosting/migrations/input/20260210_036_005_billing_store_columns.json]`
        - [ ] T1.1.3 Confirm `drsApiKey` column exists on stores table for `AFTER` placement reference

    - [ ] T1.2 Implement Migrations `[activity: backend-db]`
        - [ ] T1.2.1 Create `userfrosting/migrations/input/20260401_043_001_pos_data_gaps_table.json`
            - Type: `create_table`, database: `kiosk_buykiosk`
            - Full `posDataGaps` table with all columns per SDD schema
            - check_query: `SHOW TABLES LIKE 'posDataGaps'`
            - Include all indexes: PRIMARY, `uk_store_date`, `idx_status`, `idx_typenum_status`, `idx_persistent`
        - [ ] T1.2.2 Create `userfrosting/migrations/input/20260401_043_002_stores_earliest_data_date.json`
            - Type: `alter_table`, database: `kiosk_buykiosk`
            - Add `drsEarliestDataDate` date DEFAULT NULL column to `stores` AFTER `drsApiKey`
            - check_query: `SHOW COLUMNS FROM stores LIKE 'drsEarliestDataDate'`
        - [ ] T1.2.3 Create `userfrosting/migrations/input/20260401_043_003_pos_data_catchup_job_def.json`
            - Type: `insert`, database: `kiosk_buykiosk`
            - Job definition: name=`pos-data-catchup`, scope=`per_store`, timeout=600, schedule=`0 3 * * *`
            - Config JSON: `{"lookbackDays": 365, "batchChunkDays": 30, "bufferDays": 1, "persistentThresholdDays": 7}`
            - check_query: `SELECT id FROM task_job_definitions WHERE name = 'pos-data-catchup' LIMIT 1`
            - Double-escape backslashes for className: `BuyerKiosk\\\\PosDataCatchup\\\\Jobs\\\\PosDataCatchupJob`

    - [ ] T1.3 Validate
        - [ ] T1.3.1 Run migrations: `php userfrosting/conductor run` `[activity: run-migrations]`
        - [ ] T1.3.2 Verify table created: `SHOW CREATE TABLE posDataGaps` `[activity: verify-db]`
        - [ ] T1.3.3 Verify column added: `SHOW COLUMNS FROM stores LIKE 'drsEarliestDataDate'` `[activity: verify-db]`
        - [ ] T1.3.4 Verify job registered: `SELECT * FROM task_job_definitions WHERE name = 'pos-data-catchup'` `[activity: verify-db]`

---

### Phase 2: Core Services (GapTracker + GapDetector)

> **Goal**: Build the persistence layer and gap detection logic. These are the foundation services that the job orchestrator depends on. GapTracker has no dependencies on other new code; GapDetector depends only on database queries.

- [ ] T2 Phase 2: Core Services `[component: services]`

    - [ ] T2.1 GapTracker Persistence Layer `[component: gap-tracker]`

        - [ ] T2.1.1 Prime Context
            - [ ] T2.1.1.1 Read SDD §Application Data Models → GapTracker `[ref: solution-design.md; lines: 466-480]`
            - [ ] T2.1.1.2 Read SDD §Data Storage Changes → posDataGaps schema `[ref: solution-design.md; lines: 305-329]`

        - [ ] T2.1.2 Write Tests `[activity: backend-test]`
            - [ ] T2.1.2.1 Create `tests/Unit/PosDataCatchup/GapTrackerTest.php`
            - [ ] T2.1.2.2 Test `upsertGap()` — inserts new gap with correct defaults `[ref: PRD Feature 3 AC: each gap tracked with store, date, data type, first detected]`
            - [ ] T2.1.2.3 Test `upsertGap()` — updates existing gap via ON DUPLICATE KEY (idempotency) `[ref: SDD §Quality: job must be idempotent]`
            - [ ] T2.1.2.4 Test `markCommanded()` — updates status to 'commanded', increments commandCount, sets requestId
            - [ ] T2.1.2.5 Test `resolveGap()` — sets status='resolved' and resolvedAt timestamp `[ref: PRD Feature 3 AC: gaps transition to resolved]`
            - [ ] T2.1.2.6 Test `markUnavailable()` — bulk resolves all gaps before a given date `[ref: PRD Feature 5 AC: gaps before earliest date auto-resolved]`
            - [ ] T2.1.2.7 Test `getUnresolvedGaps()` — returns only detected+commanded gaps for a store
            - [ ] T2.1.2.8 Test `getPersistentGaps()` — returns gaps older than threshold `[ref: PRD Feature 3 AC: persistent after configurable attempts]`
            - [ ] T2.1.2.9 Test `getGapStats()` — returns summary counts by status
            - [ ] T2.1.2.10 Test `cleanupOldResolved()` — archives resolved gaps beyond retention

        - [ ] T2.1.3 Implement `[activity: backend-service]`
            - [ ] T2.1.3.1 Create `userfrosting/src/BuyerKiosk/PosDataCatchup/Persistence/GapTracker.php`
            - Constructor: `__construct(PDO $centralDb)`
            - All methods per SDD pseudocode
            - Use `INSERT INTO posDataGaps ... ON DUPLICATE KEY UPDATE` for upsert
            - Use unique PDO param names (avoid `:param` reuse gotcha)

        - [ ] T2.1.4 Validate
            - [ ] T2.1.4.1 Run tests: `cd userfrosting && ./vendor/bin/phpunit --filter "GapTrackerTest"` `[activity: run-tests]`
            - [ ] T2.1.4.2 PHPStan: `cd userfrosting && ./vendor/bin/phpstan analyse src/BuyerKiosk/PosDataCatchup/Persistence/` `[activity: lint-code]`

    - [ ] T2.2 GapDetector Service `[component: gap-detector]`

        - [ ] T2.2.1 Prime Context
            - [ ] T2.2.1.1 Read SDD §Application Data Models → GapDetector `[ref: solution-design.md; lines: 444-456]`
            - [ ] T2.2.1.2 Read SDD §Implementation Examples → Gap Detection Algorithm `[ref: solution-design.md; lines: 494-537]`
            - [ ] T2.2.1.3 Read SDD §Performance Characteristics → query patterns `[ref: solution-design.md; lines: 819-823]`
            - [ ] T2.2.1.4 Read SDD §Complex Logic → Window Calculation `[ref: solution-design.md; lines: 738-755]`

        - [ ] T2.2.2 Write Tests `[activity: backend-test]`
            - [ ] T2.2.2.1 Create `tests/Unit/PosDataCatchup/GapDetectorTest.php`
            - [ ] T2.2.2.2 Test `detectGaps()` — finds dates with missing sales when buyQueue confirms store open `[ref: PRD Rule 1, SDD Scenario 1]`
            - [ ] T2.2.2.3 Test `detectGaps()` — partial data returns only missing types `[ref: PRD Rule 3]`
            - [ ] T2.2.2.4 Test `detectGaps()` — closed day not flagged (no buyQueue + isClosed=1) `[ref: SDD Scenario 5]`
            - [ ] T2.2.2.5 Test `detectGaps()` — buyQueue overrides store hours (open on closed day) `[ref: SDD Scenario 6, PRD Rule 2]`
            - [ ] T2.2.2.6 Test `getOperationalDays()` — merges buyQueue dates with scheduled open days
            - [ ] T2.2.2.7 Test `getBuyQueueDates()` — queries per-store `buyQueue.timeEntered` with date range
            - [ ] T2.2.2.8 Test `getScheduledOpenDays()` — respects storeOperatingHours and storeHolidayHours
            - [ ] T2.2.2.9 Test `hasSalesData()` — uses `EXISTS` on `kiosk_sales.sales` with typeNum + salesDate
            - [ ] T2.2.2.10 Test `hasBuysData()` — uses `EXISTS` on `kiosk_sales.buys` with typeNum + buyDate
            - [ ] T2.2.2.11 Test `hasDailyCloseData()` — uses `EXISTS` on per-store `drsDailySFileData.closingDate`
            - [ ] T2.2.2.12 Test timezone handling — dates use store timezone `[ref: PRD Edge Case: timezone mismatch]`

        - [ ] T2.2.3 Implement `[activity: backend-service]`
            - [ ] T2.2.3.1 Create `userfrosting/src/BuyerKiosk/PosDataCatchup/Services/GapDetector.php`
            - Constructor: `__construct(PDO $centralDb, PDO $salesDb, PDO $storeDb, string $typeNum, string $timezone)`
            - Batch date queries: single query per data type for the full window (not per-date)
            - Use `SELECT 1 FROM ... WHERE ... LIMIT 1` pattern for existence checks
            - Use `SELECT DISTINCT DATE(timeEntered) FROM buyQueue WHERE DATE(timeEntered) BETWEEN :from AND :to` for buyQueue
            - Use `SELECT closingDate FROM drsDailySFileData WHERE closingDate BETWEEN :from AND :to` for daily close
            - Use `SELECT DISTINCT salesDate FROM sales WHERE typeNum = :t AND salesDate BETWEEN :from AND :to` for sales
            - Use `SELECT DISTINCT buyDate FROM buys WHERE typeNum = :t AND buyDate BETWEEN :from AND :to` for buys
            - Merge results using set operations in PHP
            - **CRITICAL**: buyQueue table is named `buyQueue` not `buys` in store DBs

        - [ ] T2.2.4 Validate
            - [ ] T2.2.4.1 Run tests: `cd userfrosting && ./vendor/bin/phpunit --filter "GapDetectorTest"` `[activity: run-tests]`
            - [ ] T2.2.4.2 PHPStan: `cd userfrosting && ./vendor/bin/phpstan analyse src/BuyerKiosk/PosDataCatchup/Services/GapDetector.php` `[activity: lint-code]`
            - [ ] T2.2.4.3 Verify batch queries used (not per-date queries) `[ref: SDD §Performance Characteristics]` `[activity: review-code]`

---

### Phase 3: Ably Publisher + Boundary Controller (Parallel)

> **Goal**: Build the Ably command publisher and the HTTP boundary endpoint. These are independent of each other and can be built in parallel. Both depend on Phase 1 (DB schema) but not Phase 2.

- [ ] T3 Phase 3: Messaging & API `[component: messaging-api]`

    - [ ] T3.1 CatchupAblyPublisher `[parallel: true]` `[component: ably-publisher]`

        - [ ] T3.1.1 Prime Context
            - [ ] T3.1.1.1 Read SDD §Ably Catchup Command Contract `[ref: solution-design.md; lines: 377-427]`
            - [ ] T3.1.1.2 Read SDD §Implementation Examples → Ably Publisher Pattern `[ref: solution-design.md; lines: 580-661]`
            - [ ] T3.1.1.3 Read WorkbookAbly for reference pattern `[ref: userfrosting/src/BuyerKiosk/Workbook/WorkbookAbly.php; lines: 1-65]`
            - [ ] T3.1.1.4 Read AblyPublishThrottle for throttle integration `[ref: userfrosting/src/BuyerKiosk/Core/AblyPublishThrottle.php]`

        - [ ] T3.1.2 Write Tests `[activity: backend-test]`
            - [ ] T3.1.2.1 Create `tests/Unit/PosDataCatchup/CatchupAblyPublisherTest.php`
            - [ ] T3.1.2.2 Test `publishCatchupRequest()` — produces correct payload structure `[ref: SDD command contract]`
            - [ ] T3.1.2.3 Test payload includes all required fields: action, category, timestamp, source, requestId, version, dataTypes, dateRange, reportBoundary, endpoints `[ref: PRD Feature 2 AC]`
            - [ ] T3.1.2.4 Test `publishCatchupRequest()` — returns false when Ably not configured (no ABLY_KEY)
            - [ ] T3.1.2.5 Test `publishCatchupRequest()` — returns false when throttled `[ref: PRD Feature 2 AC: respects throttle]`
            - [ ] T3.1.2.6 Test `publishCatchupRequest()` — returns false and logs on Ably publish exception
            - [ ] T3.1.2.7 Test endpoint URLs contain correct typeNum substitution
            - [ ] T3.1.2.8 Test version field is always 1

        - [ ] T3.1.3 Implement `[activity: backend-service]`
            - [ ] T3.1.3.1 Create `userfrosting/src/BuyerKiosk/PosDataCatchup/Services/CatchupAblyPublisher.php`
            - Follow WorkbookAbly constructor pattern exactly
            - Constructor: `__construct(string $typeNum)` — init Ably client + throttle
            - `publishCatchupRequest(array $dataTypes, string $startDate, string $endDate, string $requestId, bool $reportBoundary): bool`
            - Payload must match SDD contract exactly (action, category, timestamp, etc.)
            - Use `AblyPublishThrottle::shouldPublish()` before publishing
            - Return false (not throw) on failure — caller handles retry logic

        - [ ] T3.1.4 Validate
            - [ ] T3.1.4.1 Run tests: `cd userfrosting && ./vendor/bin/phpunit --filter "CatchupAblyPublisherTest"` `[activity: run-tests]`
            - [ ] T3.1.4.2 PHPStan: `cd userfrosting && ./vendor/bin/phpstan analyse src/BuyerKiosk/PosDataCatchup/Services/CatchupAblyPublisher.php` `[activity: lint-code]`

    - [ ] T3.2 CatchupBoundaryController + Route `[parallel: true]` `[component: boundary-api]`

        - [ ] T3.2.1 Prime Context
            - [ ] T3.2.1.1 Read SDD §Internal API Changes → Earliest Date Boundary Endpoint `[ref: solution-design.md; lines: 352-375]`
            - [ ] T3.2.1.2 Read SDD §Application Data Models → CatchupBoundaryController `[ref: solution-design.md; lines: 483-489]`
            - [ ] T3.2.1.3 Read DailyCloseApiController for validateApiKey pattern `[ref: userfrosting/routes/groups/daily-close.php; lines: 49-58]`
            - [ ] T3.2.1.4 Read SDD §Secondary Flow → Desktop Reports Earliest Date `[ref: solution-design.md; lines: 708-723]`

        - [ ] T3.2.2 Write Tests `[activity: backend-test]`
            - [ ] T3.2.2.1 Create `tests/Unit/PosDataCatchup/CatchupBoundaryControllerTest.php`
            - [ ] T3.2.2.2 Test `reportEarliestDate()` — valid date updates `stores.drsEarliestDataDate` `[ref: PRD Feature 5 AC]`
            - [ ] T3.2.2.3 Test `reportEarliestDate()` — calls GapTracker::markUnavailable() for gaps before new date `[ref: PRD Feature 5 AC: gaps auto-resolved]`
            - [ ] T3.2.2.4 Test `reportEarliestDate()` — returns previousDate in response when updating
            - [ ] T3.2.2.5 Test `reportEarliestDate()` — rejects invalid date format (400 response)
            - [ ] T3.2.2.6 Test `reportEarliestDate()` — rejects missing API key (401 response)
            - [ ] T3.2.2.7 Test `reportEarliestDate()` — rejects invalid API key (401 response)
            - [ ] T3.2.2.8 Test idempotency — calling with same date twice is safe `[ref: SDD §Quality: idempotent]`

        - [ ] T3.2.3 Implement `[activity: backend-api]`
            - [ ] T3.2.3.1 Create `userfrosting/src/BuyerKiosk/PosDataCatchup/Controllers/CatchupBoundaryController.php`
            - Constructor: `__construct(Store $store, \Slim\Slim $app)`
            - Static method: `validateApiKey(\Slim\Slim $app, string $typeNum): ?Store` — follows DailyCloseApiController pattern
            - `reportEarliestDate(): void` — POST handler
            - Validate date format (YYYY-MM-DD)
            - Update `stores.drsEarliestDataDate` via PDO
            - Call `GapTracker::markUnavailable()` for dates before the new boundary
            - Return JSON response with success, typeNum, earliestDate, previousDate
            - [ ] T3.2.3.2 Create `userfrosting/routes/groups/catchup.php`
            - Route: `POST /api/:typeNum/drs/catchup/boundary`
            - Follow `daily-close.php` pattern: validate API key → instantiate controller → call handler
            - [ ] T3.2.3.3 Modify `userfrosting/routes/api.php`
            - Add `require ("groups/catchup.php");` in the `/:typeNum` group

        - [ ] T3.2.4 Validate
            - [ ] T3.2.4.1 Run tests: `cd userfrosting && ./vendor/bin/phpunit --filter "CatchupBoundaryControllerTest"` `[activity: run-tests]`
            - [ ] T3.2.4.2 PHPStan: `cd userfrosting && ./vendor/bin/phpstan analyse src/BuyerKiosk/PosDataCatchup/Controllers/` `[activity: lint-code]`
            - [ ] T3.2.4.3 Verify route registered by checking route file loads without errors `[activity: review-code]`

---

### Phase 4: Job Orchestrator + Registration

> **Goal**: Build the main `PosDataCatchupJob` that orchestrates gap detection, command publishing, and gap tracking. Also register the job in TaskCommandFactory. Depends on Phases 1, 2, and 3.

- [ ] T4 Phase 4: Job Orchestrator `[component: job]`

    - [ ] T4.1 Prime Context
        - [ ] T4.1.1 Read SDD §Application Data Models → PosDataCatchupJob `[ref: solution-design.md; lines: 432-438]`
        - [ ] T4.1.2 Read SDD §Primary Flow sequence diagram `[ref: solution-design.md; lines: 665-706]`
        - [ ] T4.1.3 Read SDD §Implementation Examples → Chunked Backfill `[ref: solution-design.md; lines: 539-578]`
        - [ ] T4.1.4 Read SDD §Complex Logic → Gap Detection Window Calculation `[ref: solution-design.md; lines: 738-755]`
        - [ ] T4.1.5 Read BackfillMockDataJob for pattern `[ref: userfrosting/src/BuyerKiosk/TaskEngine/Jobs/BackfillMockDataJob.php; lines: 1-120]`
        - [ ] T4.1.6 Read BaseJob for available helpers `[ref: userfrosting/src/BuyerKiosk/TaskEngine/Domain/Job/BaseJob.php]`
        - [ ] T4.1.7 Read JobResult for return pattern `[ref: userfrosting/src/BuyerKiosk/TaskEngine/Domain/Job/JobResult.php]`

    - [ ] T4.2 Write Tests `[activity: backend-test]`
        - [ ] T4.2.1 Create `tests/Unit/PosDataCatchup/PosDataCatchupJobTest.php`
        - [ ] T4.2.2 Test static methods: getName()='pos-data-catchup', getScope()='per_store', getTimeout()=600, getQueue()='default' `[ref: SDD §Job Definition]`
        - [ ] T4.2.3 Test `handle()` — store without drsApiKey returns success with skip message `[ref: PRD: filter by drsApiKey]`
        - [ ] T4.2.4 Test `handle()` — calculates correct window from lookbackDays + bufferDays `[ref: SDD §Window Calculation]`
        - [ ] T4.2.5 Test `handle()` — respects drsEarliestDataDate as lower bound `[ref: PRD Rule 7, Feature 5]`
        - [ ] T4.2.6 Test `handle()` — detects gaps and publishes chunked commands `[ref: SDD Scenario 1]`
        - [ ] T4.2.7 Test `handle()` — new store with no data triggers full backfill with reportBoundary=true `[ref: SDD Scenario 2, PRD Rule 6]`
        - [ ] T4.2.8 Test `handle()` — re-checks previously commanded gaps for resolution `[ref: SDD Scenario 4]`
        - [ ] T4.2.9 Test `handle()` — marks persistent gaps after threshold days `[ref: PRD Feature 3 AC: persistent after configurable attempts]`
        - [ ] T4.2.10 Test chunk logic — 365-day range splits into 13 chunks (12x30 + 1x5) `[ref: ADR-5]`
        - [ ] T4.2.11 Test manual dispatch with payload override — startDate/endDate bypass gap detection `[ref: PRD Feature 4]`
        - [ ] T4.2.12 Test `handle()` — returns JobResult::success() with metrics (gapsFound, commandsSent, resolved) `[ref: PRD Feature 6]`
        - [ ] T4.2.13 Test checkpoint() called in loops for abort support `[ref: BaseJob::checkpoint()]`

    - [ ] T4.3 Implement `[activity: backend-service]`
        - [ ] T4.3.1 Create `userfrosting/src/BuyerKiosk/PosDataCatchup/Jobs/PosDataCatchupJob.php`
        - Extends `BaseJob`
        - Static config: getName()='pos-data-catchup', getScope()='per_store', getQueue()='default', getTimeout()=600
        - `handle()` method orchestration:
            1. Validate store has drsApiKey (skip if not)
            2. Load config: lookbackDays, batchChunkDays, bufferDays, persistentThresholdDays
            3. Check for manual payload override (startDate/endDate)
            4. Calculate detection window (respect drsEarliestDataDate)
            5. Instantiate GapDetector, GapTracker, CatchupAblyPublisher
            6. Call GapDetector::detectGaps() for the window
            7. Upsert detected gaps via GapTracker
            8. Group gaps into contiguous date ranges
            9. Chunk ranges into batchChunkDays batches
            10. Publish commands for each chunk via CatchupAblyPublisher
            11. Mark commanded gaps via GapTracker
            12. Re-check previously commanded gaps for resolution
            13. Mark persistent gaps past threshold
            14. Report progress and return JobResult with metrics
        - [ ] T4.3.2 Create private helper methods:
            - `calculateWindow(): array` — returns [fromDate, toDate]
            - `groupIntoRanges(array $dates): array` — contiguous date grouping
            - `chunkDateRange(string $start, string $end, int $chunkDays): array` — split into batches
            - `hasEarliestDate(): bool` — check if store has reported boundary
            - `checkResolutions(GapDetector, GapTracker): int` — re-check commanded gaps

    - [ ] T4.4 Register Job in TaskCommandFactory `[activity: backend-service]`
        - [ ] T4.4.1 Add `use` statement and register `PosDataCatchupJob::class` in `TaskCommandFactory::registerJobs()` `[ref: userfrosting/src/BuyerKiosk/TaskEngine/Commands/TaskCommandFactory.php; lines: 234-301]`
        - Add under a comment `// POS Data Catchup jobs (043-pos-data-catchup)`
        - Import: `use BuyerKiosk\PosDataCatchup\Jobs\PosDataCatchupJob;`

    - [ ] T4.5 Update Store Model (if needed) `[activity: backend-service]`
        - [ ] T4.5.1 Check if Store.php already loads `drsEarliestDataDate` — if not, add getter/setter
        - The migration adds the column; the Store model may or may not need explicit getter/setter depending on how it loads data
        - If Store uses `__get()` magic or loads all columns, no change needed
        - If Store has explicit property mappings, add `drsEarliestDataDate` property + getter

    - [ ] T4.6 Validate
        - [ ] T4.6.1 Run all catchup tests: `cd userfrosting && ./vendor/bin/phpunit --filter "PosDataCatchup"` `[activity: run-tests]`
        - [ ] T4.6.2 PHPStan full namespace: `cd userfrosting && ./vendor/bin/phpstan analyse src/BuyerKiosk/PosDataCatchup/` `[activity: lint-code]`
        - [ ] T4.6.3 Verify job visible: `php userfrosting/bin/task job:list | grep catchup` `[activity: verify-registration]`
        - [ ] T4.6.4 Review code against SDD patterns `[activity: review-code]`
        - [ ] T4.6.5 Verify PRD acceptance criteria for Features 1-5 `[activity: business-acceptance]`

---

### Phase 5: Integration & End-to-End Validation

> **Goal**: Run all tests together, verify cross-component interactions, and validate against full PRD/SDD specifications.

- [ ] T5 Phase 5: Integration & End-to-End Validation

    - [ ] T5.1 All Unit Tests Passing `[activity: run-tests]`
        - [ ] T5.1.1 Run: `cd userfrosting && ./vendor/bin/phpunit --filter "PosDataCatchup"` — all green
        - [ ] T5.1.2 Run: `./test.sh --testsuite unit` — no regression in existing tests

    - [ ] T5.2 Integration Test: Full Job Execution `[activity: backend-test]`
        - [ ] T5.2.1 Test PosDataCatchupJob with mocked DB connections exercising real GapDetector + GapTracker + Publisher interaction
        - [ ] T5.2.2 Test full flow: detect gaps → upsert → chunk → publish → mark commanded
        - [ ] T5.2.3 Test resolution flow: commanded gap → data appears → job resolves gap
        - [ ] T5.2.4 Test boundary flow: boundary posted → gaps before date marked unavailable → job window adjusted

    - [ ] T5.3 Manual Dispatch Validation `[activity: manual-test]`
        - [ ] T5.3.1 Dispatch for known store: `php userfrosting/bin/task job:dispatch pos-data-catchup --store=pc00`
        - [ ] T5.3.2 Verify execution logged in TaskEngine dashboard
        - [ ] T5.3.3 Check `posDataGaps` table has entries for pc00
        - [ ] T5.3.4 Dispatch with date override: `php userfrosting/bin/task job:dispatch pos-data-catchup --store=pc00 --payload='{"startDate":"2026-03-01","endDate":"2026-03-15"}'`

    - [ ] T5.4 Boundary API Validation `[activity: manual-test]`
        - [ ] T5.4.1 Test via curl: `curl -X POST https://dev2.buyerkiosk.com/api/pc00/drs/catchup/boundary -H "X-API-Key: {key}" -d '{"earliestDate":"2025-06-01"}'`
        - [ ] T5.4.2 Verify `stores.drsEarliestDataDate` updated
        - [ ] T5.4.3 Verify gaps before 2025-06-01 marked unavailable

    - [ ] T5.5 PHPStan Clean `[activity: lint-code]`
        - [ ] T5.5.1 `cd userfrosting && ./vendor/bin/phpstan analyse src/BuyerKiosk/PosDataCatchup/` — zero errors

    - [ ] T5.6 Specification Compliance `[activity: business-acceptance]`
        - [ ] T5.6.1 PRD Feature 1 (Gap Detection): ✅ All acceptance criteria met
        - [ ] T5.6.2 PRD Feature 2 (Ably Commands): ✅ All acceptance criteria met
        - [ ] T5.6.3 PRD Feature 3 (Gap Tracking): ✅ All acceptance criteria met
        - [ ] T5.6.4 PRD Feature 4 (Manual Dispatch): ✅ All acceptance criteria met
        - [ ] T5.6.5 PRD Feature 5 (Store Data Boundary): ✅ All acceptance criteria met
        - [ ] T5.6.6 PRD Feature 6 (Progress Reporting): ✅ All acceptance criteria met
        - [ ] T5.6.7 SDD ADR compliance: all 6 ADRs implemented as designed
        - [ ] T5.6.8 SDD Schema compliance: posDataGaps table matches spec exactly
        - [ ] T5.6.9 SDD Contract compliance: Ably command payload matches spec exactly
        - [ ] T5.6.10 All 10 PRD business rules verified in tests

    - [ ] T5.7 Build & Deployment Verification `[activity: verify-deployment]`
        - [ ] T5.7.1 Code changes are live on dev2.buyerkiosk.com (no deployment step needed)
        - [ ] T5.7.2 Migration ran successfully
        - [ ] T5.7.3 Job appears in TaskEngine job list
        - [ ] T5.7.4 No PHP errors in `logs/buyerkiosk_com.php.error.log`

---

## Phase Dependency Graph

```
Phase 1 (Migrations)
    ├─── Phase 2 (Core Services: GapTracker + GapDetector)
    │        └──── Phase 4 (Job Orchestrator + Registration)
    └─── Phase 3 (Ably Publisher ║ Boundary Controller)  [parallel]
              └──── Phase 4 (Job Orchestrator + Registration)
                         └──── Phase 5 (Integration & E2E)
```

**Parallel opportunities:**
- T2.1 (GapTracker) and T2.2 (GapDetector) can run in parallel within Phase 2
- T3.1 (Ably Publisher) and T3.2 (Boundary Controller) can run in parallel within Phase 3
- Phase 2 and Phase 3 can run in parallel (both depend only on Phase 1)

**Critical path**: Phase 1 → Phase 2 + Phase 3 (parallel) → Phase 4 → Phase 5

---

## File Summary

### New Files (13)

| File | Phase | Purpose |
|------|-------|---------|
| `userfrosting/migrations/input/20260401_043_001_pos_data_gaps_table.json` | T1 | posDataGaps table migration |
| `userfrosting/migrations/input/20260401_043_002_stores_earliest_data_date.json` | T1 | stores.drsEarliestDataDate column |
| `userfrosting/migrations/input/20260401_043_003_pos_data_catchup_job_def.json` | T1 | Job definition INSERT |
| `userfrosting/src/BuyerKiosk/PosDataCatchup/Persistence/GapTracker.php` | T2 | Gap CRUD operations |
| `userfrosting/src/BuyerKiosk/PosDataCatchup/Services/GapDetector.php` | T2 | Gap detection logic |
| `userfrosting/src/BuyerKiosk/PosDataCatchup/Services/CatchupAblyPublisher.php` | T3 | Ably command publisher |
| `userfrosting/src/BuyerKiosk/PosDataCatchup/Controllers/CatchupBoundaryController.php` | T3 | Boundary API controller |
| `userfrosting/routes/groups/catchup.php` | T3 | Route definitions |
| `userfrosting/src/BuyerKiosk/PosDataCatchup/Jobs/PosDataCatchupJob.php` | T4 | Main TaskEngine job |
| `tests/Unit/PosDataCatchup/GapTrackerTest.php` | T2 | GapTracker unit tests |
| `tests/Unit/PosDataCatchup/GapDetectorTest.php` | T2 | GapDetector unit tests |
| `tests/Unit/PosDataCatchup/CatchupAblyPublisherTest.php` | T3 | Publisher unit tests |
| `tests/Unit/PosDataCatchup/PosDataCatchupJobTest.php` | T4 | Job orchestrator tests |

### Modified Files (2)

| File | Phase | Change |
|------|-------|--------|
| `userfrosting/src/BuyerKiosk/TaskEngine/Commands/TaskCommandFactory.php` | T4 | Register PosDataCatchupJob |
| `userfrosting/routes/api.php` | T3 | Include catchup route group |

### Possibly Modified (1)

| File | Phase | Change |
|------|-------|--------|
| `userfrosting/src/BuyerKiosk/Core/Store.php` | T4 | Add drsEarliestDataDate getter/setter (if not auto-loaded) |
