# Solution Design Document

## Validation Checklist

- [x] All required sections are complete
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] All context sources are listed with relevance ratings
- [x] Project commands are discovered from actual project files
- [x] Constraints → Strategy → Design → Implementation path is logical
- [x] Architecture pattern is clearly stated with rationale
- [x] Every component in diagram has directory mapping
- [x] Every interface has specification
- [x] Error handling covers all error types
- [x] Quality requirements are specific and measurable
- [x] Every quality requirement has test coverage
- [x] **All architecture decisions confirmed by user**
- [x] Component names consistent across diagrams
- [x] A developer could implement from this design

---

## Constraints

**CON-1: Technology Stack**
- PHP 8.x with Slim 2.6.2 framework
- MySQL with multi-store database architecture (central + per-store DBs)
- Twig 1.44.8 templating with Handlebars for client-side templates
- Vanilla JavaScript (ES6+) with jQuery for legacy compatibility
- Ably for real-time messaging (existing integration)

**CON-2: SMS Provider Requirements**
- Must support both Vonage and Twilio providers (store-configurable)
- Messages limited to 320 characters (2 SMS segments) for cost control
- Must handle delivery webhooks within 60-second timeout
- Flood protection: max 5 messages/phone/day (existing limit)

**CON-3: Compliance Requirements**
- TCPA 2025: Process opt-outs within 5 minutes (exceeds 10-day legal requirement)
- Support granular opt-out: STOP = all messages, STOP MARKETING = marketing only
- Single confirmation message on opt-out (no follow-ups)
- Maintain audit trail of all messages for compliance

**CON-4: Multi-Store Architecture**
- Inbound SMS arrives at shared shortcode; must route to correct store
- Store identification via latest buy timestamp lookup
- Per-store databases for chat data; central DB for routing

## Implementation Context

### Required Context Sources

- ICO-1: General Application Context
```yaml
# Internal documentation and patterns
- doc: docs/specs/two-way-sms-chat/analysis.md
  relevance: HIGH
  why: "Complete infrastructure analysis with existing code patterns"

- doc: docs/features/seller-marketing-overview.md
  relevance: MEDIUM
  why: "Existing SMS queue and template patterns to follow"

- doc: docs/systems/redis-worker-system.md
  relevance: MEDIUM
  why: "Queue processing patterns for SMS delivery"

# External documentation
- url: https://developer.vonage.com/messaging/sms/guides/inbound-sms
  relevance: HIGH
  why: "Vonage inbound webhook format and response requirements"

- url: https://www.twilio.com/docs/messaging/guides/webhook-request
  relevance: HIGH
  why: "Twilio webhook request format"
```

- ICO-2: Workbook System
```yaml
- file: public_html/js/workspace/modules/workbook/layout-manager.js
  relevance: HIGH
  sections: [PANEL_REGISTRY, getDefaultConfig]
  why: "Panel registration pattern for Chat panel"

- file: public_html/js/workspace/modules/workbook/ably-sync.js
  relevance: HIGH
  sections: [handleMessage, publish methods]
  why: "Real-time event pattern for chat messages"

- file: public_html/js/workspace/modules/workbook/task-manager.js
  relevance: MEDIUM
  why: "Reference component implementation pattern"

- file: userfrosting/templates/themes/default/workspace/workspace.html
  relevance: HIGH
  why: "Panel HTML structure pattern"
```

- ICO-3: SMS Infrastructure
```yaml
- file: userfrosting/src/BuyerKiosk/SMS/TextMessageService/TextMessageService.php
  relevance: HIGH
  why: "Message sending interface to integrate with"

- file: userfrosting/routes/groups/sms.php
  relevance: HIGH
  why: "Existing inbound SMS handling pattern"

- file: public_html/api/webhooks/vonage-delivery-simple.php
  relevance: MEDIUM
  why: "Delivery webhook handling pattern"

- file: userfrosting/src/BuyerKiosk/SellerMarketing/SmsQueue.php
  relevance: MEDIUM
  why: "Queue management pattern for chat messages"
```

### Implementation Boundaries

- **Must Preserve**:
  - Existing `TextMessageService` interface for sending
  - Existing `loyaltyDoNotTextList` opt-out table
  - Existing Ably channel naming (typeNum as channel)
  - Existing flood protection limits

- **Can Modify**:
  - Add new tables to store databases
  - Add new webhook endpoint for chat inbound
  - Extend Workbook panel system with Chat panel
  - Add new admin routes for template management

- **Must Not Touch**:
  - Existing `/api/sms/inbound` route (legacy loyalty system)
  - Existing `seller_marketing_queue` table structure
  - Existing Vonage/Twilio delivery webhooks

### External Interfaces

#### System Context Diagram

```mermaid
graph TB
    Staff[Store Staff] --> Workbook[Workbook Chat Panel]
    Owner[Store Owner] --> Admin[Admin Template UI]

    Workbook --> ChatAPI[Chat API]
    Admin --> ChatAPI

    ChatAPI --> StoreDB[(Store Database)]
    ChatAPI --> CentralDB[(Central Database)]
    ChatAPI --> Ably[Ably Real-time]
    ChatAPI --> SMS[SMS Service]

    SMS --> Vonage[Vonage API]
    SMS --> Twilio[Twilio API]

    Vonage --> InboundWebhook[Chat Inbound Webhook]
    Twilio --> InboundWebhook

    InboundWebhook --> ChatAPI

    Customer[Customer Phone] --> Vonage
    Customer --> Twilio

    Vonage --> DeliveryWebhook[Delivery Webhook]
    Twilio --> DeliveryWebhook
    DeliveryWebhook --> ChatAPI
```

#### Interface Specifications

