# Implementation Plan

## 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: name]` - For multi-component features
- `[ref: document/section; lines: X-Y]` - Links to specifications, patterns, or interfaces
- `[activity: type]` - Activity hint for specialist agent selection
- `[depends: TX]` - Explicit dependency on another task

---

## Context Priming

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

**Specification**:

- `docs/specs/037-sms-delivery-cost-tracking/product-requirements.md` - Product Requirements (Features 1-9, acceptance criteria, success metrics)
- `docs/specs/037-sms-delivery-cost-tracking/solution-design.md` - Solution Design (all architecture, interfaces, data models, flows)
- `docs/specs/037-sms-delivery-cost-tracking/README.md` - Architecture decisions log (all 13 ADRs confirmed)

**Key Design Decisions** (from README.md Decisions Log + SDD ADRs):

- ADR-1: Keep 6-state DB enum in `chat_messages`, map to 3-state (`sent`/`delivered`/`failed`) in UI and `billingSmsUsage.deliveryStatus`
- ADR-2: Buy SMS delivery tracking via ChatBridgeService — leverages existing `chat_messages` records
- ADR-3: Cost columns on existing `billingSmsUsage` table — no separate cost table
- ADR-4: Separate webhook routes per provider (`/api/webhooks/twilio-delivery/:token`, `/api/webhooks/vonage-delivery/:token`)
- ADR-5: TaskEngine job (`SmsCostLookupJob`) for async Twilio cost lookup — keeps webhooks < 200ms
- ADR-7: Store lookup via central `billingSmsUsage.providerMessageId` — no cross-DB search
- ADR-9: Ignore unmatched webhook receipts — log and discard
- ADR-12: Demo simulation via TaskEngine delayed job (`DemoDeliverySimulationJob`)

**Implementation Context**:

- Commands to run:
  - `./test.sh --testsuite unit` — Run unit tests
  - `./test.sh` — Run all tests
  - `cd userfrosting && ./vendor/bin/phpunit --filter "ClassName"` — Targeted tests
  - `cd userfrosting && ./vendor/bin/phpstan analyse` — Static analysis
  - `php userfrosting/conductor run` — Run database migrations
  - `php userfrosting/conductor build-css --minify` — Build CSS (after CSS changes)

- Patterns to follow:
  - `userfrosting/src/BuyerKiosk/TaskEngine/Jobs/InvoiceGenerationJob.php` — BaseJob pattern (getName, handle, JobResult)
  - `userfrosting/src/BuyerKiosk/Chat/Controllers/ChatWebhookController.php` — Webhook handler pattern (always 200 OK, log, process)
  - `userfrosting/src/BuyerKiosk/Chat/Events/ChatAblyPublisher.php` — Ably publish pattern (publishDeliveryUpdate exists but unused)
  - `userfrosting/src/BuyerKiosk/Billing/Services/SmsUsageTracker.php` — Never-throw billing tracking pattern
  - `userfrosting/src/BuyerKiosk/Billing/Repositories/SmsUsageRepository.php` — Repository query pattern

- Interfaces to implement:
  - SDD Section "Interface Specifications" — Data Storage Changes (migrations 037_001 through 037_005)
  - SDD Section "Internal API Changes" — 4 new API endpoints
  - SDD Section "Application Data Models" — all new entities and behaviors
  - SDD Section "Integration Points" — inter-component communication

**Implementation Gotchas** (from SDD "Implementation Gotchas"):

- PDO named param reuse causes silent HY093 error — use unique param names
- Twilio `price` is a string (e.g., `"-0.00750"`) — must `abs((float) ...)`
- Vonage DLR can arrive as GET — handler must check both GET params and POST body
- `billingSmsUsage` is in central DB, `chat_messages` is in per-store DB — never JOIN them
- ChatBridgeService is static — webhook handler updates chat_messages directly
- Demo stores return fake provider message ID: `'DEMO_' . uniqid()`

---

## Implementation Phases

### Phase Dependency Graph

```
Phase 1 (Database + Value Objects)
    ↓
Phase 2 (Delivery Processing Core) ← depends on Phase 1
    ↓
Phase 3A (Webhook Handlers) ← depends on Phase 2     [parallel]
Phase 3B (Sender Modifications) ← depends on Phase 1  [parallel with 3A]
    ↓
Phase 4 (TaskEngine Jobs) ← depends on Phase 1 (job classes needed by T3C and senders)
    ↓
Phase 3C (Inbound Cost Tracking) ← depends on Phase 2 + Phase 4
    ↓
--- Integration Checkpoint: Delivery status + cost capture verified end-to-end ---
    ↓
Phase 5A (Chat UI + Ably) ← depends on Phase 2        [parallel]
Phase 5B (Completed Buys UI) ← depends on Phase 1     [parallel with 5A]
Phase 5C (WorkbookToast) ← depends on Phase 5A
    ↓
Phase 6 (Billing Report) ← depends on Phase 1
    ↓
Phase 7 (Integration + E2E Validation) ← depends on all
```

---

