# 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** (via PRD Q&A)
- [x] Component names consistent across diagrams
- [x] A developer could implement from this design

---

## Constraints

**CON-1 Technical Platform**
- PHP 8.x with Slim 2.6.2 framework
- MySQL multi-store architecture (kiosk_{typeNum} per store)
- Twig 1.44.8 templating
- PSR-4 autoloading via Composer
- Bootstrap 3 + FontAwesome for UI

**CON-2 Integration Requirements**
- Ably real-time messaging for POS sync (existing `WorkbookAbly` pattern)
- Twilio/Vonage SMS via existing `TextMessageService`
- Redis for background job queuing (existing `SmsWorker` pattern)
- Must support both session-based auth (workspace) and API key auth (POS)

**CON-3 Database Architecture**
- Coupon data MUST be at store level (`kiosk_{typeNum}` databases)
- Event configuration stored per-store (not central)
- JSON migration format with `"database": "{{store}}"` for multi-store deployment
- Foreign keys with CASCADE delete where appropriate

**CON-4 Performance Targets**
- Coupon validation API: <500ms response time (95th percentile)
- Settings sync via Ably: <2 second propagation to POS
- Redemption interface: <1 second validation display

**CON-5 Security Requirements**
- CSRF protection for workspace endpoints
- API key authentication for POS endpoints
- Audit logging for all coupon redemptions (employee ID, timestamp)
- No PII exposure in Ably event payloads (phone numbers excluded from broadcast)

## Implementation Context

**IMPORTANT**: You MUST read and analyze ALL listed context sources to understand constraints, patterns, and existing architecture.

### Required Context Sources

- ICO-1 Loyalty System Patterns (CRITICAL reference)
```yaml
- file: userfrosting/src/BuyerKiosk/Core/Loyalty/LoyaltyCoupon.php
  relevance: HIGH
  why: "UUID generation, coupon CRUD patterns, validation logic"

- file: userfrosting/src/BuyerKiosk/Core/Loyalty/LoyaltyTrigger.php
  relevance: HIGH
  why: "Event/trigger lifecycle, enum patterns, date handling"

- file: userfrosting/src/BuyerKiosk/Core/Loyalty/LoyaltyGroup.php
  relevance: MEDIUM
  why: "Base class inheritance pattern, store group context"

- file: userfrosting/routes/groups/loyalty.php
  relevance: HIGH
  why: "API route patterns, request/response formats"
```

- ICO-2 Ably Real-Time Integration
```yaml
- file: userfrosting/src/BuyerKiosk/Workbook/WorkbookAbly.php
  relevance: HIGH
  why: "Event publishing pattern, error handling, channel naming"

- file: public_html/js/workspace/modules/workbook/ably-sync.js
  relevance: MEDIUM
  why: "Frontend event subscription, deduplication, callback routing"
```

- ICO-3 Database Migration System
```yaml
- file: userfrosting/migrations/input/20251202_003_backstock_events_tables.json
  relevance: HIGH
  why: "Event lifecycle table pattern (phases, dates, status)"

- file: userfrosting/migrations/methods/storeMigration.php
  relevance: MEDIUM
  why: "Multi-store migration execution logic"
```

- ICO-4 Workspace UI Patterns
```yaml
- file: userfrosting/templates/themes/default/workspace/workspace.html
  relevance: HIGH
  why: "SPA view container structure"

- file: userfrosting/templates/themes/default/workspace/partials/sidebar-nav.html
  relevance: HIGH
  why: "Navigation integration pattern"

- file: userfrosting/templates/themes/default/workbook/modals/add-note-modal.html
  relevance: MEDIUM
  why: "Modal form pattern for quick actions"

- file: userfrosting/src/BuyerKiosk/Workbook/Controllers/TasksApiController.php
  relevance: HIGH
  why: "API controller pattern with store context"
```

- ICO-5 SMS Infrastructure
```yaml
- file: userfrosting/src/BuyerKiosk/SMS/TextMessageService/TextMessageService.php
  relevance: MEDIUM
  why: "SMS delivery pattern for reminders"
```

### Implementation Boundaries