```yaml
# Inbound Interfaces
inbound:
  - name: "Workbook Chat Panel"
    type: HTTPS
    format: REST JSON
    authentication: Session cookie + CSRF token
    data_flow: "Staff sends/receives chat messages"

  - name: "Admin Template Management"
    type: HTTPS
    format: REST JSON
    authentication: Session cookie + CSRF token
    data_flow: "Owner manages canned templates"

  - name: "SMS Inbound Webhook"
    type: HTTPS
    format: Form-encoded (Vonage) / Form-encoded (Twilio)
    authentication: None (provider IP validation recommended)
    data_flow: "Customer replies routed to store"

  - name: "SMS Delivery Webhook"
    type: HTTPS
    format: Form-encoded
    authentication: None
    data_flow: "Delivery status updates"

# Outbound Interfaces
outbound:
  - name: "Vonage SMS API"
    type: HTTPS
    format: REST JSON
    authentication: API Key + Secret
    doc: https://developer.vonage.com/messaging/sms/overview
    data_flow: "Send SMS to customers"
    criticality: HIGH

  - name: "Twilio SMS API"
    type: HTTPS
    format: REST
    authentication: Account SID + Auth Token
    doc: https://www.twilio.com/docs/sms/api
    data_flow: "Send SMS to customers"
    criticality: HIGH

  - name: "Ably Real-time"
    type: WebSocket
    format: JSON
    authentication: API Key
    data_flow: "Push notifications to Workbook"
    criticality: MEDIUM

# Data Interfaces
data:
  - name: "Store Database"
    type: MySQL
    connection: PDO Connection Pool
    data_flow: "Chat threads, messages, templates per store"

  - name: "Central Database (kiosk_buykiosk)"
    type: MySQL
    connection: PDO Connection Pool
    data_flow: "Store lookup, opt-out list, usage tracking"
```

### Project Commands

```bash
# Component: PHP Backend
Location: userfrosting/

## Environment Setup
Install Dependencies: cd userfrosting && composer install
Environment Variables: .env (VONAGE_KEY, VONAGE_SECRET, TWILIO_SID, TWILIO_TOKEN)
Start Development: Apache/Nginx with PHP-FPM

## Testing Commands
Unit Tests: ./test.sh --testsuite unit
Integration Tests: ./test.sh --testsuite integration
Test Coverage: ./test.sh --coverage
Specific Test: ./test.sh --filter ChatTest

## Code Quality Commands
PHP Linting: composer run lint (if configured)

## Database Operations
Database Migration: Run migration JSON files via migration system
Migration Input: userfrosting/migrations/input/

# Component: JavaScript Frontend
Location: public_html/js/workspace/

## Development
No build step required - vanilla JS served directly
Watch Mode: Browser refresh

## Testing
Manual testing via browser
Console debugging via Chrome DevTools
```

## Solution Strategy

- **Architecture Pattern**: Layered MVC with Service Layer
  - Controllers handle HTTP request/response
  - Services encapsulate business logic (matching, eligibility, billing)
  - Models represent database entities
  - Follows existing `BuyerKiosk` namespace conventions

- **Integration Approach**:
  - Chat panel integrates as new Workbook panel via existing layout-manager system
  - New webhook endpoint for inbound SMS (separate from legacy `/api/sms/inbound`)
  - Extends existing `TextMessageService` for sending
  - Uses existing Ably infrastructure for real-time

- **Justification**:
  - Matches existing codebase patterns for maintainability
  - Reuses proven infrastructure (Ably, SMS services)
  - Minimizes risk by following established conventions

- **Key Decisions**:
  - Store chat data in per-store databases (matches multi-tenant pattern)
  - Track usage in central database (enables cross-store billing)
  - New webhook endpoint avoids breaking legacy loyalty SMS handling

## Building Block View

### Components

```mermaid
graph LR
    subgraph Frontend
        ChatPanel[Chat Panel JS]
        ChatAbly[Chat Ably Sync]
        TemplateAdmin[Template Admin UI]
    end

    subgraph Backend
        ChatAPI[Chat API Controller]
        ChatWebhook[Chat Webhook Handler]
        ChatAdminAPI[Chat Admin API]
    end

    subgraph Services
        MatchingService[Matching Service]
        EligibilityService[Eligibility Service]
        BillingService[Billing Service]
        TemplateService[Template Service]
    end

    subgraph Models
        ChatThread[ChatThread]
        ChatMessage[ChatMessage]
        ChatTemplate[ChatTemplate]
        ChatUsage[ChatUsage]
    end

    subgraph External
        SMS[TextMessageService]
        Ably[WorkbookAbly]
        Store[Store Model]
        Customer[Customer Model]
    end

    ChatPanel --> ChatAPI
    ChatPanel --> ChatAbly
    TemplateAdmin --> ChatAdminAPI

    ChatWebhook --> MatchingService
    ChatWebhook --> ChatThread
    ChatWebhook --> Ably

    ChatAPI --> EligibilityService
    ChatAPI --> TemplateService
    ChatAPI --> ChatMessage
    ChatAPI --> SMS

    ChatAdminAPI --> ChatTemplate
    ChatAdminAPI --> BillingService

    MatchingService --> Store
    MatchingService --> Customer
    EligibilityService --> ChatThread
    BillingService --> ChatUsage
```

### Directory Map

**Component**: Backend (PHP)
```
userfrosting/src/BuyerKiosk/Chat/
├── Controllers/
│   ├── ChatApiController.php          # NEW: Staff chat API endpoints
│   ├── ChatWebhookController.php      # NEW: Inbound SMS webhook handler
│   └── ChatAdminController.php        # NEW: Admin template management
├── Models/
│   ├── ChatThread.php                 # NEW: Thread entity with eligibility
│   ├── ChatMessage.php                # NEW: Message entity with delivery
│   └── ChatTemplate.php               # NEW: Canned template entity
├── Services/
│   ├── ChatMatchingService.php        # NEW: Phone → Store routing
│   ├── ChatEligibilityService.php     # NEW: Can staff message customer?
│   ├── ChatBillingService.php         # NEW: Usage tracking
│   └── ChatTemplateService.php        # NEW: Template wildcard processing
└── Events/
    └── ChatAblyPublisher.php          # NEW: Real-time notifications

userfrosting/routes/chat/
├── api.php                            # NEW: Staff API routes
├── webhooks.php                       # NEW: Inbound webhook route
└── admin.php                          # NEW: Admin routes
```

**Component**: Frontend (JavaScript)
```
public_html/js/workspace/modules/chat/
├── chat-overlay.js                    # NEW: Floating overlay panel (minimize to KPI footer)
├── chat-thread-list.js                # NEW: Thread list with Active/Closed tabs
├── chat-conversation.js               # NEW: Message display and history
├── chat-composer.js                   # NEW: Message input with freetext lock state
├── chat-templates.js                  # NEW: Template dropdown picker
├── chat-ably-sync.js                  # NEW: Real-time message handler
├── chat-notifications.js              # NEW: Toast notifications and badge management
└── chat-buyqueue-badge.js             # NEW: Buy queue row badge integration

public_html/js/admin/chat/
├── template-manager.js                # NEW: Admin template CRUD
└── usage-dashboard.js                 # NEW: Usage reporting
```