- [x] **T1 Phase 1: Database Foundation + Value Objects** ✅ COMPLETED 2026-02-11

    *Delivers: Schema changes, value objects, and data access layer updates that all subsequent phases depend on.*

    - [ ] T1.1 Prime Context
        - [ ] T1.1.1 Read SDD migration specs (037_001 through 037_005) `[ref: SDD/Interface Specifications/Data Storage Changes; lines: 512-611]`
        - [ ] T1.1.2 Read existing `SmsUsageRepository.php` for query patterns `[ref: userfrosting/src/BuyerKiosk/Billing/Repositories/SmsUsageRepository.php]`
        - [ ] T1.1.3 Read existing `SmsUsageTracker.php` and `SmsUsageTrackerInterface.php` for method signatures `[ref: userfrosting/src/BuyerKiosk/Billing/Services/SmsUsageTracker.php]`
        - [ ] T1.1.4 Read existing `SmsUsageRecord.php` model `[ref: userfrosting/src/BuyerKiosk/Billing/Models/SmsUsageRecord.php]`
        - [ ] T1.1.5 Read existing `SmsCategory.php` enum `[ref: userfrosting/src/BuyerKiosk/Billing/Enums/SmsCategory.php]`
        - [ ] T1.1.6 Read `BaseJob.php` for job class pattern `[ref: userfrosting/src/BuyerKiosk/TaskEngine/Domain/Job/BaseJob.php]`
        - [ ] T1.1.7 Review existing migration JSON format from 036 migrations `[ref: userfrosting/migrations/input/]`

    - [ ] T1.2 Write Tests
        - [ ] T1.2.1 Unit tests for `DeliveryStatusMapper::mapTwilioStatus()` — all Twilio statuses map to correct 3-state `[ref: PRD/Feature 1 acceptance criteria; SDD/DeliveryStatusMapper]` `[activity: write-unit-tests]`
        - [ ] T1.2.2 Unit tests for `DeliveryStatusMapper::mapVonageStatus()` — all Vonage statuses map correctly `[ref: PRD/Feature 1 acceptance criteria]` `[activity: write-unit-tests]`
        - [ ] T1.2.3 Unit tests for `DeliveryStatusMapper::getErrorDescription()` — Twilio and Vonage error codes translate to human-readable strings `[ref: PRD/Feature 3 - error translations]` `[activity: write-unit-tests]`
        - [ ] T1.2.4 Unit tests for `MappedStatus` value object — `isFinal` returns true for delivered/failed, false for sent `[activity: write-unit-tests]`
        - [ ] T1.2.5 Unit tests for `SmsUsageTracker::updateDeliveryStatus()` — updates correct columns `[ref: SDD/Application Data Models/SmsUsageTracker]` `[activity: write-unit-tests]`
        - [ ] T1.2.6 Unit tests for `SmsUsageTracker::updateCost()` — sets providerCostUsd, costStatus, costCapturedAt `[activity: write-unit-tests]`
        - [ ] T1.2.7 Unit tests for `SmsUsageTracker::markCostUnknown()` — sets costStatus=unknown `[activity: write-unit-tests]`
        - [ ] T1.2.8 Unit tests for `SmsUsageTracker::incrementCostRetry()` — increments and returns new count `[activity: write-unit-tests]`

    - [ ] T1.3 Implement Database Migrations `[activity: data-architecture]`
        - [ ] T1.3.1 Create migration `20260211_037_001_billing_sms_cost_columns.json` — add cost/delivery columns + `inbound` enum value to `billingSmsUsage` `[ref: SDD/Migration 037_001]`
        - [ ] T1.3.2 Create migration `20260211_037_002_sms_webhook_log.json` — create `smsWebhookLog` table `[ref: SDD/Migration 037_002]`
        - [ ] T1.3.3 Create migration `20260211_037_003_sms_cost_lookup_job.json` — register `SmsCostLookupJob` in `taskJobDefinitions` `[ref: SDD/Migration 037_003]`
        - [ ] T1.3.4 Create migration `20260211_037_004_demo_simulation_job.json` — register `DemoDeliverySimulationJob` `[ref: SDD/Migration 037_004]`
        - [ ] T1.3.5 Create migration `20260211_037_004b_stale_cost_sweep_job.json` — register `SmsStaleCostSweepJob` (daily cron) `[ref: SDD/Migration 037_004b]`
        - [ ] T1.3.6 Create migration `20260211_037_005_enable_embedded_chat.json` — auto-enable `workbook_embedded_chat` flag `[ref: SDD/Migration 037_005]`
        - [ ] T1.3.7 Run migrations and verify: `php userfrosting/conductor run` `[activity: run-migrations]`

    - [ ] T1.4 Implement Value Objects `[activity: domain-modeling]`
        - [ ] T1.4.1 Create `userfrosting/src/BuyerKiosk/SMS/Webhooks/DeliveryStatusMapper.php` — provider status mapping + error description translation `[ref: SDD/DeliveryStatusMapper entity]`
        - [ ] T1.4.2 Create `userfrosting/src/BuyerKiosk/SMS/Webhooks/MappedStatus.php` — value object with displayStatus, rawStatus, isFinal `[ref: SDD/MappedStatus entity]`
        - [ ] T1.4.3 Create `userfrosting/src/BuyerKiosk/SMS/Webhooks/ProcessingResult.php` — value object for delivery processing outcome `[ref: SDD/ProcessingResult entity]`
        - [ ] T1.4.4 Update `SmsCategory.php` enum — add `inbound` case if not present `[ref: SDD/Migration 037_001 smsCategory ALTER]`

    - [ ] T1.5 Implement Data Access Layer Updates `[activity: backend-implementation]`
        - [ ] T1.5.1 Update `SmsUsageTrackerInterface.php` — add method signatures for `updateDeliveryStatus()`, `updateCost()`, `markCostUnknown()`, `incrementCostRetry()` `[ref: SDD/SmsUsageTracker MODIFIED]`
        - [ ] T1.5.2 Update `SmsUsageTracker.php` — implement new methods with never-throw pattern `[ref: SDD/SmsUsageTracker MODIFIED]`
        - [ ] T1.5.3 Update `NullSmsUsageTracker.php` — add no-op implementations for new interface methods
        - [ ] T1.5.4 Update `SmsUsageRepository.php` — add `findByProviderMessageId()`, cost update queries, delivery status update queries `[ref: SDD/SmsUsageRepository MODIFY]`
        - [ ] T1.5.5 Update `SmsUsageRecord.php` model — add new fields (providerCostUsd, costStatus, deliveryStatus, etc.) `[ref: SDD/Migration 037_001 columns]`

    - [ ] T1.6 Validate
        - [ ] T1.6.1 Run unit tests for DeliveryStatusMapper and MappedStatus `[activity: run-tests]`
        - [ ] T1.6.2 Run unit tests for SmsUsageTracker new methods `[activity: run-tests]`
        - [ ] T1.6.3 Run PHPStan analysis on modified files `[activity: lint-code]`
        - [ ] T1.6.4 Verify migrations applied cleanly `[activity: run-tests]`
        - [ ] T1.6.5 Verify SDD column definitions match actual migration output `[activity: business-acceptance]`

    **Definition of Done**: All 6 migrations applied. DeliveryStatusMapper maps all provider statuses correctly. SmsUsageTracker has 4 new methods passing tests. SmsUsageRecord model reflects new columns. PHPStan clean.

    ### Phase 1 Review Summary (2026-02-11)

    **Codex Review Findings:**
    - CRITICAL: Missing Twilio `canceled`/`read` and Vonage `unknown` status mappings → Fixed
    - CRITICAL: Migration 037_005 targeted wrong column for feature flag → Fixed (uses `stores.embeddedChatEnabled`)
    - IMPORTANT: ProcessingResult missing `mappedStatus`/`typeNum` fields → Fixed, aligned with SDD
    - IMPORTANT: Repository idempotent updates returning false on no-op → Fixed (check record exists)
    - LOW: `markCostUnknown()` dropped `$reason` parameter → Fixed (now logged)
    - IMPORTANT: Missing tests for new statuses + ProcessingResult → Added 7 new tests

    **Changes Made:** 3 core files + test files updated. 46 tests, 136 assertions, all passing. PHPStan clean.
    **Rejected Suggestions:** None — all findings were valid.
    **Deferred Items:** None.

---

