# 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 match repo patterns (planned commands labeled)
- [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** _(Approved 2025-12-19)_
- [x] Component names consistent across diagrams
- [x] A developer could implement from this design

---

## Constraints

**CON-1 Technical Stack**
- PHP 8.x, Slim 2.6.2, Twig 1.44.8, MySQL
- Same codebase deployed to demo server with environment flags
- Must use existing database patterns (`dbConnectByName()`)

**CON-2 Security Requirements**
- Complete isolation: demo environment cannot access production databases
- No production credentials stored on demo server
- Session tokens must expire after 24 hours
- Magic link tokens must be hashed before storage

**CON-3 Infrastructure**
- Demo runs on separate subdomain: `demo.buyerkiosk.com`
- Separate database server for demo data
- TaskEngine must run independently for demo simulation
- Flows.sh integration requires commercial license

**CON-4 Compatibility**
- Must maintain compatibility with existing Store, Buy, Customer models
- Shadow table reads must be usable without modifying core Store/Buy/Customer business logic
- MockDataGenerator must work unchanged on demo environment

---

## Implementation Context

### Required Context Sources

```yaml
# Internal documentation and patterns
- doc: docs/patterns/psr4-autoloading.md
  relevance: HIGH
  why: "New demo classes must follow PSR-4 autoloading under BuyerKiosk\Demo namespace"

- doc: docs/specs/015-task-engine/solution-design.md
  relevance: HIGH
  why: "MockDataGeneratorJob runs via TaskEngine; demo needs similar job scheduling"

# Source code files
- file: userfrosting/models/BaseModel.php
  relevance: CRITICAL
  sections: [dbConnectByName, getAllStoresData]
  why: "Core database connection pattern that demo must use"

- file: userfrosting/src/BuyerKiosk/Auth/Services/RememberMeService.php
  relevance: HIGH
  why: "Token pattern to adapt for magic link implementation"

- file: userfrosting/src/BuyerKiosk/TaskEngine/Services/PHPMailerAdapter.php
  relevance: HIGH
  why: "Email sending pattern for magic link delivery"

- file: userfrosting/mock/MockDataGenerator.php
  relevance: CRITICAL
  why: "Core simulation engine that will populate demo data"

- file: userfrosting/mock/SeasonalConfig.php
  relevance: HIGH
  why: "Historical data patterns for realistic simulation"

- file: userfrosting/middleware/UserSession.php
  relevance: HIGH
  why: "Session handling that demo middleware will extend"

- file: userfrosting/config-userfrosting.php
  relevance: HIGH
  sections: [environment detection, mode configuration]
  why: "Environment flag patterns for demo detection"

# External documentation
- url: https://flows.sh/docs
  relevance: HIGH
  sections: [SDK installation, flow definition, step types]
  why: "Product tour integration requirements"
```

### Implementation Boundaries

- **Must Preserve**:
  - Existing Store, Buy, Customer, Employee models unchanged
  - Production database connections and credentials
  - All existing route handlers (demo adds new routes, doesn't modify existing)

- **Can Modify**:
  - Add new Demo namespace under `src/BuyerKiosk/Demo/`
  - Add demo-specific routes in `routes/demo/`
  - Add demo middleware for session handling
  - Create new shadow tables in demo database

- **Must Not Touch**:
  - Production database schemas
  - Existing authentication middleware
  - Core Store/Buy model business logic
  - Production TaskEngine configuration

### External Interfaces

#### System Context Diagram

```mermaid
graph TB
    subgraph Demo Environment
        DemoApp[Demo Application<br>demo.buyerkiosk.com]
        DemoDB[(Demo Database<br>kiosk_demo_*)]
        DemoRedis[(Demo Redis)]
        DemoTaskEngine[Demo TaskEngine]
    end

    subgraph External Services
        FlowsSh[Flows.sh<br>Tour SDK]
        EmailService[Email Service<br>PHPMailer/SMTP]
        Analytics[Analytics<br>Internal/GA]
    end

    User[Demo User] --> |QR Code / Magic Link| DemoApp
    DemoApp --> DemoDB
    DemoApp --> DemoRedis
    DemoApp --> FlowsSh
    DemoApp --> EmailService
    DemoApp --> Analytics
    DemoTaskEngine --> DemoDB
    DemoTaskEngine --> DemoRedis

    subgraph Production - ISOLATED
        ProdDB[(Production DBs)]
    end

    DemoApp -.->|NO CONNECTION| ProdDB
```

#### Interface Specifications

```yaml
# Inbound Interfaces
inbound:
  - name: "Demo Landing Page"
    type: HTTPS
    format: HTML
    authentication: None (public)
    path: /demo
    data_flow: "Email capture, magic link request"

  - name: "Magic Link Entry"
    type: HTTPS
    format: HTML
    authentication: Token in URL
    path: /demo/access/:token
    data_flow: "Token validation, session creation"

  - name: "Demo Admin Routes"
    type: HTTPS
    format: HTML/JSON
    authentication: Demo session token
    path: /demo/:concept/admin/*
    data_flow: "Demo admin pages with shadow table reads/writes"

  - name: "Demo API Routes"
    type: HTTPS
    format: JSON
    authentication: Demo session token
    path: /api/demo/*
    data_flow: "Demo-specific API operations"

# Outbound Interfaces
outbound:
  - name: "Email Service (Magic Links)"
    type: SMTP
    format: HTML email
    authentication: SMTP credentials
    data_flow: "Magic link delivery"
    criticality: HIGH

  - name: "Flows.sh SDK"
    type: HTTPS (CDN)
    format: JavaScript SDK
    authentication: Client API key
    data_flow: "Tour definitions, step completion tracking"
    criticality: MEDIUM

  - name: "Analytics Tracking"
    type: HTTPS
    format: JSON events
    authentication: API key
    data_flow: "Session, page view, conversion events"
    criticality: LOW

# Data Interfaces
data:
  - name: "Demo Database"
    type: MySQL
    connection: PDO via dbConnectByName()
    databases:
      - kiosk_demo (central demo data)
      - kiosk_demo_pc (Plato's Closet demo store)
      - kiosk_demo_ou (Once Upon A Child demo store)
      - kiosk_demo_se (Style Encore demo store)
    data_flow: "Demo store data, shadow tables, session management"

  - name: "Demo Redis"
    type: Redis
    connection: Predis client
    data_flow: "Session cache, rate limiting, TaskEngine queues"
```

### Project Commands

```bash
# Component: Demo Dashboard
Location: userfrosting/

## Environment Setup
Install Dependencies: cd userfrosting && composer install
Environment Variables:
  - Copy .env.example to .env
  - Set DEMO_MODE=1 (or configure via subdomain detection)
  - Set DEMO_DB_HOST, DEMO_DB_USER, DEMO_DB_PASS
  - Set DEMO_EMAIL_* for magic link SMTP
  - Set FLOWS_SH_API_KEY for tour integration
Start Development: php -S localhost:8080 -t public_html

# Testing Commands
Unit Tests: ./test.sh --testsuite unit
Integration Tests: ./test.sh --testsuite integration
Test Coverage: ./test.sh --coverage
PHPStan Analysis: cd userfrosting && ./vendor/bin/phpstan analyse

# Database Operations
Database Migration: php userfrosting/conductor run
Demo Database Setup (planned): php userfrosting/conductor demo:setup
Demo Data Seed (planned): php userfrosting/conductor demo:seed

# CSS Build (if demo needs custom styles)
Build CSS: php userfrosting/conductor build-css
Production CSS: php userfrosting/conductor build-css --minify

# TaskEngine Commands (for demo simulation)
Start Worker: php userfrosting/conductor task worker:start --queues=demo
Scheduler Run: php userfrosting/conductor task scheduler:run
Dispatch Mock Data: php userfrosting/conductor task job:dispatch MockDataGenerator --store=pc00
```

---

## Solution Strategy

### Architecture Pattern: Layered with Shadow Table Overlay

**Pattern Description:**
The demo system uses the existing layered architecture (Controller → Service → Model → Database) with an additional "Shadow Table Overlay" pattern that intercepts writes and merges reads at the data access layer.

**Key Components:**
1. **Demo Middleware** - Detects demo environment and injects demo context
2. **Shadow Table Service** - Manages session-scoped writes and merged reads
3. **Demo Session Service** - Magic link tokens and 24-hour sessions
4. **Demo Database Connector** - Routes queries to demo databases
5. **Tour Integration** - Flows.sh SDK loader and step tracking

**Integration Approach:**
- Demo runs on separate subdomain detected via `$_SERVER['HTTP_HOST']`
- Environment flag `DEMO_MODE` enables demo-specific behavior
- Existing domain models/services can be reused; demo uses its own routes/controllers (no production route changes)
- MockDataGenerator runs on demo TaskEngine instance

**Justification:**
- **Minimal Code Changes**: Existing controllers don't need modification
- **Clean Separation**: Demo code isolated in `BuyerKiosk\Demo` namespace
- **Testable**: Shadow table logic can be unit tested independently
- **Scalable**: Can support multiple concurrent demo sessions

**Key Decisions:**
1. Shadow tables use session_id prefix for isolation
2. Magic links use RememberMeService token pattern (adapted)
3. Demo databases follow naming convention: `kiosk_demo_{concept}`
4. Flows.sh loaded via async script tag, configured per-page

---

## Building Block View

### Components

```mermaid
graph TB
    subgraph Presentation Layer
        LandingPage[Landing Page<br>/demo]
        ConceptSelect[Concept Selection<br>/demo/select]
        DemoAdmin[Demo Admin Pages<br>/demo/:concept/admin/*]
        TourOverlay[Flows.sh Tour<br>Overlay]
    end

    subgraph Application Layer
        DemoMiddleware[DemoMiddleware<br>Environment Detection]
        DemoSessionService[DemoSessionService<br>Magic Link + Sessions]
        ShadowTableService[ShadowTableService<br>Write Interception]
        TourService[TourService<br>Progress Tracking]
        AnalyticsService[AnalyticsService<br>Event Tracking]
    end

    subgraph Data Layer
        DemoDbConnector[DemoDbConnector<br>Database Routing]
        ShadowTableRepository[ShadowTableRepository<br>Merged Reads/Writes]
        SessionRepository[SessionRepository<br>Token Management]
        LeadRepository[LeadRepository<br>Email + Analytics]
    end

    subgraph Infrastructure
        DemoDatabase[(Demo MySQL)]
        DemoRedis[(Demo Redis)]
        EmailAdapter[PHPMailerAdapter]
        FlowsSdk[Flows.sh SDK]
    end

    LandingPage --> DemoSessionService
    ConceptSelect --> DemoSessionService
    DemoAdmin --> DemoMiddleware
    DemoMiddleware --> ShadowTableService
    TourOverlay --> TourService

    DemoSessionService --> SessionRepository
    DemoSessionService --> EmailAdapter
    ShadowTableService --> ShadowTableRepository
    TourService --> AnalyticsService
    AnalyticsService --> LeadRepository

    SessionRepository --> DemoDatabase
    ShadowTableRepository --> DemoDbConnector
    LeadRepository --> DemoDatabase
    DemoDbConnector --> DemoDatabase

    DemoSessionService --> DemoRedis
    ShadowTableService --> DemoRedis
```

### Directory Map

**Component**: Demo Dashboard

```
userfrosting/
├── src/BuyerKiosk/
│   └── Demo/                                    # NEW: Demo namespace
│       ├── Controllers/
│       │   ├── DemoLandingController.php        # NEW: Landing + email capture
│       │   ├── DemoAccessController.php         # NEW: Magic link handling
│       │   ├── DemoConceptController.php        # NEW: Concept selection
│       │   └── DemoAdminController.php          # NEW: Demo admin wrapper
│       ├── Services/
│       │   ├── DemoSessionService.php           # NEW: Magic link + session
│       │   ├── ShadowTableService.php           # NEW: Write interception
│       │   ├── DemoDbConnector.php              # NEW: DB routing
│       │   └── TourService.php                  # NEW: Flows.sh integration
│       ├── Repositories/
│       │   ├── SessionRepository.php            # NEW: Token storage
│       │   ├── ShadowTableRepository.php        # NEW: Shadow table ops
│       │   └── LeadRepository.php               # NEW: Lead tracking
│       ├── Middleware/
│       │   └── DemoMiddleware.php               # NEW: Request handling
│       └── Models/
│           ├── DemoSession.php                  # NEW: Session entity
│           └── DemoLead.php                     # NEW: Lead entity
│
├── routes/
│   └── demo/                                    # NEW: Demo routes
│       ├── landing.php                          # NEW: Public routes
│       ├── access.php                           # NEW: Auth routes
│       └── admin.php                            # NEW: Protected routes
│
├── templates/themes/default/
│   └── demo/                                    # NEW: Demo templates
│       ├── landing.html                         # NEW: Email capture page
│       ├── concept-select.html                  # NEW: PC/OU/SE selection
│       ├── tour/                                # NEW: Tour step content
│       │   ├── welcome.html
│       │   ├── workbook-intro.html
│       │   └── ... (per-feature steps)
│       └── partials/
│           ├── flows-init.html                  # NEW: Flows.sh loader
│           └── cta-banner.html                  # NEW: Sales CTA
│
├── migrations/input/
│   ├── 20251219_001_demo_sessions.json          # NEW: Session table
│   ├── 20251219_002_demo_leads.json             # NEW: Lead tracking
│   ├── 20251219_003_demo_shadow_tables.json     # NEW: Shadow tables
│   └── 20251219_004_demo_analytics.json         # NEW: Analytics events
│
├── mock/
│   └── DemoMockConfig.php                       # NEW: Demo-specific config
│
└── public_html/
    ├── css/admin/modules/
    │   └── demo.css                             # NEW: Demo-specific styles
    └── js/demo/
        ├── tour-config.js                       # NEW: Flows.sh configuration
        └── analytics.js                         # NEW: Event tracking
```

### Interface Specifications

#### Data Storage Changes

```yaml
# Demo Central Database: kiosk_demo

Table: demo_sessions (NEW)
  id: INT AUTO_INCREMENT PRIMARY KEY
  email: VARCHAR(255) NOT NULL
  magic_link_token_hash: VARCHAR(64) NOT NULL  # SHA-256 hash (never store plaintext)
  magic_link_expires_at: DATETIME NOT NULL
  magic_link_used_at: DATETIME NULL
  session_token_hash: VARCHAR(64) NULL  # SHA-256 hash; set after first click
  concept: ENUM('pc', 'ou', 'se') NULL
  source_qr_id: VARCHAR(50) NULL  # Event tracking
  created_at: DATETIME NOT NULL
  session_expires_at: DATETIME NULL
  last_accessed_at: DATETIME NULL
  tour_progress: JSON NULL  # {"section": "workbook", "step": 3}
  INDEX idx_magic_link_token (magic_link_token_hash)
  INDEX idx_session_token (session_token_hash)
  INDEX idx_email (email)
  INDEX idx_magic_link_expires (magic_link_expires_at)
  INDEX idx_session_expires (session_expires_at)

Table: demo_leads (NEW)
  id: INT AUTO_INCREMENT PRIMARY KEY
  session_id: INT NOT NULL REFERENCES demo_sessions(id)
  email: VARCHAR(255) NOT NULL
  company_name: VARCHAR(255) NULL
  phone: VARCHAR(50) NULL
  source_qr_id: VARCHAR(50) NULL
  cta_clicked: VARCHAR(50) NULL  # Which CTA triggered contact
  created_at: DATETIME NOT NULL
  contacted_at: DATETIME NULL  # When sales followed up
  INDEX idx_email (email)
  INDEX idx_source (source_qr_id)

Table: demo_qr_codes (OPTIONAL)
  # Only needed if QR code creation/management happens inside the demo environment.
  # MVP can treat source_qr_id as an opaque string passed via URL query params.
  id: INT AUTO_INCREMENT PRIMARY KEY
  code_id: VARCHAR(50) UNIQUE NOT NULL  # e.g., "nrf2025", "booth42"
  event_name: VARCHAR(255) NOT NULL
  created_at: DATETIME NOT NULL
  expires_at: DATETIME NULL
  created_by_user_id: INT NULL
  sessions_count: INT DEFAULT 0

Table: demo_analytics_events (NEW)
  id: BIGINT AUTO_INCREMENT PRIMARY KEY
  session_id: INT NOT NULL REFERENCES demo_sessions(id)
  event_type: VARCHAR(50) NOT NULL  # page_view, feature_click, tour_step, cta_click
  event_data: JSON NOT NULL
  page_path: VARCHAR(255) NULL
  created_at: DATETIME NOT NULL
  INDEX idx_session (session_id)
  INDEX idx_type_time (event_type, created_at)

# Demo Store Database: kiosk_demo_pc (and _ou, _se)
# Contains same schema as regular store databases PLUS shadow tables

Table: shadow_buys (NEW)
  id: INT AUTO_INCREMENT PRIMARY KEY
  session_id: INT NOT NULL
  original_buy_id: INT NULL  # If modifying existing record
  operation: ENUM('insert', 'update', 'delete') NOT NULL
  data: JSON NOT NULL  # Full row data
  created_at: DATETIME NOT NULL
  INDEX idx_session (session_id)
  INDEX idx_original (original_buy_id)

Table: shadow_customers (NEW)
  # Same structure as shadow_buys for customer records

Table: shadow_tasks (NEW)
  # Same structure for workbook tasks

Table: shadow_events (NEW)
  # Same structure for store events

Table: shadow_schedules (NEW)
  # Same structure for scheduling data

Table: shadow_notes (NEW)
  # Same structure for notes/comments (workbook notes, buy notes, etc.)
```

#### Internal API Changes

```yaml
# Demo Public Endpoints (no auth required)

Endpoint: Request Magic Link
  Method: POST
  Path: /api/demo/request-access
  Request:
    email: string, required, valid email format
    source_qr_id: string, optional
  Response:
    success:
      message: "Magic link sent to your email"
      expires_in: 300  # 5 minutes for link validity
    error:
      error_code: "RATE_LIMITED" | "INVALID_EMAIL"
      message: string

Endpoint: Magic Link Entry (HTML)
  Method: GET
  Path: /demo/access/:token
  Authentication: Token in URL (one-time)
  Behavior:
    - Validates token hash + expiry
    - Marks token used (single-use)
    - Creates 24-hour demo session + sets cookie
    - Redirects to /demo/select (concept selection)

# Demo Protected Endpoints (session required)

Endpoint: Select Concept
  Method: POST
  Path: /api/demo/select-concept
  Authentication: Cookie demo_session (recommended) OR header X-Demo-Session (optional)
  Request:
    concept: "pc" | "ou" | "se"
  Response:
    success:
      redirect_url: "/demo/pc/admin/"
    error:
      error_code: "INVALID_CONCEPT" | "SESSION_EXPIRED"

Endpoint: Track Event
  Method: POST
  Path: /api/demo/track
  Authentication: Cookie demo_session (recommended) OR header X-Demo-Session (optional)
  Request:
    event_type: string
    event_data: object
    page_path: string
  Response:
    success: { tracked: true }

Endpoint: Update Tour Progress
  Method: POST
  Path: /api/demo/tour/progress
  Authentication: Cookie demo_session (recommended) OR header X-Demo-Session (optional)
  Request:
    section: string
    step: int
    completed: boolean
  Response:
    success:
      next_step: { section: string, step: int } | null

Endpoint: Submit Contact Form
  Method: POST
  Path: /api/demo/contact
  Authentication: Cookie demo_session (recommended) OR header X-Demo-Session (optional)
  Request:
    company_name: string, optional
    phone: string, optional
    message: string, optional
  Response:
    success:
      message: "Thanks! Our team will reach out soon."
      lead_id: int
```

#### Application Data Models

```pseudocode
ENTITY: DemoSession (NEW)
  FIELDS:
    id: int
    email: string
    magicLinkTokenHash: string
    magicLinkExpiresAt: DateTime
    magicLinkUsedAt: DateTime | null
    sessionTokenHash: string | null
    concept: enum('pc', 'ou', 'se') | null
    sourceQrId: string | null
    createdAt: DateTime
    sessionExpiresAt: DateTime | null
    lastAccessedAt: DateTime | null
    tourProgress: object | null

  BEHAVIORS:
    isMagicLinkExpired(): bool
    isSessionExpired(): bool
    remainingSessionTime(): int  # seconds
    getConceptDbName(): string  # e.g., "kiosk_demo_pc"
    updateLastAccessed(): void
    setTourProgress(section, step): void

ENTITY: DemoLead (NEW)
  FIELDS:
    id: int
    sessionId: int
    email: string
    companyName: string | null
    phone: string | null
    sourceQrId: string | null
    ctaClicked: string | null
    createdAt: DateTime
    contactedAt: DateTime | null

  BEHAVIORS:
    markContacted(): void
    toArray(): array  # For export

ENTITY: ShadowRecord (NEW)
  FIELDS:
    id: int
    sessionId: int
    originalId: int | null
    operation: enum('insert', 'update', 'delete')
    tableName: string
    data: array
    createdAt: DateTime

  BEHAVIORS:
    apply(baseRecord): array  # Merge with base data
    isDelete(): bool
```

#### Integration Points

```yaml
# Inter-Component Communication

- from: DemoMiddleware
  to: DemoSessionService
  protocol: Direct PHP call
  data_flow: "Validate session token from header/cookie"

- from: ShadowTableService
  to: ShadowTableRepository
  protocol: Direct PHP call
  data_flow: "Store writes, retrieve merged reads"

- from: DemoLandingController
  to: PHPMailerAdapter
  protocol: Direct PHP call
  data_flow: "Send magic link emails"

- from: TourService
  to: AnalyticsService
  protocol: Direct PHP call
  data_flow: "Track tour step completion"

# External System Integration

Flows.sh:
  - doc: https://flows.sh/docs/sdk
  - sections: [installation, flow-definition, javascript-api]
  - integration: "SDK loaded async, flows defined per-page"
  - critical_data: [flow_id, step_id, completion_events]

Email Service:
  - doc: userfrosting/src/BuyerKiosk/TaskEngine/Services/PHPMailerAdapter.php
  - integration: "SMTP via PHPMailer, same pattern as FailureNotifier"
  - critical_data: [recipient_email, magic_link_url, expiry_time]
```

### Implementation Examples

#### Example: Shadow Table Read Merge

**Why this example**: Demonstrates the core shadow table overlay pattern that makes user changes appear while preserving simulation data.

```php
// Example: Merged read for buy queue
// This shows how shadow table data overlays baseline data

class ShadowTableService
{
    public function getMergedBuys(int $sessionId, string $conceptDb): array
    {
        // 1. Get baseline data (from simulation)
        $baselineDb = dbConnectByName($conceptDb);
        $baselineBuys = $this->fetchAllBuys($baselineDb);

        // 2. Get shadow operations for this session
        $shadows = $this->shadowRepo->getBySession($sessionId, 'buys');

        // 3. Apply shadow operations
        $result = [];
        $deletedIds = [];
        $modifiedIds = [];

        foreach ($shadows as $shadow) {
            if ($shadow->operation === 'delete') {
                $deletedIds[] = $shadow->originalId;
            } elseif ($shadow->operation === 'update') {
                $modifiedIds[$shadow->originalId] = $shadow->data;
            } elseif ($shadow->operation === 'insert') {
                // Negative IDs for shadow inserts (won't collide with real IDs)
                $result[] = array_merge($shadow->data, ['id' => -$shadow->id]);
            }
        }

        // 4. Merge: baseline - deleted + modified + inserted
        foreach ($baselineBuys as $buy) {
            if (in_array($buy['id'], $deletedIds)) {
                continue; // Skip deleted
            }
            if (isset($modifiedIds[$buy['id']])) {
                $result[] = array_merge($buy, $modifiedIds[$buy['id']]);
            } else {
                $result[] = $buy;
            }
        }

        return $result;
    }
}
```

#### Example: Magic Link Token Flow

**Why this example**: Shows the secure token pattern adapted from RememberMeService for demo access.

```php
// Example: Magic link creation and validation
// Adapted from RememberMeService pattern

class DemoSessionService
{
    private const TOKEN_EXPIRY_MINUTES = 5;  // Magic link valid for 5 minutes
    private const SESSION_EXPIRY_HOURS = 24;  // Session valid for 24 hours

    public function createMagicLink(string $email, ?string $sourceQrId = null): string
    {
        // Generate secure token (64 hex chars)
        $plaintextToken = bin2hex(random_bytes(32));
        $hashedToken = hash('sha256', $plaintextToken);

        // Store hashed token with short expiry
        $this->sessionRepo->createPendingSession([
            'email' => $email,
            'magic_link_token_hash' => $hashedToken,
            'source_qr_id' => $sourceQrId,
            'magic_link_expires_at' => date('Y-m-d H:i:s', strtotime('+' . self::TOKEN_EXPIRY_MINUTES . ' minutes')),
        ]);

        // Return plaintext for URL (never stored)
        return $plaintextToken;
    }

    public function validateAndActivate(string $plaintextToken): ?DemoSession
    {
        $hashedToken = hash('sha256', $plaintextToken);

        $pending = $this->sessionRepo->findByMagicLinkTokenHash($hashedToken);
        if (!$pending || $pending->isMagicLinkExpired() || $pending->magicLinkUsedAt) {
            return null;
        }

        // Generate new session token (different from magic link token)
        $sessionToken = bin2hex(random_bytes(32));
        $sessionTokenHash = hash('sha256', $sessionToken);

        // Upgrade to full session with 24-hour expiry and invalidate magic link (single-use)
        $session = $this->sessionRepo->activateSession($pending->id, [
            'magic_link_used_at' => date('Y-m-d H:i:s'),
            'session_token_hash' => $sessionTokenHash,
            'session_expires_at' => date('Y-m-d H:i:s', strtotime('+' . self::SESSION_EXPIRY_HOURS . ' hours')),
        ]);

        return $session->withToken($sessionToken);
    }
}
```

#### Test Examples as Interface Documentation

```php
// Example: Unit test documenting ShadowTableService contract

class ShadowTableServiceTest extends TestCase
{
    public function testMergedReadsShowUserChangesOverBaseline(): void
    {
        // Setup: baseline has 3 buys
        $baseline = [
            ['id' => 1, 'status' => 'pending', 'customer' => 'Alice'],
            ['id' => 2, 'status' => 'pending', 'customer' => 'Bob'],
            ['id' => 3, 'status' => 'pending', 'customer' => 'Carol'],
        ];

        // User actions: complete buy 1, delete buy 2, add new buy
        $shadows = [
            new ShadowRecord(sessionId: 99, originalId: 1, operation: 'update',
                data: ['status' => 'completed']),
            new ShadowRecord(sessionId: 99, originalId: 2, operation: 'delete'),
            new ShadowRecord(sessionId: 99, originalId: null, operation: 'insert',
                data: ['status' => 'pending', 'customer' => 'Dan']),
        ];

        $service = new ShadowTableService($mockRepo);
        $merged = $service->getMergedBuys(99, 'kiosk_demo_pc');

        // Expectations:
        // - Buy 1 shows as completed (updated)
        // - Buy 2 is not present (deleted)
        // - Buy 3 unchanged
        // - New buy from Dan is present
        $this->assertCount(3, $merged);
        $this->assertEquals('completed', $merged[0]['status']); // Buy 1 updated
        $this->assertEquals('Carol', $merged[1]['customer']);   // Buy 3 unchanged
        $this->assertEquals('Dan', $merged[2]['customer']);     // New insert
    }
}
```

---

## Runtime View

### Primary Flow

#### Primary Flow: QR Code to Demo Access

1. User scans QR code at conference
2. Browser opens demo landing page
3. User enters email address
4. System sends magic link email
5. User clicks magic link
6. System validates token, creates session
7. User selects store concept (PC/OU/SE)
8. User enters demo with guided tour

```mermaid
sequenceDiagram
    actor User
    participant Landing as Landing Page
    participant API as Demo API
    participant Email as Email Service
    participant Session as SessionService
    participant DB as Demo DB

    User->>Landing: Scan QR → Open /demo
    Landing->>User: Show email capture form
    User->>API: POST /api/demo/request-access
    API->>Session: createMagicLink(email)
    Session->>DB: Store hashed token
    Session-->>API: Return plaintext token
    API->>Email: Send magic link email
    Email-->>User: Email with link
    API-->>Landing: "Check your email"

    User->>API: Click link → GET /demo/access/:token
    API->>Session: validateAndActivate(token)
    Session->>DB: Find by hash, check expiry
    Session->>DB: Upgrade to 24hr session
    Session-->>API: Return session + new token
    API-->>User: Redirect to concept selection

    User->>API: POST /api/demo/select-concept
    API->>Session: Set concept on session
    API-->>User: Redirect to /demo/pc/admin/

    User->>Landing: View demo with tour overlay
```

### Error Handling

| Error Type | User Message | System Action |
|------------|--------------|---------------|
| Invalid email format | "Please enter a valid email address" | Client-side validation |
| Rate limited | "Too many requests. Please wait 5 minutes." | Redis rate limit (5 req/5 min per IP) |
| Invalid/expired token | "This link has expired. Request a new one." | Redirect to landing with message |
| Session expired (24h) | "Your demo session has ended." | Clear cookie, redirect to landing |
| Database connection error | "Service temporarily unavailable." | Log error, show retry option |
| Email delivery failure | "Could not send email. Please try again." | Log, allow retry after 1 minute |

### Complex Logic: Shadow Table Merge

```
ALGORITHM: Merge Shadow Tables with Baseline
INPUT: session_id, table_name, baseline_query
OUTPUT: merged_result_set

1. EXECUTE baseline_query → baseline_records
2. FETCH shadow_records WHERE session_id AND table_name
3. PARTITION shadows BY operation:
   - deletes: original_id to remove
   - updates: original_id → new_data mapping
   - inserts: new records to add
4. FOR EACH baseline_record:
   IF id IN deletes: SKIP
   ELSE IF id IN updates: MERGE(baseline, update_data)
   ELSE: KEEP baseline
5. APPEND inserts (with negative IDs)
6. RETURN merged_result_set
```

---

## Deployment View

### Demo Environment Configuration

- **Environment**: Separate server (demo.buyerkiosk.com)
- **Detection**: `$_SERVER['HTTP_HOST']` check OR `DEMO_MODE=1` env var
- **Configuration**:
  ```bash
  # Demo-specific environment variables
  DEMO_MODE=1
  DEMO_DB_HOST=demo-db.internal
  DEMO_DB_USER=demo_user
  DEMO_DB_PASS=<secure_password>
  DEMO_REDIS_HOST=demo-redis.internal
  DEMO_EMAIL_HOST=smtp.sendgrid.net
  DEMO_EMAIL_USER=apikey
  DEMO_EMAIL_PASS=<sendgrid_api_key>
  FLOWS_SH_API_KEY=<flows_api_key>
  ```

- **Dependencies**:
  - MySQL server (isolated demo databases)
  - Redis (sessions, rate limiting, TaskEngine queues)
  - SMTP access (magic link emails)
  - Flows.sh account (tour SDK)

- **Performance**:
  - Expected load: 100-500 concurrent sessions
  - Response time target: <500ms for page loads
  - Magic link delivery: <10 seconds
  - Shadow table queries: <100ms overhead

### Deployment Strategy

- **Deployment Order**:
  1. Create demo databases (kiosk_demo, kiosk_demo_pc, etc.)
  2. Run migrations on demo databases
  3. Deploy code to demo server
  4. Configure environment variables
  5. Start demo TaskEngine workers
  6. Seed initial mock data
  7. Verify Flows.sh integration

- **Feature Flags**:
  - `DEMO_MODE`: Master switch for demo behavior
  - `DEMO_TOUR_ENABLED`: Toggle guided tour
  - `DEMO_ANALYTICS_ENABLED`: Toggle detailed tracking

- **Rollback Strategy**:
  - Demo is isolated; production unaffected
  - Database rollback: Restore from backup
  - Code rollback: Redeploy previous version

---

## Cross-Cutting Concepts

### Pattern Documentation

```yaml
# Existing patterns used
- pattern: docs/patterns/psr4-autoloading.md
  relevance: CRITICAL
  why: "Demo classes follow same autoloading convention"

- pattern: RememberMeService token pattern
  relevance: HIGH
  why: "Magic link implementation follows same secure token approach"

- pattern: TaskEngine job pattern
  relevance: HIGH
  why: "Demo simulation uses MockDataGeneratorJob"

# New patterns created
- pattern: docs/patterns/shadow-table-overlay.md (NEW)
  relevance: CRITICAL
  why: "Core pattern for session-scoped data isolation"
```

### System-Wide Patterns

- **Security**:
  - SHA-256 token hashing (never store plaintext)
  - Session cookies with httponly, secure, samesite
  - Rate limiting on email requests (5 per 5 minutes per IP)
  - 24-hour session expiration enforced server-side

- **Error Handling**:
  - All demo errors log to separate demo error log
  - User-facing errors are friendly, technical details logged
  - Session expiration redirects to landing gracefully

- **Performance**:
  - Shadow table queries add <100ms overhead
  - Session validation cached in Redis (TTL = remaining session time)
  - Flows.sh SDK loaded async (non-blocking)

- **Logging/Auditing**:
  - All demo sessions logged with source QR ID
  - Analytics events stored for funnel analysis
  - Lead capture with full attribution chain

### Component Structure Pattern

```pseudocode
# Demo Controller Pattern
CONTROLLER: DemoFeatureController
  DEPENDENCIES: DemoSessionService, ShadowTableService

  BEFORE_EACH_ACTION:
    session = DemoMiddleware.getValidSession(request)
    IF !session: REDIRECT to /demo (landing)
    IF session.expired: REDIRECT to /demo (with expiry message)

  ACTION: showFeature(request)
    conceptDb = session.getConceptDbName()
    data = ShadowTableService.getMergedData(session.id, conceptDb)
    tourStep = TourService.getCurrentStep(session)
    RENDER template WITH {data, session, tourStep}

  ACTION: updateFeature(request)
    validated = validate(request.input)
    ShadowTableService.recordWrite(session.id, 'update', validated)
    AnalyticsService.track('feature_interaction', request)
    REDIRECT back WITH success message
```

---

## Architecture Decisions

### ADR-1: Shadow Table Pattern for User Isolation
- **Choice**: Use shadow tables with session ID to isolate user writes
- **Rationale**: Allows simulation to run continuously on baseline while each user sees their own changes. No database cloning overhead, no transaction conflicts.
- **Trade-offs**: Adds query complexity (merge logic), requires careful cleanup of expired session data.
- **Alternatives Considered**:
  - Database cloning per session: High storage/management overhead
  - Transaction-level rollback: Conflicts with simulation writes
  - Read-only mode: Less immersive demo experience
- **User confirmed**: ✅ Approved 2025-12-19

### ADR-2: Magic Link Authentication (No Passwords)
- **Choice**: Email-based magic links with 5-minute validity, 24-hour sessions
- **Rationale**: Zero friction for conference attendees, captures email for lead tracking, familiar pattern from consumer apps.
- **Trade-offs**: Requires working email, slight delay (wait for email).
- **Alternatives Considered**:
  - Simple registration: More friction, lower conversion
  - QR code with embedded token: Security risk if QR is photographed
  - Guest access without email: No lead capture
- **User confirmed**: ✅ Approved 2025-12-19

### ADR-3: Separate Demo Server/Database
- **Choice**: Completely isolated infrastructure (demo.buyerkiosk.com, separate MySQL server)
- **Rationale**: Maximum security, no risk of production data exposure, clean operational boundary.
- **Trade-offs**: Additional infrastructure cost, need to sync code deploys.
- **Alternatives Considered**:
  - Same server with separate database: Network-level risk
  - Read-only production snapshot: Data sanitization complexity
- **User confirmed**: ✅ Approved 2025-12-19

### ADR-4: Flows.sh for Product Tour
- **Choice**: Use Flows.sh SDK for interactive walkthrough
- **Rationale**: WYSIWYG editor, wait/branch steps, commercial quality, dedicated support.
- **Trade-offs**: Commercial license cost, external dependency.
- **Alternatives Considered**:
  - Shepherd.js: Free but no visual editor
  - Custom solution: Development time
  - Driver.js: Minimal features
- **User confirmed**: ✅ Approved 2025-12-19

### ADR-5: Same Codebase with Environment Flags
- **Choice**: Deploy identical codebase, differentiate via `DEMO_MODE` environment variable
- **Rationale**: Prevents code drift, easier maintenance, CI/CD can deploy to both environments.
- **Trade-offs**: Must be careful with feature flags, potential for production code to check demo flag.
- **Alternatives Considered**:
  - Separate branch: Code drift risk
  - Docker with different config: More complexity
- **User confirmed**: ✅ Approved 2025-12-19

---

## Quality Requirements

| Requirement | Target | Measurement |
|-------------|--------|-------------|
| **Response Time** | <500ms for page loads | Server response time (P95) |
| **Magic Link Delivery** | <10 seconds | Email send to receipt |
| **Session Validation** | <50ms | Redis cache lookup |
| **Shadow Table Overhead** | <100ms | Query time delta vs baseline |
| **Concurrent Sessions** | 500+ | Load test target |
| **Uptime** | 99% (demo is non-critical) | Monitoring |
| **Tour Completion Rate** | Track baseline | Analytics dashboard |

---

## Risks and Technical Debt

### Known Technical Issues
- None for greenfield demo system

### Technical Debt
- **Shadow Table Cleanup**: Need scheduled job to purge expired session shadows
- **Flows.sh Dependency**: If service unavailable, tour degrades gracefully (hide tour)

### Implementation Gotchas
- **Cookie Domain**: Demo cookies must not conflict with production (`demo.buyerkiosk.com` vs `buyerkiosk.com`)
- **Store Model Assumptions**: Some code may assume `dbConnectByName` returns production-like database; demo databases must have same schema
- **MockDataGenerator Assumptions**: MockDataGenerator expects real `typeNum` values matching `[a-z]{2}\\d+` (e.g., `pc00`, `ou00`, `se00`) and connects via `StoreController` → `Store->getDbName()`
- **External Side Effects**: MockDataGenerator initializes Ably + WhenIWork integrations; demo mode must disable these integrations (except magic link email + analytics) to avoid outbound messages and API calls
- **Session Expiry Timing**: Display remaining time to user; warn at 1 hour remaining

---

## Test Specifications

### Critical Test Scenarios

**Scenario 1: Magic Link Flow**
```gherkin
Given: A user on the demo landing page
When: User enters valid email and submits
Then: Magic link email is sent within 10 seconds
And: Clicking link within 5 minutes creates valid session
And: Clicking expired link shows friendly error
```

**Scenario 2: Shadow Table Isolation**
```gherkin
Given: Two users with active demo sessions
When: User A completes a buy in their session
Then: User A sees the completed buy
And: User B does not see User A's completed buy
And: Both users see simulation data
```

**Scenario 3: Session Expiration**
```gherkin
Given: A user with 24-hour session approaching expiry
When: Session expires while user is active
Then: Next request redirects to landing
And: Friendly "session ended" message is shown
And: All shadow table data for session is cleaned up
```

**Scenario 4: Concept Selection**
```gherkin
Given: User has validated magic link
When: User selects "Plato's Closet" concept
Then: Session is bound to PC concept
And: User sees PC-branded demo experience
And: Data comes from kiosk_demo_pc database
```

### Test Coverage Requirements

- **Business Logic**: Shadow table merge (100% coverage), session validation (100%)
- **User Interface**: Landing page, concept selection, tour overlay (E2E tests)
- **Integration Points**: Email sending (mock in unit tests, verify in integration)
- **Edge Cases**: Expired tokens, concurrent sessions, database failures
- **Security**: Token hashing, session isolation, rate limiting

---

## Glossary

### Domain Terms

| Term | Definition | Context |
|------|------------|---------|
| Concept | Store type: Plato's Closet (PC), Once Upon A Child (OU), Style Encore (SE) | User selects concept to see relevant demo |
| Magic Link | One-time URL with embedded token for passwordless authentication | Sent via email after user enters address |
| Shadow Table | Session-scoped table storing user's modifications to demo data | Overlays baseline simulation data |
| Baseline | Demo data generated by MockDataGenerator simulation | Continuously updated, shared by all users |

### Technical Terms

| Term | Definition | Context |
|------|------------|---------|
| typeNum | Store identifier pattern `[a-z]{2}\d+` (e.g., `ou00`) | Demo should use valid typeNums like `pc00`, `ou00`, `se00` (mapped to demo dbNames) |
| Session Token | 64-char hex string identifying active demo session | Stored hashed in database, plaintext in cookie |
| Shadow Merge | Process of combining baseline data with session shadow operations | Applied on demo reads (explicitly scoped to demo views) |

### API Terms

| Term | Definition | Context |
|------|------------|---------|
| X-Demo-Session | HTTP header containing demo session token | Optional alternative to cookie auth for protected demo API endpoints |
| source_qr_id | Identifier for QR code that initiated demo access | Used for attribution analytics |