**Component**: Templates (Twig)
```
userfrosting/templates/themes/default/
├── workspace/partials/
│   └── chat-panel-content.html        # NEW: Chat panel template
└── admin/chat/
    ├── templates.html                 # NEW: Template management page
    └── usage.html                     # NEW: Usage dashboard page
```

**Component**: Database Migrations
```
userfrosting/migrations/input/
├── 20251206_001_chat_threads.json     # NEW: chat_threads table
├── 20251206_002_chat_messages.json    # NEW: chat_messages table
├── 20251206_003_chat_templates.json   # NEW: chat_templates table
├── 20251206_004_chat_usage.json       # NEW: chat_sms_usage table (central)
└── 20251206_005_chat_optout.json      # NEW: Update loyaltyDoNotTextList
```

### Interface Specifications

#### Data Storage Changes

```yaml
# Per-Store Database Tables

Table: chat_threads (NEW)
  id: BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
  typeNum: VARCHAR(20) NOT NULL
  customer_id: INT UNSIGNED NOT NULL
  customer_phone: VARCHAR(15) NOT NULL
  buy_id: INT UNSIGNED NULL
  status: ENUM('pending','active','closed','archived') DEFAULT 'pending'
  staff_can_freetext: BOOLEAN DEFAULT FALSE
  last_message_at: TIMESTAMP NULL
  last_customer_message_at: TIMESTAMP NULL
  last_staff_message_at: TIMESTAMP NULL
  opened_by_employee_id: INT UNSIGNED NULL
  closed_by_employee_id: INT UNSIGNED NULL
  closed_at: TIMESTAMP NULL
  created_at: TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  updated_at: TIMESTAMP ON UPDATE CURRENT_TIMESTAMP

  INDEXES:
    - UNIQUE KEY uk_customer_buy (typeNum, customer_id, buy_id)
    - INDEX idx_status (typeNum, status, last_message_at DESC)
    - INDEX idx_phone (customer_phone)

Table: chat_messages (NEW)
  id: BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
  thread_id: BIGINT UNSIGNED NOT NULL
  typeNum: VARCHAR(20) NOT NULL
  direction: ENUM('inbound','outbound') NOT NULL
  sender_type: ENUM('customer','staff','system') NOT NULL
  sender_id: INT UNSIGNED NULL
  template_id: INT UNSIGNED NULL
  content: TEXT NOT NULL
  content_raw: TEXT NULL
  character_count: SMALLINT UNSIGNED
  sms_segment_count: TINYINT UNSIGNED DEFAULT 1
  category: ENUM('transactional','interactive') NOT NULL
  provider: ENUM('vonage','twilio') NULL
  provider_message_id: VARCHAR(255) NULL
  delivery_status: ENUM('pending','queued','sent','delivered','failed','undelivered') DEFAULT 'pending'
  delivery_timestamp: TIMESTAMP NULL
  delivery_error: TEXT NULL
  read_at: TIMESTAMP NULL
  created_at: TIMESTAMP DEFAULT CURRENT_TIMESTAMP

  INDEXES:
    - INDEX idx_thread (thread_id, created_at)
    - INDEX idx_provider_id (provider_message_id)
    - INDEX idx_category (typeNum, category, created_at)

  FOREIGN KEY (thread_id) REFERENCES chat_threads(id)

Table: chat_templates (NEW)
  id: INT UNSIGNED AUTO_INCREMENT PRIMARY KEY
  typeNum: VARCHAR(20) NOT NULL
  short_name: VARCHAR(50) NOT NULL
  content: TEXT NOT NULL
  category: ENUM('transactional','initial_contact','follow_up') NOT NULL
  character_count: SMALLINT UNSIGNED
  sms_segment_count: TINYINT UNSIGNED
  is_active: BOOLEAN DEFAULT TRUE
  is_system: BOOLEAN DEFAULT FALSE
  sort_order: SMALLINT UNSIGNED DEFAULT 0
  created_by_employee_id: INT UNSIGNED NULL
  created_at: TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  updated_at: TIMESTAMP ON UPDATE CURRENT_TIMESTAMP

  INDEXES:
    - UNIQUE KEY uk_shortname (typeNum, short_name)
    - INDEX idx_category (typeNum, category, is_active)

# Central Database (kiosk_buykiosk)

Table: chat_sms_usage (NEW)
  id: BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
  typeNum: VARCHAR(20) NOT NULL
  thread_id: BIGINT UNSIGNED NULL
  message_id: BIGINT UNSIGNED NULL
  direction: ENUM('inbound','outbound') NOT NULL
  category: ENUM('transactional','interactive') NOT NULL
  provider: ENUM('vonage','twilio') NOT NULL
  provider_message_id: VARCHAR(255) NULL
  segment_count: TINYINT UNSIGNED DEFAULT 1
  cost_per_segment: DECIMAL(10,4) DEFAULT 0.0000
  total_cost: DECIMAL(10,4) DEFAULT 0.0000
  billable: BOOLEAN DEFAULT FALSE
  sent_at: TIMESTAMP NOT NULL
  billing_period: VARCHAR(7) NOT NULL
  created_at: TIMESTAMP DEFAULT CURRENT_TIMESTAMP

  INDEXES:
    - INDEX idx_typenum_period (typeNum, billing_period)
    - INDEX idx_billable (typeNum, billable, billing_period)

Table: loyaltyDoNotTextList (MODIFY)
  ADD COLUMN: optout_type ENUM('all','marketing') DEFAULT 'all'
  ADD COLUMN: optout_source ENUM('sms','admin','api') DEFAULT 'sms'
  ADD COLUMN: updated_at TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
```

#### Internal API Changes