- [x] **T2 Phase 2: Delivery Processing Core** `[depends: T1]` ✅ COMPLETED 2026-02-11

    *Delivers: The central `DeliveryStatusProcessor` that orchestrates all delivery status updates across databases, Ably, and cost jobs.*

    - [ ] T2.1 Prime Context
        - [ ] T2.1.1 Read SDD DeliveryStatusProcessor entity and example code `[ref: SDD/Application Data Models/DeliveryStatusProcessor; lines: 748-789]`
        - [ ] T2.1.2 Read SDD "Primary Flow: Delivery Receipt Processing" sequence diagram `[ref: SDD/Runtime View; lines: 1328-1370]`
        - [ ] T2.1.3 Read SDD idempotency strategy `[ref: SDD/Error Handling; lines: 1456-1458]`
        - [ ] T2.1.4 Read `ChatAblyPublisher.php` — understand `publishDeliveryUpdate()` signature `[ref: userfrosting/src/BuyerKiosk/Chat/Events/ChatAblyPublisher.php]`
        - [ ] T2.1.5 Read SDD tracking events map `[ref: SDD/Tracking Events Map; lines: 1701-1718]`

    - [ ] T2.2 Write Tests
        - [ ] T2.2.1 Test: Twilio "delivered" status updates billingSmsUsage, chat_messages, publishes Ably, dispatches cost job `[ref: SDD/Test Scenario 1; PRD/Feature 1]` `[activity: write-unit-tests]`
        - [ ] T2.2.2 Test: Vonage "failed" status updates both DBs, includes human-readable error, publishes Ably with errorReason `[ref: SDD/Test Scenario 2; PRD/Feature 1]` `[activity: write-unit-tests]`
        - [ ] T2.2.3 Test: Unmatched providerMessageId returns failure result, no DB updates `[ref: SDD/Test Scenario 4; PRD/Feature 1a]` `[activity: write-unit-tests]`
        - [ ] T2.2.4 Test: Idempotency — duplicate webhook with same rawStatus is no-op `[ref: SDD/Error Handling idempotency]` `[activity: write-unit-tests]`
        - [ ] T2.2.5 Test: Forward-only transitions — "delivered" cannot be overwritten by "sent" `[ref: SDD/Error Handling idempotency]` `[activity: write-unit-tests]`
        - [ ] T2.2.6 Test: Store DB failure is non-fatal — billingSmsUsage still updated, result.chatMessageUpdated = false `[ref: SDD/Error Handling]` `[activity: write-unit-tests]`
        - [ ] T2.2.7 Test: Ably publish failure is non-fatal — result.ablyPublished = false, no exception thrown `[ref: SDD/Error Handling]` `[activity: write-unit-tests]`
        - [ ] T2.2.8 Test: Twilio final status dispatches SmsCostLookupJob; non-final ("sent") does NOT dispatch `[ref: SDD/DeliveryStatusProcessor step 6]` `[activity: write-unit-tests]`
        - [ ] T2.2.9 Test: Vonage delivery does NOT dispatch cost job (cost comes in DLR) `[activity: write-unit-tests]`
        - [ ] T2.2.10 Test: Tracking events emitted for delivered, failed, and webhook processing failures `[ref: SDD/Tracking Events Map]` `[activity: write-unit-tests]`

    - [ ] T2.3 Implement `[activity: backend-implementation]`
        - [ ] T2.3.1 Create `userfrosting/src/BuyerKiosk/SMS/Webhooks/DeliveryStatusProcessor.php` — full processing flow per SDD example `[ref: SDD/Example: DeliveryStatusProcessor Core Logic; lines: 934-1018]`
        - [ ] T2.3.2 Implement `processDeliveryUpdate()` method — 6-step flow: map status, lookup usage, update central DB, update store DB, publish Ably, dispatch cost job
        - [ ] T2.3.3 Implement `updateChatMessage()` private method — UPDATE chat_messages in store DB by provider_message_id
        - [ ] T2.3.4 Implement `getCustomerName()` private method — fetch from chat_threads.customer for Ably payload
        - [ ] T2.3.5 Implement idempotency check — compare deliveryUpdatedAt + providerRawStatus before processing
        - [ ] T2.3.6 Implement tracking event emission at each step per Tracking Events Map

    - [ ] T2.4 Validate
        - [ ] T2.4.1 Run all DeliveryStatusProcessor unit tests `[activity: run-tests]`
        - [ ] T2.4.2 Run PHPStan on new class `[activity: lint-code]`
        - [ ] T2.4.3 Verify all 10 test scenarios pass `[activity: run-tests]`
        - [ ] T2.4.4 Verify ProcessingResult captures all operation outcomes `[activity: review-code]`
        - [ ] T2.4.5 Verify PRD Feature 1 acceptance criteria mapped to tests `[activity: business-acceptance]`

    **Definition of Done**: DeliveryStatusProcessor handles all status transitions for both providers. Idempotency enforced. Non-critical failures (Ably, store DB) are non-fatal. All 10 test scenarios pass. PHPStan clean.

    ### Phase 2 Review Summary (2026-02-11)

    **Codex Review Findings:**
    - CRITICAL: Method signature missing `errorMessage` param (type error risk) → Fixed
    - CRITICAL: Forward-only logic allowed `delivered → failed` → Fixed (delivered/failed are terminal)
    - IMPORTANT: Idempotency blocked Vonage cost re-capture → Fixed (cost decoupled from status)
    - IMPORTANT: Central DB failure didn't short-circuit downstream → Fixed
    - IMPORTANT: chat_messages update didn't persist error info → Fixed
    - MEDIUM: Tracking event names mismatched SDD → Fixed (aligned)
    - MEDIUM: Ably channel pattern verified correct

    **Changes Made:** DeliveryStatusProcessor + tests updated. 15 tests, 109 assertions, all passing. PHPStan clean.
    **Rejected Suggestions:** None — all findings were valid and actionable.

---