- **Must Preserve**:
  - Existing `LoyaltyCoupon` and `LoyaltyTrigger` systems (separate from Comeback Cash)
  - Existing `WorkbookAbly` class structure (extend, don't modify core)
  - Store database isolation pattern (`kiosk_{typeNum}`)
  - Existing SMS queue infrastructure

- **Can Modify**:
  - `WorkbookAbly.php` - Add new coupon event publishing methods
  - `workspace.html` - Add new SPA view container
  - `sidebar-nav.html` - Add navigation item
  - Route files - Add new route groups

- **Must Not Touch**:
  - `LoyaltyCoupon.php` / `LoyaltyTrigger.php` - These are separate systems
  - `kiosk_users` central database schema - Comeback Cash is store-level only
  - POS system code - API contract only, implementation is external

### External Interfaces

#### System Context Diagram

```mermaid
graph TB
    subgraph "External Actors"
        Employee[Store Employee]
        POS[POS System]
        Customer[Customer via SMS]
    end

    subgraph "Comeback Cash System"
        WorkspaceUI[Workspace UI]
        CashAPI[Comeback Cash API]
        AblyPub[Ably Publisher]
        SMSQueue[SMS Queue]
    end

    subgraph "External Services"
        Ably[Ably Real-time]
        SMS[Twilio/Vonage]
    end

    subgraph "Data Storage"
        StoreDB[(Store DB kiosk_{typeNum})]
    end

    Employee --> WorkspaceUI
    WorkspaceUI --> CashAPI

    POS -->|POST coupon/GET settings| CashAPI
    CashAPI --> StoreDB
    CashAPI --> AblyPub
    AblyPub --> Ably
    Ably -->|Settings sync| POS

    CashAPI --> SMSQueue
    SMSQueue --> SMS
    SMS --> Customer
```

#### Interface Specifications

```yaml
# Inbound Interfaces (what calls this system)
inbound:
  - name: "Store Employee Workspace"
    type: HTTPS
    format: HTML + REST API
    authentication: Session (cookie-based)
    data_flow: "Event management, coupon redemption, reporting"
    permissions: "uri_comeback_cash (new permission)"

  - name: "POS System API"
    type: HTTPS
    format: REST JSON
    authentication: API Key header (X-API-Key)
    data_flow: "Coupon issuance, settings retrieval, redemption posting"
    rate_limit: "100 req/min per store"

  - name: "Admin Configuration"
    type: HTTPS
    format: HTML + REST API
    authentication: Session (cookie-based)
    data_flow: "System-wide settings, store enablement"
    permissions: "uri_store_settings"

# Outbound Interfaces (what this system calls)
outbound:
  - name: "Ably Real-time Messaging"
    type: WebSocket (via Ably SDK)
    format: JSON
    authentication: Ably API Key (existing)
    channel: "{typeNum}" (store-specific)
    data_flow: "Settings sync broadcasts to POS"
    criticality: HIGH
    event_types:
      - "comeback_cash.settings_updated"
      - "comeback_cash.event_started"
      - "comeback_cash.event_ended"

  - name: "SMS Service (Twilio/Vonage)"
    type: HTTPS
    format: REST
    authentication: Account credentials (existing)
    data_flow: "Coupon notification, expiration reminders"
    criticality: MEDIUM
    via: "Existing TextMessageService queue"

# Data Interfaces
data:
  - name: "Store Database"
    type: MySQL
    connection: PDO via dbConnectByName($store->getDbName())
    data_flow: "Event config, coupon records, redemption log"
    tables:
      - ccEvents (event configuration)
      - ccCoupons (issued coupons)
      - ccRedemptions (redemption audit log)

  - name: "Redis (SMS Queue)"
    type: Redis
    connection: Existing SmsWorker infrastructure
    data_flow: "Async SMS job processing"
```

### Cross-Component Boundaries

- **API Contracts**:
  - POS API endpoints (`/api/{typeNum}/comeback-cash/*`) - Must maintain backward compatibility
  - Ably event schema - Versioned events to support POS upgrade cycles

- **Shared Resources**:
  - Store database (`kiosk_{typeNum}`) - New tables only, no modifications to existing
  - Ably channel (`{typeNum}`) - Shared with existing workspace events
  - SMS queue infrastructure - Uses existing worker pool

- **Breaking Change Policy**:
  - POS API: Deprecate for 90 days before removal; version in URL if breaking
  - Ably events: Include `version` field in payload; support N-1 versions

### Project Commands

```bash
# Component: BuyerKiosk Web Application
Location: /Users/rvanvuren/Projects/buyerkiosk-web

## Environment Setup
Install Dependencies: cd userfrosting && composer install
Environment Variables: Configuration in userfrosting/config/config.php

# Testing Commands
Unit Tests: ./test.sh
# Note: No dedicated integration or E2E test runner discovered

# Deployment
Deploy: ./deploy.sh  # Runs tests then deploys

# Database Operations
Run Migrations: php userfrosting/conductor migrate
Create Migration: Create JSON file in userfrosting/migrations/input/

# Development Server
Apache via MAMP/local environment (dev2.buyerkiosk.com)

# Comeback Cash Specific Verification
After implementation, verify:
1. Database tables created: SELECT * FROM ccEvents, ccCoupons, ccRedemptions;
2. API endpoints accessible: curl /api/{typeNum}/comeback-cash/settings
3. Workspace UI loads: Navigate to /{typeNum}/workspace/comeback-cash
4. Ably events fire: Monitor Chrome DevTools Network tab for WebSocket messages
```

## Solution Strategy

- **Architecture Pattern**: Modular feature addition following existing BuyerKiosk patterns
  - New namespace `BuyerKiosk\ComebackCash\` under `userfrosting/src/BuyerKiosk/ComebackCash/`
  - MVC structure: Models (entities), Controllers (API + Page), Services (business logic)
  - Follows existing patterns from `Loyalty/`, `Workbook/`, and `Backstock/` modules

- **Integration Approach**:
  - Database: Store-level tables via JSON migration (same as bsEvents pattern)
  - Real-time: Extend `WorkbookAbly` with new event types for settings sync
  - UI: New SPA view in workspace with dedicated navigation item
  - API: New route group `/api/{typeNum}/comeback-cash/` for POS integration
  - SMS: Queue jobs via existing `TextMessageService` infrastructure

- **Justification**:
  - Modular approach allows independent development without touching core systems
  - Follows proven patterns already working in production (Backstock events, Loyalty coupons)
  - Store-level isolation matches existing multi-tenant architecture
  - Ably integration provides sub-second sync without polling overhead

- **Key Decisions**:
  1. **Separate from Loyalty system** - Comeback Cash is distinct from existing LoyaltyCoupon; no code sharing to avoid coupling
  2. **Bearer instrument model** - Simplifies redemption (no identity verification at POS)
  3. **POS-driven issuance** - All coupons originate from POS transactions; no manual creation
  4. **Pre-tax thresholds** - Consistent calculation basis across buy-side and sales-side

## Building Block View

### Components

```mermaid
graph TB
    subgraph "Workspace UI"
        PageCtrl[ComebackCashPageController]
        Templates[Twig Templates]
        JS[JavaScript Modules]
    end

    subgraph "API Layer"
        PosAPI[ComebackCashPosApiController]
        WorkspaceAPI[ComebackCashApiController]
    end

    subgraph "Business Logic"
        EventSvc[EventService]
        CouponSvc[CouponService]
        RedemptionSvc[RedemptionService]
        AblySvc[ComebackCashAbly]
    end

    subgraph "Data Layer"
        EventModel[Event Model]
        CouponModel[Coupon Model]
        RedemptionModel[Redemption Model]
    end

    subgraph "External"
        Ably[Ably Channel]
        SMS[SMS Queue]
        DB[(Store DB)]
    end

    PageCtrl --> Templates
    Templates --> JS
    JS --> WorkspaceAPI

    PosAPI --> CouponSvc
    PosAPI --> EventSvc
    WorkspaceAPI --> RedemptionSvc
    WorkspaceAPI --> EventSvc

    EventSvc --> EventModel
    EventSvc --> AblySvc
    CouponSvc --> CouponModel
    CouponSvc --> SMS
    RedemptionSvc --> RedemptionModel

    EventModel --> DB
    CouponModel --> DB
    RedemptionModel --> DB
    AblySvc --> Ably
```

### Directory Map

**New Files (Backend)**
```
userfrosting/src/BuyerKiosk/ComebackCash/           # NEW: Feature namespace
├── Controllers/
│   ├── ComebackCashPageController.php              # NEW: Workspace page controller
│   ├── ComebackCashApiController.php               # NEW: Workspace API (redemption, events)
│   └── ComebackCashPosApiController.php            # NEW: POS API (coupon issuance, settings)
├── Models/
│   ├── Event.php                                   # NEW: Event entity (buy-side/sales-side config)
│   ├── Coupon.php                                  # NEW: Coupon entity
│   └── Redemption.php                              # NEW: Redemption audit entity
├── Services/
│   ├── EventService.php                            # NEW: Event CRUD, lifecycle management
│   ├── CouponService.php                           # NEW: Coupon issuance, validation
│   ├── RedemptionService.php                       # NEW: Redemption processing
│   └── ComebackCashAbly.php                        # NEW: Ably event publishing
└── ComebackCashFactory.php                         # NEW: Service factory/DI container

userfrosting/routes/
├── comeback-cash/
│   ├── pages.php                                   # NEW: Workspace page routes
│   └── api.php                                     # NEW: Workspace API routes
└── api.php                                         # MODIFY: Add POS API route group

userfrosting/migrations/input/
└── 20251205_001_comeback_cash_tables.json          # NEW: Database migration
```

**New Files (Frontend)**
```
userfrosting/templates/themes/default/
├── workspace/partials/comeback-cash/
│   ├── main.html                                   # NEW: Main SPA view container
│   ├── events-list.html                            # NEW: Event management list
│   ├── event-form.html                             # NEW: Event create/edit form
│   ├── redemption-panel.html                       # NEW: Coupon redemption interface
│   └── reports.html                                # NEW: Coupon reporting view
├── workspace/partials/sidebar-nav.html             # MODIFY: Add nav item
└── modals/
    └── comeback-cash-redeem-modal.html             # NEW: Redemption confirmation modal

public_html/
├── js/workspace/modules/comeback-cash/
│   ├── comeback-cash.js                            # NEW: Main module init
│   ├── events-controller.js                        # NEW: Event management
│   ├── redemption-controller.js                    # NEW: Redemption flow
│   └── ably-sync.js                                # NEW: Real-time settings sync
└── css/workspace/
    └── comeback-cash.css                           # NEW: Feature styles
```

**Modified Files**
```
userfrosting/src/BuyerKiosk/Workbook/WorkbookAbly.php  # MODIFY: Add event type constants
userfrosting/templates/themes/default/workspace/workspace.html  # MODIFY: Include new view
```

### Interface Specifications

#### Interface Documentation References

```yaml
# Existing patterns to follow
interfaces:
  - name: "Workspace API Pattern"
    doc: @userfrosting/src/BuyerKiosk/Workbook/Controllers/TasksApiController.php
    relevance: HIGH
    why: "API controller pattern with store context and permission checks"

  - name: "Ably Event Pattern"
    doc: @userfrosting/src/BuyerKiosk/Workbook/WorkbookAbly.php
    relevance: HIGH
    why: "Real-time event publishing structure and error handling"

  - name: "POS API Pattern"
    doc: @userfrosting/routes/api.php
    relevance: HIGH
    why: "External API authentication and response format"

  - name: "SMS Queue Pattern"
    doc: @userfrosting/src/BuyerKiosk/SMS/TextMessageService/TextMessageService.php
    relevance: MEDIUM
    why: "Async message queuing for notifications"
```

#### Data Storage Changes

**Database**: `kiosk_{typeNum}` (store-level)

```sql
-- Table: ccEvents (Comeback Cash Events)
-- Stores event configuration for both buy-side and sales-side programs
-- NOTE: Buy-side and sales-side have different earning models:
--   - Sales-side: Tiered/percentage earning (spend $X, earn $Y)
--   - Buy-side: Flat earning only (any transaction = flat coupon value, no thresholds)
CREATE TABLE `ccEvents` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(100) NOT NULL COMMENT 'Display name for event',
  `side` ENUM('buy', 'sales') NOT NULL COMMENT 'buy = customer selling TO store, sales = customer buying FROM store',
  `status` ENUM('draft', 'scheduled', 'active', 'ended', 'cancelled') NOT NULL DEFAULT 'draft',

  -- Timing
  `start_date` DATETIME NULL COMMENT 'When event becomes active (NULL = manual start)',
  `end_date` DATETIME NULL COMMENT 'When event ends (NULL = manual end)',

  -- Earning Rules
  -- For side='buy': ALWAYS use earning_type='flat' with earning_flat_amount (no thresholds)
  -- For side='sales': Can use 'tiered', 'flat', or 'percentage'
  `earning_type` ENUM('tiered', 'flat', 'percentage') NOT NULL DEFAULT 'flat',
  `earning_tiers` JSON NULL COMMENT 'Sales-side only: [{"min": 50, "max": 99.99, "reward": 10}]',
  `earning_flat_amount` DECIMAL(10,2) NULL COMMENT 'Flat reward amount - REQUIRED for buy-side, optional for sales-side',
  `earning_percentage` DECIMAL(5,2) NULL COMMENT 'Sales-side only: Percentage of transaction',

  -- Redemption Rules
  `redemption_min_purchase` DECIMAL(10,2) NULL COMMENT 'Minimum purchase to redeem (pre-tax)',
  `redemption_start_date` DATETIME NULL COMMENT 'When redemption period starts',
  `redemption_end_date` DATETIME NULL COMMENT 'When redemption period ends',
  `redemption_days_valid` INT UNSIGNED NULL COMMENT 'Days coupon is valid from issue date',

  -- Options
  `allow_double_up` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Sales-side only: Allow earning new coupon while redeeming',
  `refund_policy` ENUM('forfeit', 'reinstate') NOT NULL DEFAULT 'forfeit',
  `sms_enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Send SMS notifications',

  -- Audit
  `created_by` INT UNSIGNED NOT NULL,
  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

  PRIMARY KEY (`id`),
  INDEX `idx_side_status` (`side`, `status`),
  INDEX `idx_dates` (`start_date`, `end_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Table: ccCoupons (Issued Coupons)
CREATE TABLE `ccCoupons` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `event_id` INT UNSIGNED NOT NULL,
  `code` VARCHAR(12) NOT NULL COMMENT '8-char unique code (e.g., A1B2C3D4)',

  -- Value
  `value` DECIMAL(10,2) NOT NULL COMMENT 'Coupon face value',
  `original_value` DECIMAL(10,2) NOT NULL COMMENT 'Original value (for partial redemption tracking)',

  -- Source Transaction
  `source_transaction_id` VARCHAR(50) NOT NULL COMMENT 'POS transaction ID that generated this coupon',
  `source_transaction_amount` DECIMAL(10,2) NOT NULL COMMENT 'Transaction amount that qualified',

  -- Customer (optional - for SMS)
  `customer_phone` VARCHAR(20) NULL COMMENT 'Phone for SMS notifications',
  `customer_name` VARCHAR(100) NULL COMMENT 'Customer name (if provided)',

  -- Status
  `status` ENUM('active', 'redeemed', 'expired', 'voided') NOT NULL DEFAULT 'active',
  `expires_at` DATETIME NOT NULL,

  -- Audit
  `issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `issued_by_employee_id` INT UNSIGNED NULL COMMENT 'NULL if issued by POS automatically',

  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_code` (`code`),
  INDEX `idx_event` (`event_id`),
  INDEX `idx_status_expires` (`status`, `expires_at`),
  INDEX `idx_phone` (`customer_phone`),
  CONSTRAINT `fk_coupon_event` FOREIGN KEY (`event_id`) REFERENCES `ccEvents`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Table: ccRedemptions (Redemption Audit Log)
CREATE TABLE `ccRedemptions` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `coupon_id` INT UNSIGNED NOT NULL,

  -- Redemption Details
  `redeemed_amount` DECIMAL(10,2) NOT NULL COMMENT 'Amount applied to transaction',
  `transaction_id` VARCHAR(50) NOT NULL COMMENT 'POS transaction ID where redeemed',
  `transaction_amount` DECIMAL(10,2) NOT NULL COMMENT 'Transaction subtotal (pre-tax)',

  -- Audit
  `redeemed_by_employee_id` INT UNSIGNED NOT NULL,
  `redeemed_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `redemption_method` ENUM('scan', 'manual', 'pos') NOT NULL DEFAULT 'scan',

  PRIMARY KEY (`id`),
  INDEX `idx_coupon` (`coupon_id`),
  INDEX `idx_transaction` (`transaction_id`),
  INDEX `idx_employee_date` (`redeemed_by_employee_id`, `redeemed_at`),
  CONSTRAINT `fk_redemption_coupon` FOREIGN KEY (`coupon_id`) REFERENCES `ccCoupons`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

**Migration File**: `userfrosting/migrations/input/20251205_001_comeback_cash_tables.json`

#### Internal API Changes

**POS API Endpoints** (External - API Key Auth)

```yaml
# GET Current Settings
Endpoint: Get Active Event Settings
  Method: GET
  Path: /api/{typeNum}/comeback-cash/settings
  Auth: X-API-Key header
  Response:
    success:
      buy_side:
        active: boolean
        event_id: int|null
        event_name: string|null
        earning_type: "tiered"|"flat"|"percentage"
        earning_tiers: array|null
        earning_flat_amount: decimal|null
        earning_percentage: decimal|null
        redemption_min_purchase: decimal|null
        allow_double_up: boolean
      sales_side:
        active: boolean
        event_id: int|null
        # ... same structure as buy_side
      version: string  # For cache invalidation
    error:
      success: false
      error: string
      code: string

# POST Issue Coupon
Endpoint: Issue New Coupon
  Method: POST
  Path: /api/{typeNum}/comeback-cash/coupons
  Auth: X-API-Key header
  Request:
    side: "buy"|"sales" (required)
    transaction_id: string (required, max 50 chars)
    transaction_amount: decimal (required, pre-tax)
    customer_phone: string (optional, E.164 format)
    customer_name: string (optional, max 100 chars)
  Response:
    success:
      success: true
      coupon:
        code: string (8 chars)
        value: decimal
        expires_at: ISO8601 datetime
        event_name: string
    no_qualification:
      success: true
      coupon: null
      reason: "Amount below minimum threshold"
    error:
      success: false
      error: string
      code: "NO_ACTIVE_EVENT"|"INVALID_SIDE"|"DUPLICATE_TRANSACTION"

# POST Redemption (from POS)
Endpoint: Redeem Coupon via POS
  Method: POST
  Path: /api/{typeNum}/comeback-cash/redeem
  Auth: X-API-Key header
  Request:
    code: string (required)
    transaction_id: string (required)
    transaction_amount: decimal (required, pre-tax)
    amount_to_redeem: decimal (optional, defaults to full value)
  Response:
    success:
      success: true
      redeemed_amount: decimal
      remaining_value: decimal
      coupon_status: "redeemed"|"active" (active if partial)
    error:
      success: false
      error: string
      code: "INVALID_CODE"|"ALREADY_REDEEMED"|"EXPIRED"|"MIN_PURCHASE_NOT_MET"

# GET Validate Coupon
Endpoint: Validate Coupon Code
  Method: GET
  Path: /api/{typeNum}/comeback-cash/coupons/{code}
  Auth: X-API-Key header
  Response:
    success:
      valid: true
      coupon:
        code: string
        value: decimal
        status: "active"|"redeemed"|"expired"|"voided"
        expires_at: ISO8601
        event_name: string
        redemption_min_purchase: decimal|null
    not_found:
      valid: false
      error: "Coupon not found"
```

**Workspace API Endpoints** (Internal - Session Auth)

```yaml
# Event Management
Endpoint: List Events
  Method: GET
  Path: /{typeNum}/api/comeback-cash/events
  Auth: Session + uri_comeback_cash permission
  Query: ?side=buy|sales&status=active,scheduled
  Response: { events: Event[], total: int }

Endpoint: Create Event
  Method: POST
  Path: /{typeNum}/api/comeback-cash/events
  Request: Event object (see data model)
  Response: { success: true, event: Event }

Endpoint: Update Event
  Method: PUT
  Path: /{typeNum}/api/comeback-cash/events/{id}
  Request: Partial Event object
  Response: { success: true, event: Event }
  Side Effect: Publishes Ably event if status changes

Endpoint: Delete Event
  Method: DELETE
  Path: /{typeNum}/api/comeback-cash/events/{id}
  Constraint: Only draft events can be deleted
  Response: { success: true }

# Coupon Operations (Workspace)
Endpoint: Lookup Coupon for Redemption
  Method: GET
  Path: /{typeNum}/api/comeback-cash/lookup?code={code}
  Response: { coupon: Coupon, redeemable: boolean, reason: string|null }

Endpoint: Redeem Coupon (Workspace)
  Method: POST
  Path: /{typeNum}/api/comeback-cash/redeem
  Request: { code: string, transaction_id: string, transaction_amount: decimal }
  Response: { success: true, redemption: Redemption }

# Reporting
Endpoint: Get Event Report
  Method: GET
  Path: /{typeNum}/api/comeback-cash/events/{id}/report
  Response: { issued_count: int, redeemed_count: int, total_value_issued: decimal, total_value_redeemed: decimal, ... }
```

#### Application Data Models

```php
namespace BuyerKiosk\ComebackCash\Models;

/**
 * Event - Comeback Cash promotion event configuration
 */
class Event
{
    // Properties (map to ccEvents table)
    public int $id;
    public string $name;
    public string $side;           // 'buy' | 'sales'
    public string $status;         // 'draft' | 'scheduled' | 'active' | 'ended' | 'cancelled'
    public ?DateTime $startDate;
    public ?DateTime $endDate;
    public string $earningType;    // 'tiered' | 'flat' | 'percentage'
    public ?array $earningTiers;   // JSON decoded
    public ?float $earningFlatAmount;
    public ?float $earningPercentage;
    public ?float $redemptionMinPurchase;
    public ?DateTime $redemptionStartDate;
    public ?DateTime $redemptionEndDate;
    public ?int $redemptionDaysValid;
    public bool $allowDoubleUp;
    public string $refundPolicy;   // 'forfeit' | 'reinstate'
    public bool $smsEnabled;

    // Behaviors
    public function isActive(): bool;
    public function isInRedemptionPeriod(): bool;
    public function calculateReward(float $transactionAmount): ?float;
    public function toSettingsArray(): array;  // For POS API response
}

/**
 * Coupon - Issued comeback cash coupon
 */
class Coupon
{
    public int $id;
    public int $eventId;
    public string $code;           // 8-char unique
    public float $value;
    public float $originalValue;
    public string $sourceTransactionId;
    public float $sourceTransactionAmount;
    public ?string $customerPhone;
    public ?string $customerName;
    public string $status;         // 'active' | 'redeemed' | 'expired' | 'voided'
    public DateTime $expiresAt;
    public DateTime $issuedAt;
    public ?int $issuedByEmployeeId;

    // Behaviors
    public function isRedeemable(): bool;
    public function canRedeemForAmount(float $transactionAmount, float $minPurchase): bool;
    public function getDisplayCode(): string;  // Formatted for printing/display
    public function markRedeemed(float $amount): void;
    public function markExpired(): void;
    public function reinstate(): void;  // For refund policy = 'reinstate'

    // Static
    public static function generateCode(): string;  // 8-char alphanumeric
}

/**
 * Redemption - Audit log entry for coupon redemption
 */
class Redemption
{
    public int $id;
    public int $couponId;
    public float $redeemedAmount;
    public string $transactionId;
    public float $transactionAmount;
    public int $redeemedByEmployeeId;
    public DateTime $redeemedAt;
    public string $redemptionMethod;  // 'scan' | 'manual' | 'pos'

    // Relationships
    public function getCoupon(): Coupon;
}
```

#### Integration Points

```yaml
# External System Integration
POS_System:
  - protocol: REST API over HTTPS
  - auth: API Key (X-API-Key header)
  - direction: Bidirectional
  - data_flow:
    - POS → Server: Coupon issuance requests, redemption requests
    - Server → POS: Settings sync via Ably, coupon validation responses
  - critical_data: [transaction_id, transaction_amount, coupon_code, customer_phone]

Ably_Real_Time:
  - protocol: WebSocket via Ably SDK
  - auth: Existing Ably API key
  - channel: "{typeNum}" (shared with workspace)
  - events_published:
    - "comeback_cash.settings_updated": When event config changes
    - "comeback_cash.event_started": When event goes active
    - "comeback_cash.event_ended": When event ends or is cancelled
  - payload_format:
    action: string
    version: int
    buy_side: object|null
    sales_side: object|null
    timestamp: int

SMS_Service:
  - protocol: Existing TextMessageService queue
  - trigger: Coupon issuance (if phone provided and sms_enabled)
  - message_types:
    - Coupon issued notification
    - Expiration reminder (24 hours before)
  - template_variables: [coupon_code, value, expires_at, store_name]
```

### Implementation Examples

#### Example: Reward Calculation (Buy-Side vs Sales-Side)

**Why this example**: The earning calculation differs significantly between buy-side and sales-side and must be handled correctly.

```php
/**
 * Calculate reward amount based on transaction and event configuration.
 *
 * IMPORTANT DISTINCTION:
 * - Buy-side: ANY completed transaction earns the flat amount (no thresholds)
 * - Sales-side: Must meet threshold for tiered/percentage, or any amount for flat
 *
 * Returns null if transaction doesn't qualify (sales-side only).
 */
public function calculateReward(float $transactionAmount): ?float
{
    // BUY-SIDE: Always earns flat amount, no thresholds
    // Any completed buy transaction qualifies regardless of payout amount
    if ($this->side === 'buy') {
        return $this->earningFlatAmount;
    }

    // SALES-SIDE: Apply threshold-based earning rules
    // Must meet minimum threshold for any reward
    if ($transactionAmount < $this->getMinimumThreshold()) {
        return null;
    }

    switch ($this->earningType) {
        case 'flat':
            return $this->earningFlatAmount;

        case 'percentage':
            return round($transactionAmount * ($this->earningPercentage / 100), 2);

        case 'tiered':
            // Tiers are sorted ascending by min value
            // Find highest qualifying tier
            $reward = null;
            foreach ($this->earningTiers as $tier) {
                if ($transactionAmount >= $tier['min']) {
                    if ($tier['max'] === null || $transactionAmount <= $tier['max']) {
                        $reward = $tier['reward'];
                    }
                }
            }
            return $reward;

        default:
            return null;
    }
}

/**
 * Get minimum transaction amount required to earn a coupon.
 * For buy-side events, returns 0 (any transaction qualifies).
 */
private function getMinimumThreshold(): float
{
    // Buy-side: No minimum threshold - any completed transaction qualifies
    if ($this->side === 'buy') {
        return 0;
    }

    // Sales-side: Depends on earning type
    if ($this->earningType === 'tiered' && !empty($this->earningTiers)) {
        return $this->earningTiers[0]['min'];  // First tier minimum
    }
    return 0.01;  // Any positive transaction qualifies for flat/percentage
}
```

#### Example: Coupon Code Generation

**Why this example**: Code generation must be collision-resistant and use cryptographically secure random bytes.

```php
/**
 * Generate unique 8-character alphanumeric coupon code.
 * Uses same pattern as existing LoyaltyCoupon for consistency.
 */
public static function generateCode(): string
{
    // Exclude ambiguous characters (0, O, I, l, 1)
    $chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
    $charsLength = strlen($chars);

    $code = '';
    $bytes = openssl_random_pseudo_bytes(8);

    for ($i = 0; $i < 8; $i++) {
        $code .= $chars[ord($bytes[$i]) % $charsLength];
    }

    return $code;
}
```

#### Example: Ably Settings Broadcast

**Why this example**: Shows the event payload structure that POS systems will receive.

```php
/**
 * Broadcast settings update to all connected POS systems.
 * Called when event status changes or configuration is modified.
 */
public function broadcastSettingsUpdate(): void
{
    $payload = [
        'action' => 'comeback_cash.settings_updated',
        'version' => time(),  // Use timestamp as version for cache invalidation
        'buy_side' => $this->getActiveEventSettings('buy'),
        'sales_side' => $this->getActiveEventSettings('sales'),
        'timestamp' => time(),
        'source' => 'workspace'
    ];

    try {
        $channel = $this->ably->channel($this->typeNum);
        $channel->publish('comeback_cash.settings_updated', $payload);
    } catch (\Exception $e) {
        error_log("ComebackCashAbly: Failed to broadcast settings - " . $e->getMessage());
        // Don't throw - settings API provides fallback for POS
    }
}

private function getActiveEventSettings(string $side): ?array
{
    $event = $this->eventService->getActiveEvent($side);
    return $event ? $event->toSettingsArray() : null;
}
```

## Runtime View

### Primary Flow: Coupon Issuance (POS)

1. Customer completes transaction at POS
2. POS sends transaction details to Comeback Cash API
3. System checks for active event on matching side (buy/sales)
4. System calculates reward based on event configuration
5. If qualified, system generates coupon and returns details
6. If customer phone provided, SMS notification is queued

```mermaid
sequenceDiagram
    actor Customer
    participant POS
    participant API as ComebackCashPosApiController
    participant CouponSvc as CouponService
    participant EventSvc as EventService
    participant DB as Store Database
    participant SMS as SMS Queue

    Customer->>POS: Complete transaction ($75)
    POS->>API: POST /api/{typeNum}/comeback-cash/coupons
    Note right of POS: {side: "sales", transaction_id: "T123",<br/>transaction_amount: 75.00, phone: "+15551234567"}

    API->>EventSvc: getActiveEvent("sales")
    EventSvc->>DB: SELECT * FROM ccEvents WHERE side='sales' AND status='active'
    DB-->>EventSvc: Event (tiered: $50=$10, $100=$25)
    EventSvc-->>API: Event object

    API->>CouponSvc: issueCoupon(event, transactionData)
    CouponSvc->>CouponSvc: calculateReward(75.00) → $10
    CouponSvc->>CouponSvc: generateCode() → "A1B2C3D4"
    CouponSvc->>DB: INSERT INTO ccCoupons
    CouponSvc->>SMS: Queue notification (async)
    CouponSvc-->>API: Coupon object

    API-->>POS: {success: true, coupon: {code: "A1B2C3D4", value: 10.00, expires_at: ...}}
    POS->>Customer: Print receipt with coupon code
```

### Secondary Flow: Coupon Redemption (Workspace)

1. Employee scans barcode or enters coupon code
2. System looks up coupon and validates status
3. System displays coupon details and redemption requirements
4. Employee enters transaction details and confirms
5. System records redemption and updates coupon status

```mermaid
sequenceDiagram
    actor Employee
    participant UI as Workspace UI
    participant API as ComebackCashApiController
    participant RedemptionSvc as RedemptionService
    participant DB as Store Database

    Employee->>UI: Scan barcode "A1B2C3D4"
    UI->>API: GET /api/comeback-cash/lookup?code=A1B2C3D4
    API->>DB: SELECT * FROM ccCoupons WHERE code='A1B2C3D4'
    DB-->>API: Coupon (value: $10, status: active, min_purchase: $25)
    API-->>UI: {coupon: {...}, redeemable: true}

    UI->>Employee: Display coupon ($10 value, requires $25 min)
    Employee->>UI: Enter transaction ID + amount ($50)
    UI->>API: POST /api/comeback-cash/redeem
    Note right of UI: {code: "A1B2C3D4", transaction_id: "R456", transaction_amount: 50.00}

    API->>RedemptionSvc: redeemCoupon(code, transactionData, employeeId)
    RedemptionSvc->>DB: BEGIN TRANSACTION
    RedemptionSvc->>DB: UPDATE ccCoupons SET status='redeemed'
    RedemptionSvc->>DB: INSERT INTO ccRedemptions
    RedemptionSvc->>DB: COMMIT
    RedemptionSvc-->>API: Redemption object

    API-->>UI: {success: true, redeemed_amount: 10.00}
    UI->>Employee: Show success confirmation
```

### Secondary Flow: Settings Sync (Ably)

```mermaid
sequenceDiagram
    actor Manager
    participant UI as Workspace UI
    participant API as ComebackCashApiController
    participant EventSvc as EventService
    participant Ably as ComebackCashAbly
    participant POS as POS Systems

    Manager->>UI: Activate new event
    UI->>API: PUT /api/comeback-cash/events/5 {status: "active"}
    API->>EventSvc: updateEvent(5, {status: "active"})
    EventSvc->>EventSvc: Deactivate any conflicting event (same side)
    EventSvc->>API: Updated Event

    API->>Ably: broadcastSettingsUpdate()
    Ably->>POS: WebSocket: "comeback_cash.settings_updated"
    Note right of POS: POS caches new settings locally

    API-->>UI: {success: true, event: {...}}
    UI->>Manager: Show "Event activated" + "POS synced"
```

### Error Handling

**POS API Errors**

| Error Code | HTTP Status | Description | Client Action |
|------------|-------------|-------------|---------------|
| `NO_ACTIVE_EVENT` | 200 | No active event for requested side | Display "Comeback Cash not available" |
| `INVALID_SIDE` | 400 | Side must be "buy" or "sales" | Fix request |
| `DUPLICATE_TRANSACTION` | 409 | Transaction ID already used | Ignore (idempotent) |
| `INVALID_CODE` | 404 | Coupon code not found | Display "Invalid code" |
| `ALREADY_REDEEMED` | 409 | Coupon already fully redeemed | Display "Already used" |
| `EXPIRED` | 410 | Coupon has expired | Display "Coupon expired" |
| `MIN_PURCHASE_NOT_MET` | 422 | Transaction below minimum | Display minimum required |
| `UNAUTHORIZED` | 401 | Invalid or missing API key | Check configuration |

**Workspace Errors**

| Error Type | User Message | Recovery Action |
|------------|--------------|-----------------|
| Invalid coupon code | "Coupon not found. Check the code and try again." | Re-enter code |
| Coupon already used | "This coupon has already been redeemed." | None |
| Coupon expired | "This coupon expired on {date}." | None |
| Minimum not met | "Requires minimum purchase of ${amount}." | Adjust transaction |
| Network failure | "Connection error. Please try again." | Retry button |

**Ably Sync Failures**

- Ably publish failures are logged but do NOT block the operation
- POS systems should poll `/settings` endpoint on startup and periodically (every 5 minutes)
- Version field in response allows POS to detect stale cache

### Complex Logic: Event Lifecycle Management

```
ALGORITHM: Activate Event
INPUT: event_id, requested_status='active'
OUTPUT: updated_event, sync_result

1. VALIDATE:
   - Event exists and belongs to this store
   - Event is in valid transition state (draft→active, scheduled→active)
   - Required fields are complete (earning config, dates if scheduled)

2. CHECK CONFLICTS:
   - Find any existing active event on same side (buy/sales)
   - If found: Auto-end conflicting event (status='ended')

3. ACTIVATE:
   - Set status='active'
   - Set start_date=NOW() if not already set
   - Update updated_at timestamp

4. SYNC:
   - Broadcast settings via Ably
   - Log activation for audit

5. RESPOND:
   - Return updated event
   - Include sync status (success/failed)
```

```
ALGORITHM: Coupon Expiration Check (Cron Job)
INPUT: none (runs daily)
OUTPUT: count of expired coupons

1. QUERY: SELECT all coupons WHERE status='active' AND expires_at < NOW()

2. FOR EACH expired coupon:
   - UPDATE status='expired'
   - Log expiration

3. OPTIONAL (if enabled):
   - Send "coupon expired" SMS to customers who had phone numbers

4. RETURN: count of coupons expired
```

## Deployment View

### Deployment Sequence

1. **Database Migration** (First)
   - Run: `php userfrosting/conductor migrate`
   - Creates `ccEvents`, `ccCoupons`, `ccRedemptions` tables in all store databases
   - Migration is idempotent (safe to re-run)

2. **Code Deployment** (Second)
   - Standard deployment via `./deploy.sh`
   - No special configuration required
   - New routes and controllers auto-discovered

3. **Permission Setup** (Post-Deploy)
   - Add `uri_comeback_cash` permission to appropriate role groups
   - Typically assigned to Manager and Owner roles

4. **POS Integration** (External - Coordinated)
   - Provide API documentation to POS team
   - POS must implement:
     - Settings fetch on startup
     - Ably subscription for real-time updates
     - Coupon issuance on qualifying transactions
     - Coupon validation/redemption flow

### Configuration

```php
// No new environment variables required
// Uses existing Ably credentials from store configuration
// Uses existing SMS service credentials
```

### Rollout Strategy

- **Phase 1**: Deploy to dev/staging for integration testing with POS
- **Phase 2**: Pilot with 2-3 stores (feature flag at store level via ccEvents)
- **Phase 3**: Full rollout (create events for all stores)

### Rollback

- **Code Rollback**: Standard git revert + deploy
- **Database**: Tables can remain (empty tables cause no harm)
- **POS Fallback**: Settings API returns empty response if no active events

## Cross-Cutting Concepts

### Pattern Documentation

```yaml
# Existing patterns used
- pattern: PSR-4 Autoloading
  doc: @docs/patterns/psr4-autoloading.md
  relevance: CRITICAL
  why: "All new classes follow PSR-4 namespace conventions"

- pattern: Store Database Pattern
  doc: @userfrosting/models/BaseModel.php
  relevance: HIGH
  why: "Database connections via dbConnectByName($store->getDbName())"

- pattern: API Controller Pattern
  doc: @userfrosting/src/BuyerKiosk/Workbook/Controllers/TasksApiController.php
  relevance: HIGH
  why: "Permission checks, store context, JSON responses"

- pattern: Ably Event Pattern
  doc: @userfrosting/src/BuyerKiosk/Workbook/WorkbookAbly.php
  relevance: HIGH
  why: "Real-time event publishing structure"
```

### System-Wide Patterns

**Security**:
- Workspace: Session-based auth + CSRF token validation
- POS API: API key in `X-API-Key` header (validated against store config)
- No PII in Ably payloads (phone numbers never broadcast)

**Error Handling**:
- Controllers catch exceptions and return structured JSON
- Database errors logged via `error_log()`, user sees generic message
- Validation errors return specific field-level messages

**Logging/Auditing**:
- All redemptions logged with employee ID and timestamp
- Event status changes logged (who activated/ended)
- API errors logged with request context

### Implementation Patterns

#### Service Factory Pattern

```php
// ComebackCashFactory.php - Dependency injection for services
class ComebackCashFactory
{
    public static function createEventService(string $typeNum): EventService
    {
        $storeController = new StoreController($typeNum);
        $store = $storeController->getStore();
        $db = dbConnectByName($store->getDbName());
        $ably = new ComebackCashAbly($typeNum, $store->getAblyKey());

        return new EventService($db, $ably);
    }
}
```

#### API Controller Pattern

```php
// Standard pattern for workspace API controllers
class ComebackCashApiController
{
    public function getEvents($app, $typeNum)
    {
        // 1. Permission check
        if (!$app->user->checkAccess('uri_comeback_cash')) {
            $app->notAuthorized();
            return;
        }

        // 2. Store context
        $storeController = new StoreController($typeNum);
        if (!$app->user->checkStoreGroup($typeNum)) {
            $app->notAuthorized();
            return;
        }

        // 3. Business logic via service
        $service = ComebackCashFactory::createEventService($typeNum);
        $events = $service->listEvents($app->request->get('side'));

        // 4. JSON response
        echo json_encode(['success' => true, 'events' => $events]);
    }
}
```

#### Workspace SPA View Pattern

```html
<!-- data-view attribute for SPA navigation -->
<div id="comeback-cash-view" data-view="comeback-cash" class="workspace-view">
    {% include 'workspace/partials/comeback-cash/main.html' %}
</div>
```

```javascript
// JavaScript module initialization
WorkspaceApp.registerView('comeback-cash', {
    init: function() {
        ComebackCash.init();
    },
    destroy: function() {
        ComebackCash.cleanup();
    }
});
```

## Architecture Decisions

- [x] **ADR-1 Separate System from Existing Loyalty**: Create new `ComebackCash` namespace rather than extending existing `Loyalty` classes
  - Rationale: Comeback Cash has different business rules (bearer instrument, POS-driven, event-based) than existing loyalty system
  - Trade-offs: Some code duplication (UUID generation); cleaner separation of concerns
  - User confirmed: Yes (per PRD discussion)

- [x] **ADR-2 Bearer Instrument Model**: Coupons are redeemable by anyone with the code (no identity verification)
  - Rationale: Simpler POS integration, matches Kohl's Cash model, reduces friction at redemption
  - Trade-offs: Potential for coupon sharing (acceptable per business requirements)
  - User confirmed: Yes (per PRD Q&A)

- [x] **ADR-3 POS-Only Coupon Issuance**: No manual coupon creation in workspace; all coupons from valid transactions
  - Rationale: Prevents fraud, ensures audit trail, maintains system integrity
  - Trade-offs: Less flexibility for managers; can void individual coupons if needed
  - User confirmed: Yes (per PRD Q&A)

- [x] **ADR-4 Store-Level Database**: All Comeback Cash data in `kiosk_{typeNum}` databases (not central)
  - Rationale: Matches existing multi-store pattern, simplifies queries, enables store-specific events
  - Trade-offs: No cross-store coupon redemption (not a requirement)
  - User confirmed: Yes (per original requirements)

- [x] **ADR-5 Ably for Real-Time + Polling Fallback**: Primary sync via Ably, with `/settings` API as fallback
  - Rationale: Sub-second updates for active POS; polling ensures eventual consistency
  - Trade-offs: Slight complexity; necessary for reliability
  - User confirmed: Yes (per original requirements)

- [x] **ADR-6 Pre-Tax Thresholds**: All minimum purchase thresholds calculated on merchandise subtotal (pre-tax)
  - Rationale: Consistent behavior, simpler calculation, avoids tax jurisdiction complexity
  - Trade-offs: None significant
  - User confirmed: Yes (per PRD Q&A)

## Quality Requirements

**Performance**:
| Metric | Target | Measurement |
|--------|--------|-------------|
| POS API Response Time | <500ms (95th percentile) | Server-side timing logs |
| Coupon Lookup | <200ms | Time from scan to display |
| Ably Sync Latency | <2 seconds | Time from save to POS receipt |
| Database Query Performance | <100ms per query | Slow query log analysis |

**Usability**:
- Workspace redemption: 3 clicks or less from scan to confirmation
- Event creation: Complete form in under 2 minutes
- Clear error messages: Specific guidance on how to resolve
- Mobile-friendly: Redemption UI works on tablet-sized screens

**Security**:
- API key validation on every POS endpoint request
- Session authentication + store group validation for workspace
- Audit log retention: 2 years minimum
- No PII in Ably event payloads
- CSRF protection on all workspace POST/PUT/DELETE

**Reliability**:
- Database transactions: ACID compliance for redemptions
- Idempotent coupon issuance (duplicate transaction_id returns same coupon)
- Graceful degradation: Ably failure doesn't block operations
- Data integrity: Coupon value cannot exceed original_value

## Risks and Technical Debt

### Known Technical Issues

- **Ably Connection Reliability**: Existing `WorkbookAbly` occasionally fails silently on network issues
  - Mitigation: Always provide `/settings` API as polling fallback for POS
- **SMS Queue Delays**: High-volume periods may delay SMS notifications by several minutes
  - Mitigation: Coupon code printed on receipt; SMS is supplementary

### Technical Debt

- **Code Duplication**: UUID generation copied from `LoyaltyCoupon` rather than extracted to shared utility
  - Rationale: Avoid coupling; can refactor later if pattern is used elsewhere
- **No Shared Base Model**: Each model class has its own PDO handling
  - Mitigation: Follow existing patterns; consider base class in future

### Implementation Gotchas

- **JSON Column**: MySQL JSON columns require PHP 7.4+ json_decode for proper handling
  - Ensure `earning_tiers` is decoded before use in PHP
- **Timezone Handling**: All dates stored in UTC; frontend must convert for display
  - Use existing `DateHelper` patterns from workspace
- **Concurrent Event Activation**: Two users activating events simultaneously could create race condition
  - Mitigation: Use database transaction with SELECT FOR UPDATE on conflicting events
- **Coupon Code Collisions**: 8-char codes have ~2.8 trillion combinations; collision unlikely but possible
  - Mitigation: Retry code generation on unique constraint violation (max 3 attempts)

## Test Specifications

### Critical Test Scenarios

**Scenario 1: Coupon Issuance Happy Path**
```gherkin
Given: An active sales-side event with tiered earning ($50→$10, $100→$25)
And: No coupon exists for transaction "T001"
When: POS posts transaction {side: "sales", transaction_id: "T001", amount: 75.00}
Then: Response contains coupon with value $10
And: Coupon status is "active"
And: Coupon code is 8 alphanumeric characters
And: Expiration date matches event configuration
```

**Scenario 2: Coupon Redemption Happy Path**
```gherkin
Given: An active coupon "A1B2C3D4" with value $10 and min_purchase $25
And: Coupon status is "active"
When: Employee redeems coupon with transaction amount $50
Then: Redemption is recorded with employee ID
And: Coupon status changes to "redeemed"
And: Response shows redeemed_amount of $10
```

**Scenario 3: Sales-Side Below Threshold - No Coupon Issued**
```gherkin
Given: An active SALES-SIDE event with minimum threshold $50
When: POS posts transaction {side: "sales", amount: 35.00}
Then: Response has success: true, coupon: null
And: Reason indicates "Amount below minimum threshold"
And: No coupon record is created
```

**Scenario 3B: Buy-Side - Any Transaction Earns Coupon (No Threshold)**
```gherkin
Given: An active BUY-SIDE event with flat coupon value $10
When: POS posts transaction {side: "buy", transaction_id: "B001", amount: 5.00}
Then: Response contains coupon with value $10
And: Coupon is issued regardless of payout amount (could be $5 or $500)
Note: Buy-side has NO earning thresholds - any completed transaction qualifies
```

**Scenario 4: Expired Coupon Rejection**
```gherkin
Given: A coupon "EXPIRED1" with expires_at in the past
And: Coupon status is still "active" (not yet batch-updated)
When: Employee attempts to redeem coupon
Then: Response has success: false
And: Error code is "EXPIRED"
And: No redemption record is created
```

**Scenario 5: Duplicate Transaction Idempotency**
```gherkin
Given: A coupon was already issued for transaction "T002"
When: POS posts same transaction {transaction_id: "T002", amount: 75.00}
Then: Response returns the existing coupon (same code)
And: No duplicate coupon is created
```

**Scenario 6: Minimum Purchase Not Met**
```gherkin
Given: A coupon with redemption min_purchase of $50
When: Employee attempts to redeem with transaction amount $30
Then: Response has success: false
And: Error code is "MIN_PURCHASE_NOT_MET"
And: Error message includes required minimum amount
```

### Test Coverage Requirements

- **Business Logic**:
  - Buy-side: Flat reward (any transaction earns, no threshold)
  - Sales-side: Tiered reward calculation (all tier boundaries)
  - Sales-side: Flat reward calculation
  - Sales-side: Percentage reward calculation
  - Event lifecycle transitions (draft→active, active→ended)
  - Double-up earning calculation (sales-side only)

- **API Endpoints**:
  - All POS API endpoints with valid/invalid API keys
  - All workspace API endpoints with valid/invalid sessions
  - Error response formats match specification

- **Integration Points**:
  - Ably event publishing (mock Ably, verify payload format)
  - SMS queue integration (mock TextMessageService)
  - Database transactions (verify ACID on redemption)

- **Edge Cases**:
  - Coupon code collision handling
  - Concurrent event activation
  - Partial redemption (if implemented)
  - Refund policy: forfeit vs reinstate

- **Security**:
  - API key validation
  - Store group permission checks
  - CSRF token validation on workspace endpoints

---

## Glossary

### Domain Terms

| Term | Definition | Context |
|------|------------|---------|
| Comeback Cash | Store credit coupon program similar to Kohl's Cash | The feature being implemented |
| Buy-side | Transactions where customer SELLS items TO the store. NO earning thresholds - any transaction earns flat coupon | One of two event sides |
| Sales-side | Transactions where customer BUYS items FROM the store. Supports tiered/percentage earning with thresholds | One of two event sides |
| Event | A time-boxed promotional period with specific earning/redemption rules | Core configuration entity |
| Earning | The process of qualifying for and receiving a coupon | Triggered by qualifying transaction |
| Redemption | Using a coupon to get discount on a purchase | Employee or POS processes code |
| Bearer Instrument | A coupon redeemable by anyone holding the code | No identity verification needed |
| Double-up | Earning a new coupon while redeeming an existing one | Optional event configuration |
| Tiered Earning | Reward amounts that increase with transaction value | e.g., $50=$10, $100=$25 |

### Technical Terms

| Term | Definition | Context |
|------|------------|---------|
| typeNum | Store identifier pattern `[a-z][a-z]\d+` (e.g., `ou00`) | URL routing, database selection |
| Store Database | Per-store MySQL database `kiosk_{typeNum}` | Where ccEvents, ccCoupons stored |
| Ably | Real-time messaging service | Settings sync to POS |
| PDO | PHP Data Objects database abstraction | Database connections |
| PSR-4 | PHP autoloading standard | Namespace → file path mapping |

### API/Interface Terms

| Term | Definition | Context |
|------|------------|---------|
| X-API-Key | HTTP header for POS authentication | Required on all POS API calls |
| Settings Endpoint | `/api/{typeNum}/comeback-cash/settings` | POS fetches active event config |
| Coupon Endpoint | `/api/{typeNum}/comeback-cash/coupons` | POS posts new coupon requests |
| Redemption Endpoint | `/api/{typeNum}/comeback-cash/redeem` | POS or workspace processes redemption |