```yaml
# Staff Chat API - /api/:typeNum/chat/

Endpoint: Get Active Threads
  Method: GET
  Path: /api/:typeNum/chat/threads
  Request:
    status: string (optional, default: 'active,pending')
    limit: int (optional, default: 50)
  Response:
    success: boolean
    threads: array
      - id, customerId, customerName, customerPhone
      - buyId, buyDailyNum
      - status, staffCanFreetext
      - lastMessagePreview, lastMessageAt
      - unreadCount
    hasMore: boolean

Endpoint: Get Thread Messages
  Method: GET
  Path: /api/:typeNum/chat/threads/:threadId
  Response:
    success: boolean
    thread: object (thread details)
    messages: array
      - id, direction, senderType, senderId, senderName
      - content, characterCount
      - deliveryStatus, deliveryTimestamp
      - createdAt
    customer: object (customer details)
    buy: object (buy context if active)

Endpoint: Send Message
  Method: POST
  Path: /api/:typeNum/chat/threads/:threadId/messages
  Request:
    content: string (required if templateId not provided)
    templateId: int (required if content not provided)
    employeeId: int (required)
  Response:
    success: boolean
    message: object (created message)
    error: string (if failed)

Endpoint: Get Eligible Customers
  Method: GET
  Path: /api/:typeNum/chat/eligible
  Response:
    success: boolean
    customers: array
      - customerId, firstName, lastName, phone
      - buyId, buyDailyNum, buyTimeEntered
      - hasActiveThread: boolean

Endpoint: Create Thread
  Method: POST
  Path: /api/:typeNum/chat/threads
  Request:
    customerId: int (required)
    buyId: int (required)
    employeeId: int (required)
  Response:
    success: boolean
    thread: object (created thread)

Endpoint: Close Thread
  Method: POST
  Path: /api/:typeNum/chat/threads/:threadId/close
  Request:
    employeeId: int (required)
  Response:
    success: boolean

Endpoint: Get Templates
  Method: GET
  Path: /api/:typeNum/chat/templates
  Request:
    category: string (optional)
  Response:
    success: boolean
    templates: array
      - id, shortName, content, category
      - characterCount, smsSegmentCount
      - isSystem

# Admin Chat API - /admin/:typeNum/chat/

Endpoint: List Templates (Admin)
  Method: GET
  Path: /admin/:typeNum/chat/templates
  Response:
    success: boolean
    templates: array (all templates with edit status)

Endpoint: Create Template
  Method: POST
  Path: /admin/:typeNum/chat/templates
  Request:
    shortName: string (required)
    content: string (required)
    category: string (required)
  Response:
    success: boolean
    template: object

Endpoint: Update Template
  Method: PUT
  Path: /admin/:typeNum/chat/templates/:templateId
  Request:
    shortName: string
    content: string
    isActive: boolean
  Response:
    success: boolean
    template: object

Endpoint: Delete Template
  Method: DELETE
  Path: /admin/:typeNum/chat/templates/:templateId
  Response:
    success: boolean

Endpoint: Get Customer Chat History
  Method: GET
  Path: /admin/:typeNum/chat/customers/:customerId/history
  Response:
    success: boolean
    threads: array (all threads with messages)

Endpoint: Get Usage Report
  Method: GET
  Path: /admin/:typeNum/chat/usage
  Request:
    startDate: date
    endDate: date
  Response:
    success: boolean
    summary:
      totalSent, totalReceived
      transactionalCount, interactiveCount
      deliveryRate, responseRate
    byDay: array

# Webhook - /api/webhooks/chat/

Endpoint: Inbound SMS
  Method: POST
  Path: /api/webhooks/chat-inbound
  Request (Vonage):
    messageId: string
    msisdn: string
    text: string
    keyword: string
    message-timestamp: string
  Request (Twilio):
    MessageSid: string
    From: string
    Body: string
    To: string
  Response:
    HTTP 200 OK
    {"status": "success"}
```

#### Application Data Models

```pseudocode
ENTITY: ChatThread (NEW)
  FIELDS:
    id: bigint (PK)
    typeNum: string
    customerId: int
    customerPhone: string
    buyId: int (nullable)
    status: enum (pending, active, closed, archived)
    staffCanFreetext: boolean
    lastMessageAt: timestamp
    lastCustomerMessageAt: timestamp
    lastStaffMessageAt: timestamp
    openedByEmployeeId: int
    closedByEmployeeId: int
    closedAt: timestamp

  BEHAVIORS:
    getMessages(): ChatMessage[]
    getCustomer(): Customer
    getBuy(): Buy
    canStaffSendFreetext(): boolean
    unlock(): void  // Sets staffCanFreetext = true
    close(employeeId): void
    reopen(): void
    getUnreadCount(): int

ENTITY: ChatMessage (NEW)
  FIELDS:
    id: bigint (PK)
    threadId: bigint (FK)
    typeNum: string
    direction: enum (inbound, outbound)
    senderType: enum (customer, staff, system)
    senderId: int (nullable, employee ID if staff)
    templateId: int (nullable)
    content: text
    contentRaw: text (before wildcard substitution)
    characterCount: int
    smsSegmentCount: int
    category: enum (transactional, interactive)
    provider: enum (vonage, twilio)
    providerMessageId: string
    deliveryStatus: enum
    deliveryTimestamp: timestamp
    deliveryError: text
    readAt: timestamp

  BEHAVIORS:
    getThread(): ChatThread
    getSender(): Employee|null
    getTemplate(): ChatTemplate|null
    markAsRead(): void
    updateDeliveryStatus(status, timestamp, error): void
    calculateSegmentCount(): int

ENTITY: ChatTemplate (NEW)
  FIELDS:
    id: int (PK)
    typeNum: string
    shortName: string
    content: text
    category: enum (transactional, initial_contact, follow_up)
    characterCount: int
    smsSegmentCount: int
    isActive: boolean
    isSystem: boolean
    sortOrder: int
    createdByEmployeeId: int

  BEHAVIORS:
    render(customer, store, buy): string  // Replaces wildcards
    getWildcards(): string[]
    calculateCharacterCount(): int
    calculateSegmentCount(): int
    validate(): ValidationResult
```

#### Integration Points

```yaml
# Inter-Component Communication

- from: ChatWebhookController
  to: ChatMatchingService
    - method: findStoreByPhone(phone)
    - returns: {store, customer, latestBuy} or null

- from: ChatWebhookController
  to: ChatAblyPublisher
    - method: publishNewMessage(typeNum, threadId, message)
    - publishes: workbook:chat:message event

- from: ChatApiController
  to: TextMessageService
    - method: sendCustomText(customer, message)
    - returns: {success, providerId}

- from: ChatApiController
  to: ChatBillingService
    - method: trackUsage(typeNum, message, category)
    - returns: void

# External System Integration

Vonage_SMS_API:
  - endpoints: [POST /sms/json]
  - integration: "Send outbound SMS via existing VonageTextSender"
  - critical_data: [to, from, text, api_key, api_secret]

Twilio_SMS_API:
  - endpoints: [POST /Messages]
  - integration: "Send outbound SMS via existing TwilioTextSender"
  - critical_data: [To, From, Body, AccountSid, AuthToken]

Ably_Realtime:
  - channels: [{typeNum} per store]
  - events: [workbook:chat:message, workbook:chat:thread:new, workbook:chat:delivered]
  - integration: "Push via existing WorkbookAbly patterns"
```