- [x] **T3 Phase 3: Webhook Handlers + Sender Modifications + Inbound Cost** (T3A+T3B COMPLETED 2026-02-11, T3C pending Phase 4)

    *Delivers: The HTTP endpoints that receive provider callbacks, sender modifications to trigger callbacks, and inbound cost tracking.*

    - [ ] T3A Webhook Handlers `[parallel: true]` `[component: webhook-handlers]` `[depends: T2]`

        - [ ] T3A.1 Prime Context
            - [ ] T3A.1.1 Read SDD TwilioDeliveryHandler and VonageDeliveryHandler entities `[ref: SDD/Application Data Models; lines: 843-864]`
            - [ ] T3A.1.2 Read SDD webhook route definitions `[ref: SDD/Integration Points; lines: 875-926]`
            - [ ] T3A.1.3 Read existing `ChatWebhookController.php` for handler pattern `[ref: userfrosting/src/BuyerKiosk/Chat/Controllers/ChatWebhookController.php]`
            - [ ] T3A.1.4 Read existing `userfrosting/routes/chat/webhooks.php` for route registration pattern `[ref: userfrosting/routes/chat/webhooks.php]`
            - [ ] T3A.1.5 Read SDD test scenarios for invalid token + unmatched messages `[ref: SDD/Test Specifications; lines: 1827-1881]`

        - [ ] T3A.2 Write Tests
            - [ ] T3A.2.1 Test: TwilioDeliveryHandler with valid token processes webhook and returns 200 `[ref: SDD/Scenario 1; PRD/Feature 1]` `[activity: write-unit-tests]`
            - [ ] T3A.2.2 Test: TwilioDeliveryHandler with invalid token returns 200, logs error, no processing `[ref: SDD/Scenario 3; PRD/Feature 1]` `[activity: write-unit-tests]`
            - [ ] T3A.2.3 Test: TwilioDeliveryHandler logs raw payload to smsWebhookLog `[ref: PRD/Feature 1a]` `[activity: write-unit-tests]`
            - [ ] T3A.2.4 Test: TwilioDeliveryHandler catches processing exceptions, still returns 200 `[ref: SDD/Error Handling webhook failure]` `[activity: write-unit-tests]`
            - [ ] T3A.2.5 Test: VonageDeliveryHandler processes GET request (params in query string) `[ref: SDD/VonageDeliveryHandler; PRD/Feature 1]` `[activity: write-unit-tests]`
            - [ ] T3A.2.6 Test: VonageDeliveryHandler processes POST request (params in body) `[ref: SDD/VonageDeliveryHandler]` `[activity: write-unit-tests]`
            - [ ] T3A.2.7 Test: VonageDeliveryHandler extracts price and calls updateCost() directly `[ref: SDD/VonageDeliveryHandler step 5; PRD/Feature 4]` `[activity: write-unit-tests]`
            - [ ] T3A.2.8 Test: VonageDeliveryHandler with invalid token returns 200, logs error `[activity: write-unit-tests]`
            - [ ] T3A.2.9 Test: Malformed payload returns 200, logs full payload `[ref: SDD/Error Handling malformed webhook]` `[activity: write-unit-tests]`

        - [ ] T3A.3 Implement `[activity: backend-implementation]`
            - [ ] T3A.3.1 Create `userfrosting/src/BuyerKiosk/SMS/Webhooks/TwilioDeliveryHandler.php` `[ref: SDD/TwilioDeliveryHandler entity]`
            - [ ] T3A.3.2 Create `userfrosting/src/BuyerKiosk/SMS/Webhooks/VonageDeliveryHandler.php` — handle both GET + POST `[ref: SDD/VonageDeliveryHandler entity]`
            - [ ] T3A.3.3 Create `userfrosting/routes/sms/webhooks.php` — register delivery webhook routes `[ref: SDD/Directory Map]`
            - [ ] T3A.3.4 Register new route file in application bootstrap (initialize.php or route loader)
            - [ ] T3A.3.5 Implement smsWebhookLog insertion in both handlers (provider, webhookType, rawPayload, processingResult, processingTimeMs)
            - [ ] T3A.3.6 Implement `sms.webhook.received` tracking event emission `[ref: SDD/Tracking Events Map]`

        - [ ] T3A.4 Validate
            - [ ] T3A.4.1 Run all webhook handler tests `[activity: run-tests]`
            - [ ] T3A.4.2 Run PHPStan on new files `[activity: lint-code]`
            - [ ] T3A.4.3 Verify route registration works (no conflicts with existing routes) `[activity: review-code]`
            - [ ] T3A.4.4 Verify PRD Feature 1 and 1a acceptance criteria `[activity: business-acceptance]`

        **Definition of Done (T3A)**: Both webhook endpoints registered and accessible. Token validation works. Payload logging to smsWebhookLog works. All webhook tests pass. Routes don't conflict.

    - [ ] T3B Sender Modifications `[parallel: true]` `[component: sms-senders]` `[depends: T1]`

        - [ ] T3B.1 Prime Context
            - [ ] T3B.1.1 Read `TwilioTextSender.php` — identify where StatusCallback URL should be added `[ref: userfrosting/src/BuyerKiosk/SMS/TextMessageService/TwilioTextSender.php]`
            - [ ] T3B.1.2 Read `VonageTextSender.php` — identify existing delivery receipt URL config `[ref: userfrosting/src/BuyerKiosk/SMS/TextMessageService/VonageTextSender.php]`
            - [ ] T3B.1.3 Read SDD sender modification spec `[ref: SDD/External System Integration; lines: 905-914]`
            - [ ] T3B.1.4 Read SDD demo store simulation pattern `[ref: SDD/Runtime View/Secondary Flow: Demo Store Simulation; lines: 1438-1454]`
            - [ ] T3B.1.5 Read `ChatApiController.php` — verify provider_message_id and delivery_status='sent' persistence on outbound sends `[ref: userfrosting/src/BuyerKiosk/Chat/Controllers/ChatApiController.php; SDD/ChatApiController MODIFY]`

        - [ ] T3B.2 Write Tests
            - [ ] T3B.2.1 Test: TwilioTextSender includes `StatusCallback` URL in messages->create() `[ref: PRD/Feature 7]` `[activity: write-unit-tests]`
            - [ ] T3B.2.2 Test: TwilioTextSender returns provider message ID in response `[ref: PRD/Feature 7]` `[activity: write-unit-tests]`
            - [ ] T3B.2.3 Test: VonageTextSender uses updated webhook URL with VONAGE_WEBHOOK_SECRET `[ref: PRD/Feature 7]` `[activity: write-unit-tests]`
            - [ ] T3B.2.4 Test: VonageTextSender returns provider message ID `[ref: PRD/Feature 7]` `[activity: write-unit-tests]`
            - [ ] T3B.2.5 Test: Demo mode dispatches `DemoDeliverySimulationJob` instead of expecting real webhook `[ref: PRD/Feature 9; SDD/ADR-12]` `[activity: write-unit-tests]`
            - [ ] T3B.2.6 Test: Demo mode does NOT include StatusCallback URL (no real webhook expected) `[activity: write-unit-tests]`
            - [ ] T3B.2.7 Test: ChatApiController stores provider_message_id on outbound send response `[ref: SDD/ChatApiController MODIFY; PRD/Feature 7]` `[activity: write-unit-tests]`
            - [ ] T3B.2.8 Test: ChatApiController sets delivery_status='sent' on outbound chat_messages `[ref: SDD/ChatApiController MODIFY]` `[activity: write-unit-tests]`

        - [ ] T3B.3 Implement `[activity: backend-implementation]`
            - [ ] T3B.3.1 Modify `TwilioTextSender.php` — add `'StatusCallback' => $callbackUrl` to `messages->create()` params `[ref: SDD/TwilioTextSender MODIFY]`
            - [ ] T3B.3.2 Modify `TwilioTextSender.php` — dispatch `DemoDeliverySimulationJob` when `$store->getDev()` is true
            - [ ] T3B.3.3 Modify `VonageTextSender.php` — update delivery receipt URL from old path to `/api/webhooks/vonage-delivery/{VONAGE_WEBHOOK_SECRET}` `[ref: SDD/VonageTextSender MODIFY]`
            - [ ] T3B.3.4 Modify `VonageTextSender.php` — dispatch `DemoDeliverySimulationJob` in demo mode
            - [ ] T3B.3.5 Verify/modify `ChatApiController.php` — ensure `provider_message_id` is stored in `chat_messages` on outbound sends and `delivery_status` is set to `'sent'` `[ref: SDD/ChatApiController MODIFY]`

        - [ ] T3B.4 Validate
            - [ ] T3B.4.1 Run sender unit tests `[activity: run-tests]`
            - [ ] T3B.4.2 Run existing SMS test suite to ensure no regression `[activity: run-tests]`
            - [ ] T3B.4.3 Verify PRD Feature 7 + Feature 9 acceptance criteria `[activity: business-acceptance]`

        **Definition of Done (T3B)**: Both senders include callback URLs. Provider message ID returned. Demo mode dispatches simulation job. ChatApiController persists provider_message_id + delivery_status='sent'. No SMS send regressions.

    - [ ] T3C Inbound SMS Cost Tracking `[component: inbound-cost]` `[depends: T2, T4]`

        *NOTE: T3C depends on T4 because it dispatches SmsCostLookupJob for Twilio inbound. Must be implemented after T4.*

        - [ ] T3C.1 Prime Context
            - [ ] T3C.1.1 Read SDD "Secondary Flow: Inbound SMS Cost Capture" `[ref: SDD/Runtime View; lines: 1402-1436]`
            - [ ] T3C.1.2 Read `ChatWebhookController.php` — identify `handleInbound()` method and insertion point `[ref: userfrosting/src/BuyerKiosk/Chat/Controllers/ChatWebhookController.php]`
            - [ ] T3C.1.3 Read `ChatMatchingService.php` — understand how typeNum is resolved for inbound `[ref: userfrosting/src/BuyerKiosk/Chat/Services/ChatMatchingService.php]`

        - [ ] T3C.2 Write Tests
            - [ ] T3C.2.1 Test: Vonage inbound — cost extracted from webhook `message-price` field and stored immediately `[ref: PRD/Feature 5; SDD/Inbound flow Vonage path]` `[activity: write-unit-tests]`
            - [ ] T3C.2.2 Test: Twilio inbound — SmsCostLookupJob dispatched with inbound message SID `[ref: PRD/Feature 5; SDD/Inbound flow Twilio path]` `[activity: write-unit-tests]`
            - [ ] T3C.2.3 Test: Inbound usage logged with smsCategory='inbound' `[ref: PRD/Feature 5]` `[activity: write-unit-tests]`
            - [ ] T3C.2.4 Test: Inbound cost tracking failure does NOT block message processing `[ref: SDD/Error Handling never-throw]` `[activity: write-unit-tests]`
            - [ ] T3C.2.5 Test: `sms.inbound.received` tracking event emitted `[ref: SDD/Tracking Events Map]` `[activity: write-unit-tests]`

        - [ ] T3C.3 Implement `[activity: backend-implementation]`
            - [ ] T3C.3.1 Modify `ChatWebhookController.php` — after saving inbound chat_message, call `SmsUsageTracker::logUsage()` with `smsCategory='inbound'` `[ref: SDD/ChatWebhookController MODIFY]`
            - [ ] T3C.3.2 Implement Vonage inbound cost extraction — extract `message-price` from webhook payload, call `updateCost()` immediately
            - [ ] T3C.3.3 Implement Twilio inbound cost dispatch — dispatch `SmsCostLookupJob` with inbound MessageSid
            - [ ] T3C.3.4 Wrap all inbound cost tracking in try/catch (never block message processing)

        - [ ] T3C.4 Validate
            - [ ] T3C.4.1 Run inbound cost tracking tests `[activity: run-tests]`
            - [ ] T3C.4.2 Verify existing ChatWebhookController tests still pass `[activity: run-tests]`
            - [ ] T3C.4.3 Verify PRD Feature 5 acceptance criteria `[activity: business-acceptance]`

        **Definition of Done (T3C)**: Inbound messages logged with smsCategory='inbound'. Vonage inbound cost captured immediately. Twilio inbound dispatches cost lookup job. No inbound message processing regressions.

---

- [x] **T4 Phase 4: TaskEngine Jobs** `[depends: T1]` ✅ COMPLETED 2026-02-11

    *Delivers: Background jobs for async Twilio cost lookup, demo store simulation, and stale cost sweep. Depends on T1 (migrations create job definitions + cost columns). T3B and T3C dispatch these jobs, so T4 should be implemented early.*

    - [ ] T4.1 Prime Context
        - [ ] T4.1.1 Read `InvoiceGenerationJob.php` for BaseJob patterns `[ref: userfrosting/src/BuyerKiosk/TaskEngine/Jobs/InvoiceGenerationJob.php]`
        - [ ] T4.1.2 Read `BaseJob.php` for lifecycle hooks and JobResult `[ref: userfrosting/src/BuyerKiosk/TaskEngine/Domain/Job/BaseJob.php]`
        - [ ] T4.1.3 Read SDD SmsCostLookupJob example code `[ref: SDD/Example: Twilio Cost Lookup Job; lines: 1021-1080]`
        - [ ] T4.1.4 Read SDD DemoDeliverySimulationJob entity `[ref: SDD/Application Data Models; lines: 804-818]`
        - [ ] T4.1.5 Read SDD SmsStaleCostSweepJob entity `[ref: SDD/Application Data Models; lines: 819-829]`
        - [ ] T4.1.6 Read SDD cost lookup retry flow `[ref: SDD/Runtime View/Secondary Flow: Twilio Cost Lookup; lines: 1372-1400]`

    - [ ] T4.2 SmsCostLookupJob `[component: taskengine-jobs]`

        - [ ] T4.2.1 Write Tests
            - [ ] T4.2.1.1 Test: Twilio API returns price — updateCost() called with abs(float) value `[ref: SDD/Scenario 5; PRD/Feature 4]` `[activity: write-unit-tests]`
            - [ ] T4.2.1.2 Test: Twilio API returns null price, retries < 3 — returns JobResult::failure for retry `[ref: SDD/Scenario 6]` `[activity: write-unit-tests]`
            - [ ] T4.2.1.3 Test: Twilio API returns null price, retries exhausted — markCostUnknown() `[ref: SDD/Scenario 6; PRD/Feature 4]` `[activity: write-unit-tests]`
            - [ ] T4.2.1.4 Test: Twilio 404 error — markCostUnknown('Message not found') `[ref: SDD/SmsCostLookupJob]` `[activity: write-unit-tests]`
            - [ ] T4.2.1.5 Test: Twilio transient error (500) — returns failure for retry `[activity: write-unit-tests]`
            - [ ] T4.2.1.6 Test: Non-Twilio provider — skip with success `[ref: SDD/SmsCostLookupJob]` `[activity: write-unit-tests]`
            - [ ] T4.2.1.7 Test: `sms.cost.retry_attempted` tracking event emitted on retry `[ref: SDD/Tracking Events Map]` `[activity: write-unit-tests]`
            - [ ] T4.2.1.8 Test: `sms.outbound.cost_captured` event on success, `sms.outbound.cost_unknown` on final failure `[ref: SDD/Tracking Events Map]` `[activity: write-unit-tests]`

        - [ ] T4.2.2 Implement `[activity: backend-implementation]`
            - [ ] T4.2.2.1 Create `userfrosting/src/BuyerKiosk/TaskEngine/Jobs/SmsCostLookupJob.php` — extending BaseJob `[ref: SDD/SmsCostLookupJob entity]`
            - [ ] T4.2.2.2 Implement `handle()` — Twilio API call, price extraction, abs(float) conversion, retry logic
            - [ ] T4.2.2.3 Verify migration 037_003 job definition has correct retry config: `maxRetries: 3`, `retryDelaySeconds: 3600` (1 hour between retries = 3 retries over ~4 hours per PRD requirement) `[ref: SDD/Migration 037_003; PRD/Feature 4 retry requirement]`

    - [ ] T4.3 DemoDeliverySimulationJob `[component: taskengine-jobs]`

        - [ ] T4.3.1 Write Tests
            - [ ] T4.3.1.1 Test: Updates chat_messages.delivery_status = 'delivered' in store DB `[ref: SDD/Scenario 7; PRD/Feature 9]` `[activity: write-unit-tests]`
            - [ ] T4.3.1.2 Test: Updates billingSmsUsage deliveryStatus + sets cost to $0.00 with costStatus='captured' `[ref: SDD/Scenario 7]` `[activity: write-unit-tests]`
            - [ ] T4.3.1.3 Test: Publishes Ably delivery event `[ref: SDD/DemoDeliverySimulationJob step 5]` `[activity: write-unit-tests]`

        - [ ] T4.3.2 Implement `[activity: backend-implementation]`
            - [ ] T4.3.2.1 Create `userfrosting/src/BuyerKiosk/TaskEngine/Jobs/DemoDeliverySimulationJob.php` — extending BaseJob `[ref: SDD/DemoDeliverySimulationJob entity]`
            - [ ] T4.3.2.2 Implement `handle()` — sleep 3-5s, update both DBs, set $0 cost, publish Ably

    - [ ] T4.4 SmsStaleCostSweepJob `[component: taskengine-jobs]`

        - [ ] T4.4.1 Write Tests
            - [ ] T4.4.1.1 Test: Finds stale records (costStatus='pending' > 24h old) `[ref: SDD/SmsStaleCostSweepJob]` `[activity: write-unit-tests]`
            - [ ] T4.4.1.2 Test: Dispatches SmsCostLookupJob for stale Twilio records `[ref: SDD/Error Handling stale sweep]` `[activity: write-unit-tests]`
            - [ ] T4.4.1.3 Test: Marks Vonage stale records as costStatus='unknown' immediately `[ref: SDD/Error Handling stale sweep]` `[activity: write-unit-tests]`
            - [ ] T4.4.1.4 Test: Returns JobResult::success with count of processed records `[activity: write-unit-tests]`

        - [ ] T4.4.2 Implement `[activity: backend-implementation]`
            - [ ] T4.4.2.1 Create `userfrosting/src/BuyerKiosk/TaskEngine/Jobs/SmsStaleCostSweepJob.php` — extending BaseJob `[ref: SDD/SmsStaleCostSweepJob entity]`
            - [ ] T4.4.2.2 Implement `handle()` — query stale records, dispatch/mark per provider, log stats

    - [ ] T4.5 Validate
        - [ ] T4.5.1 Run all TaskEngine job tests `[activity: run-tests]`
        - [ ] T4.5.2 Run PHPStan on all new job files `[activity: lint-code]`
        - [ ] T4.5.3 Verify PRD Feature 4 (outbound cost) + Feature 5 (inbound cost) + Feature 9 (demo sim) acceptance criteria `[activity: business-acceptance]`

    **Definition of Done (T4)**: All 3 jobs created and extending BaseJob. SmsCostLookupJob handles price extraction, retry, and unknown marking. DemoDeliverySimulationJob updates both DBs and publishes Ably. StaleCostSweepJob finds and processes stale records. All job tests pass. PHPStan clean.

---

- [ ] **T4.5b Mid-Plan Integration Checkpoint** `[depends: T2, T3A, T3B, T4]`

    *Gate: Verify the core backend delivery + cost pipeline works end-to-end before investing in UI.*

    - [ ] T4.5b.1 Integration test: Simulate Twilio outbound → webhook delivery receipt → DeliveryStatusProcessor updates billingSmsUsage + chat_messages → SmsCostLookupJob dispatched `[activity: write-integration-tests]`
    - [ ] T4.5b.2 Integration test: Simulate Vonage outbound → DLR webhook → status + cost updated in one pass `[activity: write-integration-tests]`
    - [ ] T4.5b.3 Integration test: Demo store sender → DemoDeliverySimulationJob → status='delivered', cost=$0 `[activity: write-integration-tests]`
    - [ ] T4.5b.4 Run full unit test suite: `./test.sh --testsuite unit` — all pass, no regressions `[activity: run-tests]`
    - [ ] T4.5b.5 Run PHPStan on all new/modified backend files `[activity: lint-code]`

    **Definition of Done**: All delivery status transitions work. Cost capture works for both providers. Webhook handlers log to smsWebhookLog. Demo simulation works. No test regressions.

---