### Implementation Examples

#### Example: Phone → Store Matching Logic

**Why this example**: This is the most critical and complex business logic - correctly routing an inbound SMS to the right store.

```php
// ChatMatchingService.php
public function findStoreByPhone(string $phone): ?array
{
    // Normalize phone to 10 digits
    $normalizedPhone = preg_replace('/\D/', '', $phone);
    if (strlen($normalizedPhone) === 11 && $normalizedPhone[0] === '1') {
        $normalizedPhone = substr($normalizedPhone, 1);
    }

    if (strlen($normalizedPhone) !== 10) {
        return null; // Invalid phone format
    }

    // Check opt-out status first
    if ($this->isOptedOut($normalizedPhone, 'all')) {
        return ['optedOut' => true, 'optoutType' => 'all'];
    }

    // Get all active stores
    $stores = getAllStoresData(0, 1); // dev=0, active=1

    $bestMatch = null;
    $latestTimestamp = null;

    foreach ($stores as $store) {
        $storeDb = dbConnectByName($store->getDbName());

        // Find customer by phone
        $stmt = $storeDb->prepare(
            "SELECT c.customerID, c.firstName, c.lastName, c.phone,
                    b.buyID, b.dailyNum, b.timeEntered, b.timeCompleted,
                    COALESCE(b.timeCompleted, b.timeEntered) as latestTime
             FROM customers c
             LEFT JOIN buyQueue b ON c.customerID = b.customerID
             WHERE c.phone = :phone
             ORDER BY latestTime DESC
             LIMIT 1"
        );
        $stmt->execute([':phone' => $normalizedPhone]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);

        if ($result && $result['latestTime']) {
            $timestamp = strtotime($result['latestTime']);
            if ($latestTimestamp === null || $timestamp > $latestTimestamp) {
                $latestTimestamp = $timestamp;
                $bestMatch = [
                    'store' => $store,
                    'customer' => $result,
                    'buyId' => $result['buyID'],
                    'typeNum' => $store->getTypeNum()
                ];
            }
        }
    }

    return $bestMatch;
}
```

#### Example: Freetext Unlock Logic

**Why this example**: Critical business rule that prevents staff from sending freetext until customer has replied.

```php
// ChatThread.php
public function canStaffSendFreetext(): bool
{
    return $this->staffCanFreetext === true;
}

public function processInboundMessage(string $content, string $providerMessageId): ChatMessage
{
    // Create the inbound message
    $message = new ChatMessage();
    $message->setThreadId($this->id);
    $message->setDirection('inbound');
    $message->setSenderType('customer');
    $message->setContent($content);
    $message->setCategory('interactive'); // Inbound always interactive
    $message->setProviderMessageId($providerMessageId);
    $message->save();

    // UNLOCK FREETEXT - customer has replied!
    if (!$this->staffCanFreetext) {
        $this->staffCanFreetext = true;
        $this->status = 'active';
        $this->save();
    }

    // Update timestamps
    $this->lastMessageAt = new DateTime();
    $this->lastCustomerMessageAt = new DateTime();
    $this->save();

    return $message;
}

public function sendStaffMessage(
    int $employeeId,
    ?string $content,
    ?int $templateId
): ChatMessage {
    // Validate: must have content OR template
    if (empty($content) && empty($templateId)) {
        throw new InvalidArgumentException('Message content or template required');
    }

    // If freetext not unlocked, must use template
    if (!$this->canStaffSendFreetext() && empty($templateId)) {
        throw new BusinessRuleException('Must use template until customer replies');
    }

    // ... rest of send logic
}
```

#### Example: Template Wildcard Processing

**Why this example**: Shows the wildcard substitution pattern for canned messages.

```php
// ChatTemplateService.php
public function render(
    ChatTemplate $template,
    Customer $customer,
    Store $store,
    ?Buy $buy = null
): string {
    $content = $template->getContent();

    $replacements = [
        '{{customer_name}}' => $customer->getFirstName() ?: 'Customer',
        '{{customer_full_name}}' => trim($customer->getFirstName() . ' ' . $customer->getLastName()),
        '{{store_name}}' => $store->getCompanyName(),
        '{{store_phone}}' => $this->formatPhone($store->getPhone()),
        '{{store_address}}' => $store->getAddress(),
        '{{store_city}}' => $store->getCity(),
        '{{store_hours}}' => $this->getTodayHours($store),
    ];

    if ($buy) {
        $replacements['{{buy_number}}'] = '#' . $buy->getDailyNum();
        $replacements['{{buy_date}}'] = date('m/d', strtotime($buy->getTimeEntered()));
    }

    foreach ($replacements as $wildcard => $value) {
        $content = str_replace($wildcard, $value, $content);
    }

    return $content;
}

public function getAvailableWildcards(): array
{
    return [
        '{{customer_name}}' => 'Customer first name',
        '{{customer_full_name}}' => 'Customer full name',
        '{{store_name}}' => 'Store name',
        '{{store_phone}}' => 'Store phone number',
        '{{store_address}}' => 'Store street address',
        '{{store_city}}' => 'Store city',
        '{{store_hours}}' => "Today's hours",
        '{{buy_number}}' => 'Buy daily number (e.g., #47)',
        '{{buy_date}}' => 'Buy date',
    ];
}
```

## Runtime View

### Primary Flow: Staff Sends Message

1. Staff opens Chat panel in Workbook
2. Staff selects thread or creates new from eligible customer
3. Staff selects template (if freetext locked) or types message
4. Frontend calls POST `/api/:typeNum/chat/threads/:threadId/messages`
5. Controller validates eligibility and freetext permission
6. Service processes template wildcards if applicable
7. TextMessageService sends via Vonage/Twilio
8. Message saved with pending delivery status
9. BillingService tracks usage
10. Ably publishes `workbook:chat:message` event
11. Frontend updates thread with sent message

```mermaid
sequenceDiagram
    actor Staff
    participant ChatPanel
    participant ChatAPI
    participant ChatService
    participant SMS as TextMessageService
    participant Ably
    participant Provider as Vonage/Twilio

    Staff->>ChatPanel: Click Send
    ChatPanel->>ChatAPI: POST /threads/:id/messages
    ChatAPI->>ChatService: validateAndSend()
    ChatService->>ChatService: checkEligibility()
    ChatService->>ChatService: processTemplate()
    ChatService->>SMS: sendCustomText()
    SMS->>Provider: Send SMS
    Provider-->>SMS: Message ID
    SMS-->>ChatService: Success + ID
    ChatService->>ChatService: saveMessage()
    ChatService->>ChatService: trackUsage()
    ChatService->>Ably: publishMessage()
    Ably-->>ChatPanel: workbook:chat:message
    ChatAPI-->>ChatPanel: {success, message}
    ChatPanel->>ChatPanel: updateThread()
```

### Secondary Flow: Customer Reply Processing

1. Customer sends SMS to shortcode
2. Vonage/Twilio calls webhook `/api/webhooks/chat-inbound`
3. Webhook extracts phone and message
4. ChatMatchingService finds store by latest buy
5. If STOP command, process opt-out and return
6. Find or create thread for customer
7. Save inbound message
8. If first customer message in thread, unlock freetext
9. Publish Ably event to store channel
10. Staff sees notification in Workbook

```mermaid
sequenceDiagram
    actor Customer
    participant Provider as Vonage/Twilio
    participant Webhook as ChatWebhook
    participant Matching as MatchingService
    participant Thread as ChatThread
    participant Ably
    participant ChatPanel

    Customer->>Provider: Send SMS
    Provider->>Webhook: POST /chat-inbound
    Webhook->>Webhook: extractPhone()
    Webhook->>Matching: findStoreByPhone()
    Matching->>Matching: queryAllStores()
    Matching-->>Webhook: {store, customer, buy}
    Webhook->>Webhook: checkOptOut()
    Webhook->>Thread: findOrCreate()
    Webhook->>Thread: processInboundMessage()
    Thread->>Thread: unlockFreetext()
    Thread->>Thread: save()
    Webhook->>Ably: publishNewMessage()
    Ably-->>ChatPanel: workbook:chat:message
    ChatPanel->>ChatPanel: showNotification()
    Webhook-->>Provider: 200 OK
```

### Error Handling

- **Invalid phone format**: Log warning, return 200 to provider, don't create thread
- **No store match**: Log as unroutable, return 200, admin can review in monitoring
- **SMS send failure**: Save message with failed status, show error to staff, allow retry
- **Ably unavailable**: Queue event for retry, staff sees message on page refresh
- **Database error**: Return 500, log full error, alert monitoring

### Complex Logic: Opt-Out Processing

```
ALGORITHM: Process Opt-Out Request
INPUT: phone, message_text
OUTPUT: confirmation_sent

1. NORMALIZE: phone -> 10 digits
2. DETECT_OPTOUT_TYPE:
   - If message contains "STOP" alone → optout_type = 'all'
   - If message contains "STOP MARKETING" → optout_type = 'marketing'
   - If message contains variations (unsubscribe, cancel, quit) → optout_type = 'all'
   - Otherwise → not an opt-out, continue normal processing

3. CHECK_EXISTING:
   - Query loyaltyDoNotTextList for phone
   - If exists with 'all' → already fully opted out, send confirmation anyway
   - If exists with 'marketing' and new is 'all' → upgrade to 'all'

4. PROCESS_OPTOUT:
   - INSERT/UPDATE loyaltyDoNotTextList
   - Set optout_type, optout_source = 'sms', updated_at = NOW()

5. SEND_CONFIRMATION:
   - Within 5 minutes (TCPA requirement)
   - Single message only: "You've been unsubscribed from [type] messages from BuyerKiosk stores."
   - Log confirmation sent

6. CLOSE_THREADS:
   - Find all active threads for this phone
   - Set status = 'closed', closed_at = NOW()

7. RETURN: confirmation_sent = true
```

## Deployment View

### Single Application Deployment

- **Environment**: Existing PHP application on Apache/Nginx with PHP-FPM
- **Configuration**:
  - Add new routes to Slim application
  - Add webhook URL to Vonage/Twilio dashboard
  - No new environment variables required (uses existing SMS credentials)
- **Dependencies**:
  - Existing Vonage/Twilio SMS configuration
  - Existing Ably configuration
  - MySQL database access
- **Performance**:
  - Webhook must respond within 15 seconds (provider timeout)
  - Chat panel should load threads in <2 seconds
  - Real-time Ably notifications <5 seconds latency

### Database Migration Sequencing

1. **Central DB first**: Create `chat_sms_usage` table
2. **Update opt-out table**: Add columns to `loyaltyDoNotTextList`
3. **Per-store tables**: Create `chat_threads`, `chat_messages`, `chat_templates`
4. **Seed templates**: Insert default transactional templates

### Rollback Strategy

- Feature flag: `chat_enabled` in store settings (default false)
- Gradual rollout: Enable per-store
- If issues: Disable flag, chat panel hidden, webhooks log but don't process
- Database rollback: Keep tables, they're isolated from other features

## Cross-Cutting Concepts

### Pattern Documentation

```yaml
# Existing patterns used
- pattern: Multi-store database pattern
  relevance: CRITICAL
  why: "Chat data stored per-store; usage tracking in central DB"

- pattern: Workbook panel registration
  relevance: HIGH
  why: "Chat panel follows exact PANEL_REGISTRY pattern"

- pattern: Ably real-time events
  relevance: HIGH
  why: "workbook:chat:* events follow existing workbook:* pattern"

- pattern: API permission checking
  relevance: HIGH
  why: "checkAccess() + checkStoreGroup() on all endpoints"

# New patterns created
- pattern: Phone-to-store matching (NEW)
  relevance: HIGH
  why: "Novel algorithm for routing inbound SMS to correct store"

- pattern: Freetext unlock state machine (NEW)
  relevance: MEDIUM
  why: "Thread state controls staff messaging capabilities"
```

### System-Wide Patterns