- [x] **T5 Phase 5: Frontend — Chat UI, Completed Buys, and WorkbookToast** ✅ COMPLETED 2026-02-11

    *Delivers: Real-time delivery status in Chat, delivery status column on Completed Buys grid, and global toast notifications for delivery failures.*

    - [ ] T5A Chat UI Delivery Status `[parallel: true]` `[component: frontend-chat]` `[depends: T2]`

        - [ ] T5A.1 Prime Context
            - [ ] T5A.1.1 Read chat panel content template `[ref: userfrosting/templates/themes/default/workspace/partials/chat-panel-content.html]`
            - [ ] T5A.1.2 Read chat-ably-sync.js — existing `workbook:chat:delivered` subscription `[ref: public_html/js/workspace/modules/chat/chat-ably-sync.js]`
            - [ ] T5A.1.3 Read chat-conversation.js — message rendering `[ref: public_html/js/workspace/modules/chat/chat-conversation.js]`
            - [ ] T5A.1.4 Read SDD Ably payload spec (threadId, messageId, status, errorReason, customerName, typeNum) `[ref: SDD/Integration Points Ably; lines: 917-926]`

        - [ ] T5A.2 Write Tests (Manual test scenarios — frontend JS without test framework)
            - [ ] T5A.2.1 Define: Outbound messages show delivery status icons (gray check / green double-check / red X) `[ref: PRD/Feature 3]`
            - [ ] T5A.2.2 Define: Status updates in real-time via Ably without refresh `[ref: PRD/Feature 3]`
            - [ ] T5A.2.3 Define: Failed messages show "Message Failed" inline in chat timeline `[ref: PRD/Feature 3]`
            - [ ] T5A.2.4 Define: Clicking failed message shows human-readable error tooltip `[ref: PRD/Feature 3]`

        - [ ] T5A.3 Implement `[activity: frontend-implementation]`
            - [ ] T5A.3.1 Update `chat-panel-content.html` — update FA icons to FA6 native (`fa-solid fa-check`, `fa-solid fa-check-double`, `fa-solid fa-circle-xmark`) `[ref: SDD/ADR-13]`
            - [ ] T5A.3.2 Update `chat-panel-content.html` — add delivery status icon rendering in outbound message template
            - [ ] T5A.3.3 Update `chat-panel-content.html` — add "Message Failed" inline status template (styled like existing system messages)
            - [ ] T5A.3.4 Update `chat-panel-content.html` — add error tooltip/expandable detail on failed messages
            - [ ] T5A.3.5 Update `chat-ably-sync.js` — enhance `workbook:chat:delivered` handler to update message DOM with new status icon `[ref: SDD/Ably event handler]`
            - [ ] T5A.3.6 Update `chat-conversation.js` — render delivery status from message data on initial load
            - [ ] T5A.3.7 Add CSS for delivery status icons (colors using design token vars: `--bs-success` for delivered, `--bs-danger` for failed, `--bs-secondary` for sent)

        - [ ] T5A.4 Validate
            - [ ] T5A.4.1 Visual inspection: sent message shows gray checkmark `[activity: review-code]`
            - [ ] T5A.4.2 Visual inspection: Ably event updates icon to green double-check `[activity: review-code]`
            - [ ] T5A.4.3 Visual inspection: Failed message shows red X + "Message Failed" inline `[activity: review-code]`
            - [ ] T5A.4.4 Verify PRD Feature 3 acceptance criteria `[activity: business-acceptance]`

    - [ ] T5B Completed Buys Delivery Status `[parallel: true]` `[component: frontend-completed]` `[depends: T1]`

        - [ ] T5B.1 Prime Context
            - [ ] T5B.1.1 Read `completed-buys.html` and `completed-buys-grid.html` templates `[ref: userfrosting/templates/themes/default/workspace/partials/completed/completed-buys.html]`
            - [ ] T5B.1.2 Read `CompletedManager.js` — DataTables initialization `[ref: public_html/js/workspace/modules/completed/CompletedManager.js]`
            - [ ] T5B.1.3 Read `completed-buys-grid.js` `[ref: public_html/js/workspace/modules/completed/completed-buys-grid.js]`

        - [ ] T5B.2 Write Tests (Manual test scenarios)
            - [ ] T5B.2.1 Define: Each completed buy row shows delivery status (Sent/Delivered/Failed icon) `[ref: PRD/Feature 2]`
            - [ ] T5B.2.2 Define: Status visible as icon directly in row without clicking `[ref: PRD/Feature 2]`
            - [ ] T5B.2.3 Define: Failed status uses `text-danger` color `[ref: PRD/Feature 2]`

        - [ ] T5B.3 Implement `[activity: frontend-implementation]`
            - [ ] T5B.3.1 Update `completed-buys-grid.html` or `completed-buys.html` — add delivery status column to grid template
            - [ ] T5B.3.2 Update `CompletedManager.js` or `completed-buys-grid.js` — add delivery status column to DataTables definition with icon render function
            - [ ] T5B.3.3 Implement status icon render: `sent` → gray check, `delivered` → green double-check, `failed` → red X with `text-danger`
            - [ ] T5B.3.4 Update Completed Buys backend query — JOIN `chat_messages` on `provider_message_id` (via `billingSmsUsage` or `ChatBridgeService` reference) to include `delivery_status` field in the completed buys API response. Source: `chat_messages.delivery_status` in the per-store DB, looked up by the buy's associated chat message `[ref: SDD/ADR-2 Buy Delivery via ChatBridgeService]` `[activity: backend-implementation]`

        - [ ] T5B.4 Validate
            - [ ] T5B.4.1 Visual inspection: delivery status column visible in grid `[activity: review-code]`
            - [ ] T5B.4.2 Verify PRD Feature 2 acceptance criteria `[activity: business-acceptance]`

    - [ ] T5C WorkbookToast Service `[component: frontend-toast]` `[depends: T5A]`

        - [ ] T5C.1 Prime Context
            - [ ] T5C.1.1 Read SDD WorkbookToast example code `[ref: SDD/Example: Frontend WorkbookToast Service; lines: 1154-1228]`
            - [ ] T5C.1.2 Read workspace.js — initialization point `[ref: public_html/js/workspace/workspace.js]`
            - [ ] T5C.1.3 Read existing Ably sync for global subscription pattern `[ref: public_html/js/workspace/modules/chat/chat-ably-sync.js]`

        - [ ] T5C.2 Write Tests (Manual test scenarios)
            - [ ] T5C.2.1 Define: Toast appears on ANY Workbook page when delivery fails `[ref: PRD/Feature 3 - toast on all SPA pages]`
            - [ ] T5C.2.2 Define: Toast includes customer name and store `[ref: PRD/Feature 3]`
            - [ ] T5C.2.3 Define: Toast auto-dismisses after 8 seconds `[ref: SDD/WorkbookToast]`

        - [ ] T5C.3 Implement `[activity: frontend-implementation]`
            - [ ] T5C.3.1 Create `public_html/js/workspace/modules/shared/WorkbookToast.js` — global toast manager `[ref: SDD/WorkbookToast entity]`
            - [ ] T5C.3.2 Implement `init(ablyChannel)` — subscribe to `workbook:chat:delivered`, filter for `status === 'failed'`
            - [ ] T5C.3.3 Implement `showDeliveryFailure(data)` — format toast with customerName, error detail
            - [ ] T5C.3.4 Implement `showToast(message, type, duration, detail)` — generic toast using Bootstrap 5 Toast component
            - [ ] T5C.3.5 Update `workspace.js` — initialize WorkbookToast with Ably channel on workspace load
            - [ ] T5C.3.6 Add toast container CSS (positioned top-right, z-index 1090) `[ref: SDD/WorkbookToast example]`

        - [ ] T5C.4 Validate
            - [ ] T5C.4.1 Visual inspection: toast appears when Ably failure event fires `[activity: review-code]`
            - [ ] T5C.4.2 Toast auto-dismisses `[activity: review-code]`
            - [ ] T5C.4.3 Verify PRD Feature 3 toast requirements `[activity: business-acceptance]`

    **Definition of Done (T5)**: Chat messages show 3-state delivery icons. Real-time Ably updates work. Failed messages show "Message Failed" inline + error tooltip. Completed Buys grid has delivery status column. WorkbookToast shows failure toasts on any page. CSS built.

---