- **Security**: Session + CSRF for staff/admin APIs; no auth on webhooks (provider IP validation recommended)
- **Error Handling**: Log all errors; return 200 to webhooks even on error; show user-friendly messages in UI
- **Performance**: Webhook must complete quickly; use queued sending for high volume
- **Logging/Auditing**: All messages stored; usage tracked for billing; opt-outs logged

### Implementation Patterns

#### Code Patterns and Conventions
- Follow existing `BuyerKiosk` namespace structure
- Controllers extend common base or use helper functions
- Models use PDO with prepared statements
- JavaScript uses vanilla ES6 with optional jQuery for DOM
- Handlebars for client-side templating (wrapped in Twig `{% raw %}`)

#### State Management Patterns
- Chat panel state in `ChatManager` class instance
- Thread list cached in memory, refreshed on Ably events
- Templates cached on page load via Twig data injection
- localStorage for panel layout preferences (via layout-manager.js)

#### Component Structure Pattern
```pseudocode
COMPONENT: ChatManager
  INITIALIZE:
    - Get typeNum from meta tag
    - Get CSRF token from meta tag
    - Get employee ID from session
    - Compile Handlebars templates

  BIND_EVENTS:
    - Send button click
    - Template picker open
    - Thread selection
    - Ably message subscription

  LOAD_DATA:
    - Fetch active threads on init
    - Fetch eligible customers
    - Templates injected from server

  RENDER:
    - Thread list with Handlebars
    - Message history with Handlebars
    - Show/hide freetext input based on unlock state
```

## Architecture Decisions

- [x] **ADR-1: Separate Inbound Webhook**
  - Choice: Create new `/api/webhooks/chat-inbound` instead of modifying `/api/sms/inbound`
  - Rationale: Avoids breaking legacy loyalty SMS handling; clean separation of concerns
  - Trade-offs: Two inbound endpoints to maintain; need to configure in provider dashboard
  - User confirmed: Yes (follows analysis recommendation)

- [x] **ADR-2: Per-Store Chat Tables**
  - Choice: Store `chat_threads`, `chat_messages`, `chat_templates` in per-store databases
  - Rationale: Matches existing multi-tenant pattern; data isolation; simpler queries
  - Trade-offs: Cross-store queries require iteration; usage tracking in central DB separately
  - User confirmed: Yes (matches CLAUDE.md architecture)

- [x] **ADR-3: Granular Opt-Out Support**
  - Choice: Support `STOP` (all) and `STOP MARKETING` (marketing only) opt-out types
  - Rationale: User-requested; allows transactional messages even if marketing opted out
  - Trade-offs: More complex opt-out logic; need to check type on each send
  - User confirmed: Yes (explicitly requested)

- [x] **ADR-4: Template-Only First Message**
  - Choice: Staff must use canned templates until customer replies
  - Rationale: Prevents spam; ensures professional first contact; owner control
  - Trade-offs: Less flexibility for staff; requires good template library
  - User confirmed: Yes (in PRD requirements)

- [x] **ADR-5: Real-Time via Ably (Not Polling)**
  - Choice: Use existing Ably infrastructure for instant message notifications
  - Rationale: Proven pattern in Workbook; sub-second latency; already configured
  - Trade-offs: Ably dependency; fallback needed if disconnected
  - User confirmed: Yes (follows existing pattern)

## Quality Requirements

- **Performance**:
  - Webhook response time: <5 seconds (target <1 second)
  - Thread list load: <2 seconds for 50 threads
  - Message send: <3 seconds end-to-end
  - Ably notification latency: <5 seconds

- **Reliability**:
  - Message delivery rate: 98%+ (matches SMS provider SLA)
  - Zero message loss on inbound (log before processing)
  - Graceful degradation if Ably unavailable

- **Security**:
  - All staff endpoints require session + CSRF
  - Admin endpoints require `uri_store_settings` permission
  - Opt-out processed before any message handling
  - No PII in error logs

- **Usability**:
  - Unread indicator visible without opening panel
  - Character counter always visible when composing
  - Template search by short name
  - One-click template selection

## Risks and Technical Debt

### Known Technical Issues
- Existing SMS webhooks have no signature verification (security gap)
- Flood protection is per-phone globally, not per-thread

### Technical Debt
- Inbound SMS logic split between `/api/sms/inbound` and new webhook
- Two template systems (seller_marketing_messages and chat_templates)
- Variable syntax inconsistent: `%var%` in seller marketing, `{{var}}` in chat

### Implementation Gotchas
- Phone numbers stored inconsistently (some with +1, some without)
- Store timezone must be used for "today's buys" calculation
- Ably channel name is typeNum (case-sensitive)
- Handlebars in Twig requires `{% raw %}` wrapper

## Test Specifications

### Critical Test Scenarios

**Scenario 1: Staff Sends Template Message Successfully**
```gherkin
Given: Staff has access to store "ou00"
And: Customer "John" has a buy entered today
And: No existing thread for this customer
When: Staff opens chat panel and selects customer
And: Staff selects "Pickup Ready" template
And: Staff clicks Send
Then: Message is sent via SMS provider
And: Thread is created with status "pending"
And: Message appears in thread with "Sending" status
And: Delivery webhook updates status to "Delivered"
```

**Scenario 2: Freetext Unlocks After Customer Reply**
```gherkin
Given: Staff sent canned message to customer
And: Thread status is "pending"
And: staffCanFreetext is false
When: Customer replies "Thanks!"
And: Webhook processes inbound message
Then: Thread status changes to "active"
And: staffCanFreetext becomes true
And: Staff sees freetext input enabled
And: Ably notification shows new message
```

**Scenario 3: Opt-Out Processed Correctly**
```gherkin
Given: Customer phone is not in do-not-text list
When: Customer sends "STOP" to shortcode
Then: Phone is added to loyaltyDoNotTextList with type "all"
And: Confirmation message sent within 5 minutes
And: Any active threads for this phone are closed
And: Customer no longer appears in eligible list
```

**Scenario 4: Staff Cannot Freetext Before Customer Reply**
```gherkin
Given: Thread exists with staffCanFreetext = false
When: Staff attempts to send freetext message
Then: API returns error "Must use template until customer replies"
And: Message is not sent
And: UI shows template picker instead of text input
```

**Scenario 5: Inbound Routes to Correct Store**
```gherkin
Given: Customer has buys at store "ou00" (yesterday) and "pa00" (today)
When: Customer sends SMS reply to shortcode
Then: Message is routed to store "pa00" (most recent buy)
And: Thread created/updated in pa00 database
And: ou00 does not receive the message
```