- [x] **T6 Phase 6: Billing Report — SMS Cost Report UI + API** `[depends: T1]` ✅ COMPLETED 2026-02-11

    *Delivers: SMS Cost Report page in Billing module with Syncfusion Grid + Chart, API endpoints for cost/profit data.*

    - [ ] T6.1 Prime Context
        - [ ] T6.1.1 Read SDD Internal API Changes — all 4 endpoints `[ref: SDD/Internal API Changes; lines: 613-733]`
        - [ ] T6.1.2 Read SDD SmsCostReportService entity `[ref: SDD/Application Data Models; lines: 832-841]`
        - [ ] T6.1.3 Read existing `BillingApiController.php` and `BillingPageController.php` `[ref: userfrosting/src/BuyerKiosk/Billing/Controllers/BillingApiController.php]`
        - [ ] T6.1.4 Read existing `billing/dashboard.html` for Syncfusion patterns `[ref: userfrosting/templates/themes/default/billing/dashboard.html]`
        - [ ] T6.1.5 Read existing billing route files `[ref: userfrosting/routes/billing/api.php; userfrosting/routes/billing/pages.php]`
        - [ ] T6.1.6 Read SDD cost/profit calculation algorithm `[ref: SDD/Complex Logic: Cost/Profit Calculation; lines: 1469-1498]`
        - [ ] T6.1.7 Read SDD SMS Cost Report UI rendering notes `[ref: SDD/SMS Cost Report UI Rendering Notes; lines: 1937-1943]`

    - [ ] T6.2 Write Tests
        - [ ] T6.2.1 Test: `SmsCostReportService::getSmsCostSummary()` — aggregates cost, revenue, profit by store `[ref: SDD/SmsCostReportService; PRD/Feature 6]` `[activity: write-unit-tests]`
        - [ ] T6.2.2 Test: `SmsCostReportService::getSmsCostSummary()` — aggregates by category `[activity: write-unit-tests]`
        - [ ] T6.2.3 Test: `SmsCostReportService::getStoreCostDetail()` — per-store breakdown with categories `[ref: PRD/Feature 6]` `[activity: write-unit-tests]`
        - [ ] T6.2.4 Test: Base Plan categories show $0 revenue `[ref: PRD/Feature 6; SDD/UI Rendering Notes]` `[activity: write-unit-tests]`
        - [ ] T6.2.5 Test: Demo stores excluded from all calculations `[ref: PRD/Feature 9; SDD/SmsCostReportService]` `[activity: write-unit-tests]`
        - [ ] T6.2.6 Test: `SmsCostReportService::getDeliveryRates()` — per-store delivery rate with flagged stores < 90% `[ref: PRD/Feature 8; SDD/SmsCostReportService]` `[activity: write-unit-tests]`
        - [ ] T6.2.7 Test: `SmsCostReportService::getCostTrend()` — returns 12-month trend data `[activity: write-unit-tests]`
        - [ ] T6.2.8 Test: API endpoint `/api/billing/sms-costs` requires `uri_admin_billing` permission `[ref: SDD/API spec]` `[activity: write-unit-tests]`
        - [ ] T6.2.9 Test: API endpoint `/api/billing/{typeNum}/sms-costs` allows store owner access `[ref: SDD/API spec]` `[activity: write-unit-tests]`
        - [ ] T6.2.10 Test: `billing.sms_report.viewed` tracking event emitted `[ref: SDD/Tracking Events Map]` `[activity: write-unit-tests]`
        - [ ] T6.2.11 Test: Profit margin calculation: (revenue - cost) / revenue * 100, handle zero revenue `[activity: write-unit-tests]`
        - [ ] T6.2.12 Test: Webhook processing health query — returns success/failure counts for a given time period from `smsWebhookLog` `[ref: PRD/Feature 1a; SDD/smsWebhookLog table]` `[activity: write-unit-tests]`

    - [ ] T6.3 Implement Backend `[activity: backend-implementation]`
        - [ ] T6.3.1 Create `userfrosting/src/BuyerKiosk/Billing/Services/SmsCostReportService.php` — cost/profit aggregation queries `[ref: SDD/SmsCostReportService entity]`
        - [ ] T6.3.2 Implement `getSmsCostSummary()` — join billingSmsUsage with billingSmsCategoryConfig, exclude dev stores
        - [ ] T6.3.3 Implement `getStoreCostDetail()` — per-store with category breakdown, Base Plan labeling
        - [ ] T6.3.4 Implement `getDeliveryRates()` — per-store delivery rate KPI with flagging
        - [ ] T6.3.5 Implement `getCostTrend()` — monthly aggregation for chart data
        - [ ] T6.3.5b Implement `getWebhookHealth()` method on `SmsCostReportService` — query `smsWebhookLog` for success/failure/unmatched counts per time period, fulfilling PRD Feature 1a "queryable" requirement `[ref: PRD/Feature 1a]`
        - [ ] T6.3.6 Update `BillingApiController.php` — add `getSmsCosts()`, `getStoreSmsCosts()`, `getSmsDeliveryRates()`, `getSmsCostTrend()` methods `[ref: SDD/BillingApiController MODIFY]`
        - [ ] T6.3.7 Update `BillingPageController.php` — add `smsCosts()` page method `[ref: SDD/BillingPageController MODIFY]`
        - [ ] T6.3.8 Update `userfrosting/routes/billing/api.php` — add SMS cost API routes `[ref: SDD/billing routes MODIFY]`
        - [ ] T6.3.9 Update `userfrosting/routes/billing/pages.php` — add SMS cost page route

    - [ ] T6.4 Implement Frontend `[activity: frontend-implementation]`
        - [ ] T6.4.1 Create `userfrosting/templates/themes/default/billing/sms-costs.html` — SMS cost report page `[ref: SDD/Directory Map]`
        - [ ] T6.4.2 Implement Syncfusion EJ2 Grid for cost breakdown table — columns: store/category, messages, segments, cost, revenue, profit, margin% `[ref: SDD/ADR-10]`
        - [ ] T6.4.3 Implement Syncfusion EJ2 Chart — stacked bar (cost/revenue) + line overlay (margin %) `[ref: SDD/ADR-10; PRD/Feature 6]`
        - [ ] T6.4.4 Implement store filter dropdown and period selector
        - [ ] T6.4.5 Implement "Base Plan" badge (`badge bg-secondary`) for non-billable categories `[ref: SDD/UI Rendering Notes]`
        - [ ] T6.4.6 Implement delivery rate KPI section with flagging (< 90% = `text-danger` + warning icon) `[ref: PRD/Feature 8; SDD/UI Rendering Notes]`
        - [ ] T6.4.7 Update `billing/dashboard.html` — add "SMS Costs" nav link `[ref: SDD/billing dashboard MODIFY]`
        - [ ] T6.4.8 Update `css/admin/modules/billing.css` — add SMS cost report styles
        - [ ] T6.4.9 Build CSS: `php userfrosting/conductor build-css --minify` `[activity: build-css]`

    - [ ] T6.5 Validate
        - [ ] T6.5.1 Run SmsCostReportService unit tests `[activity: run-tests]`
        - [ ] T6.5.2 Run BillingApiController tests `[activity: run-tests]`
        - [ ] T6.5.3 Run PHPStan on all modified/new billing files `[activity: lint-code]`
        - [ ] T6.5.4 Visual inspection: report renders with grid + chart `[activity: review-code]`
        - [ ] T6.5.5 Verify PRD Feature 6 + Feature 8 acceptance criteria `[activity: business-acceptance]`

    **Definition of Done (T6)**: SMS Cost Report page renders in Billing module. Syncfusion Grid shows cost/revenue/profit per store and category. Chart displays stacked bar + margin line. Delivery rate KPI flags stores < 90%. Webhook health queryable. All billing tests pass. CSS built. PHPStan clean.

---

- [x] **T7 Phase 7: Integration & End-to-End Validation** `[depends: T1, T2, T3, T4, T5, T6]` ✅ COMPLETED 2026-02-11

    *Delivers: Comprehensive validation that all components work together end-to-end.*

    - [x] T7.1 Cross-Component Unit Tests
        - [x] T7.1.1 Run full unit test suite: `./test.sh --testsuite unit` `[activity: run-tests]`
        - [x] T7.1.2 Verify no regressions in existing SMS tests (294 tests passing) `[activity: run-tests]`
        - [x] T7.1.3 Verify no regressions in existing billing tests (229 tests passing) `[activity: run-tests]`
        - [x] T7.1.4 Verify no regressions in existing chat tests (passing) `[activity: run-tests]`

    - [ ] T7.2 Integration Tests
        - [ ] T7.2.1 Integration test: Full Twilio delivery flow — send message → webhook → status update → cost lookup → both DBs updated `[ref: SDD/Scenario 1]` `[activity: write-integration-tests]`
        - [ ] T7.2.2 Integration test: Full Vonage delivery flow — send message → DLR webhook → status + cost update → both DBs updated `[ref: SDD/Scenario 2]` `[activity: write-integration-tests]`
        - [ ] T7.2.3 Integration test: Demo store flow — send → DemoDeliverySimulationJob → status set to delivered, cost $0 `[ref: SDD/Scenario 7]` `[activity: write-integration-tests]`
        - [ ] T7.2.4 Integration test: Inbound cost tracking — inbound webhook → usage logged with 'inbound' → cost captured `[ref: PRD/Feature 5]` `[activity: write-integration-tests]`
        - [ ] T7.2.5 Integration test: SMS Cost Report API — returns correct cost/revenue/profit for seeded data `[ref: SDD/Scenario 8]` `[activity: write-integration-tests]`

    - [ ] T7.3 End-to-End Flows
        - [ ] T7.3.1 E2E: Primary user journey — staff sends message → delivery status updates on Chat page in real-time `[ref: PRD/Primary User Journey]` `[activity: write-e2e-tests]`
        - [ ] T7.3.2 E2E: Failure user journey — message fails → toast appears on Workbook → failed icon in Chat `[ref: PRD/Primary User Journey step 3-4]` `[activity: write-e2e-tests]`
        - [ ] T7.3.3 E2E: Admin billing journey — navigate to SMS Costs → filter by store → see cost/revenue/profit breakdown `[ref: PRD/Secondary User Journey]` `[activity: write-e2e-tests]`
        - [ ] T7.3.4 E2E: Completed Buys delivery column — complete buy → delivery status visible in grid `[ref: PRD/Feature 2]` `[activity: write-e2e-tests]`

    - [ ] T7.4 Quality Gates
        - [ ] T7.4.1 Performance: Webhook endpoint responds < 200ms p95 `[ref: SDD/Quality Requirements]` `[activity: review-code]`
        - [ ] T7.4.2 Performance: Report API < 3s all-stores, < 1s single-store `[ref: SDD/Quality Requirements]` `[activity: review-code]`
        - [ ] T7.4.3 Security: Webhook token validation works (invalid token = logged, not processed) `[ref: SDD/Cross-Cutting Concepts/Security]` `[activity: review-code]`
        - [ ] T7.4.4 Security: Admin-only endpoints require `uri_admin_billing` permission `[activity: review-code]`
        - [ ] T7.4.5 Run PHPStan on entire modified codebase `[activity: lint-code]`
        - [ ] T7.4.6 Run `./test.sh` — all tests pass `[activity: run-tests]`

    - [ ] T7.5 Success Metrics Verification
        - [ ] T7.5.1 Verify: 100% of outbound messages have tracked delivery status `[ref: PRD/KPI - Delivery Visibility Coverage]` `[activity: business-acceptance]`
        - [ ] T7.5.2 Verify: Cost capture mechanism works for both Twilio (async) and Vonage (DLR) `[ref: PRD/KPI - Cost Capture Rate]` `[activity: business-acceptance]`
        - [ ] T7.5.3 Verify: All 11 PRD tracking events are emitted at correct points `[ref: SDD/Tracking Events Map]` `[activity: business-acceptance]`
        - [ ] T7.5.4 Verify: Demo stores excluded from profit calculations `[ref: PRD/Feature 9]` `[activity: business-acceptance]`
        - [ ] T7.5.5 Verify: Stores below 90% delivery rate are flagged `[ref: PRD/Feature 8]` `[activity: business-acceptance]`

    - [ ] T7.6 Final Acceptance
        - [ ] T7.6.1 All PRD Must Have acceptance criteria verified (Features 1-7) `[ref: PRD/Must Have Features]`
        - [ ] T7.6.2 All PRD Should Have acceptance criteria verified (Features 8-9) `[ref: PRD/Should Have Features]`
        - [ ] T7.6.3 All 13 SDD ADRs implemented as designed `[ref: SDD/Architecture Decisions]`
        - [ ] T7.6.4 Implementation follows SDD component structure `[ref: SDD/Building Block View]`
        - [ ] T7.6.5 Build verification: `php userfrosting/conductor build-css --minify` succeeds `[activity: build-css]`
        - [ ] T7.6.6 Deployment verification: all migrations run cleanly `[activity: run-migrations]`
        - [ ] T7.6.7 ENV variables documented (TWILIO_WEBHOOK_SECRET, VONAGE_WEBHOOK_SECRET) `[ref: SDD/Deployment View]`

---

## Task Summary

| Phase | Focus | Tasks | Dependencies | Parallel |
|-------|-------|-------|-------------|----------|
| T1 | Database + Value Objects | 30 | None | No |
| T2 | Delivery Processing Core | 19 | T1 | No |
| T3A | Webhook Handlers | 18 | T2 | Yes (with T3B) |
| T3B | Sender Modifications + ChatApiController | 17 | T1 | Yes (with T3A) |
| T4 | TaskEngine Jobs | 23 | T1 | No |
| T4.5b | Integration Checkpoint | 5 | T2, T3A, T3B, T4 | No |
| T3C | Inbound Cost Tracking | 12 | T2, T4 | No |
| T5A | Chat UI | 14 | T2 | Yes (with T5B) |
| T5B | Completed Buys UI | 10 | T1 | Yes (with T5A) |
| T5C | WorkbookToast | 10 | T5A | No |
| T6 | Billing Report + Webhook Health | 27 | T1 | No |
| T7 | Integration & E2E | 26 | All | No |
| **Total** | | **~211** | | |

## PRD Feature → Phase Traceability

| PRD Feature | Phase(s) | Status |
|-------------|----------|--------|
| Feature 1: Delivery Status Webhooks | T1, T2, T3A | Must Have |
| Feature 1a: Webhook Monitoring | T3A | Must Have |
| Feature 2: Delivery on Completed Buys | T5B | Must Have |
| Feature 3: Delivery in Chat UI | T5A, T5C | Must Have |
| Feature 4: Provider Cost (Outbound) | T1, T4.2 | Must Have |
| Feature 5: Provider Cost (Inbound) | T3C | Must Have |
| Feature 6: SMS Cost Report | T6 | Must Have |
| Feature 7: Sender Configuration | T3B | Must Have |
| Feature 8: Delivery Rate KPI | T6 | Should Have |
| Feature 9: Demo Store Simulation | T3B, T4.3 | Should Have |

## SDD Component → Phase Traceability

| SDD Component | Phase |
|--------------|-------|
| DeliveryStatusMapper | T1 |
| MappedStatus | T1 |
| ProcessingResult | T1 |
| DeliveryStatusProcessor | T2 |
| TwilioDeliveryHandler | T3A |
| VonageDeliveryHandler | T3A |
| SmsCostLookupJob | T4.2 |
| DemoDeliverySimulationJob | T4.3 |
| SmsStaleCostSweepJob | T4.4 |
| SmsCostReportService | T6 |
| WorkbookToast | T5C |
| Webhook routes | T3A |
| billingSmsUsage migrations | T1 |
| smsWebhookLog migration | T1 |
| Job definition migrations | T1 |
| Chat UI updates | T5A |
| Completed Buys UI | T5B |
| Billing Report UI | T6 |