### Test Coverage Requirements

- **Business Logic**: Matching algorithm, eligibility rules, freetext unlock, opt-out processing
- **User Interface**: Panel rendering, template picker, character counter, Ably notifications
- **Integration Points**: SMS provider sends, Ably publishing, webhook processing
- **Edge Cases**: Phone format variations, same-day multi-store, expired threads
- **Performance**: Webhook response time under load
- **Security**: Permission checks, CSRF validation, opt-out enforcement

---

## UI Specifications

### Chat Panel Design

**Panel Type**: Floating overlay window
- **Behavior**: Floats over the Workbook content, can be dragged/repositioned
- **Minimize Location**: Minimizes to the KPI footer in the bottom right corner
- **Minimized State**: Chat icon with unread badge count visible in footer
- **Expand Action**: Click minimized icon to restore floating panel
- **Default Size**: 400px wide × 500px tall (resizable)

### Entry Points

Staff can access chat from multiple locations:

| Entry Point | Behavior |
|-------------|----------|
| **KPI Footer Icon** | Opens/restores floating chat overlay |
| **Buy Queue Badge** | Opens chat overlay, auto-scrolls to that customer's thread |
| **Completed Buys Page** | Chat icon on each row opens overlay to that thread |
| **Chat Overlay List** | Browse all eligible customers and active threads |

### Notification System

**Inbound Message Notifications**:
1. **Toast Notification**: Slide-in notification with customer name and message preview (auto-dismisses after 5 seconds)
2. **Buy Queue Badge**: Red badge appears on the specific buy queue item for that customer
3. **Footer Icon Badge**: Unread count badge on minimized chat icon in KPI footer

**Audio**: Optional chime sound (respects browser/system notification settings)

### Thread List UI

**Tab Structure**:
- **Active Tab**: Shows open/pending threads sorted by most recent message
- **Closed Tab**: Shows closed/archived threads for reference

**Thread Item Display**:
```
┌─────────────────────────────────┐
│ John D. - Buy #47          2:34p│
│ "Thanks! What time do you..."   │
│ ● Unread indicator (if unread)  │
└─────────────────────────────────┘
```

**Thread Item Fields**:
- Customer name (first name + last initial)
- Buy number (daily number)
- Timestamp of last message
- Message preview (truncated to ~40 chars)
- Unread indicator (dot or badge)

### Message Composer

**Freetext Locked State** (before customer reply):
```
┌─────────────────────────────────┐
│ [Awaiting customer reply...]    │ ← Grayed out, disabled
│ [Template Dropdown ▼] [Send]    │
└─────────────────────────────────┘
```
- Text input is visible but disabled (grayed out)
- Tooltip on hover: "Customer must reply before you can send custom messages"
- Template dropdown is active and usable

**Freetext Unlocked State** (after customer reply):
```
┌─────────────────────────────────┐
│ [Type your message here...]     │ ← Active, enabled
│ 0/320                           │ ← Character counter
│ [Template ▼] [Send]             │
└─────────────────────────────────┘
```
- Text input enabled
- Character counter always visible: "142/320"
- Template dropdown still available for convenience

### Template Picker

**In Process Buy Modal**:
- Dropdown in the modal with default transactional message pre-selected
- Owner-configurable options (transactional category only)
- Preview shows message with wildcards filled in

**In Chat Overlay**:
- Dropdown button next to send button
- Shows categorized templates: Initial Contact, Follow-up, Custom
- Click to insert template content into composer
- Preview with wildcards before sending

### Character Counter

**Display**: Always visible below message input
**Format**: `{current}/{max}` (e.g., "142/320")
**Visual States**:
- **Normal** (0-159): Default text color
- **Warning** (160-280): Yellow/amber color (multi-segment warning)
- **Danger** (281-320): Red color (approaching limit)
- **Over Limit** (>320): Red with send button disabled

### Process Buy Modal Integration

**SMS Section in Process Buy Modal**:
```
┌─────────────────────────────────────────┐
│ Send SMS Notification                    │
│ ┌─────────────────────────────────────┐ │
│ │ [Pickup Ready ▼]                    │ │ ← Default selected
│ └─────────────────────────────────────┘ │
│ Preview: "Hi John, your buy #47 is      │
│ ready for pickup! We're open until 8pm."│
│                                          │
│ [ ] Don't send SMS                       │
└─────────────────────────────────────────┘
```

### Buy Queue Badge

**Visual Design**:
- Small chat bubble icon with unread count
- Position: Right side of buy queue item row
- Color: Accent color (matches unread notification style)
- States:
  - No badge: No active chat or no unread messages
  - Badge with count: Unread messages in thread
  - Chat icon (no count): Active thread, all read

### Responsive Considerations

**Desktop/Tablet** (primary):
- Floating overlay as described
- Full feature set

**Small Screens** (<768px):
- Chat overlay expands to near-full-screen
- Minimize to footer icon only
- Thread list and conversation are separate views (not side-by-side)

---

## Glossary

### Domain Terms

| Term | Definition | Context |
|------|------------|---------|
| Thread | A conversation between store and customer | One thread per customer per buy |
| Freetext | Custom message typed by staff | Only available after customer replies |
| Canned Message | Pre-approved template message | Required for first contact |
| Transactional | Messages about specific transactions | "Your buy is ready" - free |
| Interactive | Conversation messages | Staff-initiated and replies - billable |
| Eligible Customer | Customer staff can message | Has same-day buy or open thread |

### Technical Terms

| Term | Definition | Context |
|------|------------|---------|
| typeNum | Store identifier (e.g., ou00, pa00) | Used for routing and database selection |
| staffCanFreetext | Thread flag for freetext permission | Unlocked when customer replies |
| Provider | SMS service (Vonage or Twilio) | Store-configurable |
| Segment | 160-character SMS unit | Multiple segments = higher cost |
| TCPA | Telephone Consumer Protection Act | Regulates SMS marketing compliance |

### API/Interface Terms

| Term | Definition | Context |
|------|------------|---------|
| msisdn | Mobile Station ISDN Number | Vonage's term for phone number |
| MessageSid | Twilio message identifier | Unique ID for tracking |
| Delivery Status | Provider-reported message state | pending → sent → delivered/failed |

---

*SDD Complete (including UI Specifications) - Ready for Implementation Plan*
