# Solution Design Document: Floor Plan Velocity Heatmap

## 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** (6 ADRs approved)
- [x] Component names consistent across diagrams
- [x] A developer could implement from this design
- [x] **Phase 1 scope explicitly defined** (Must Have features only)
- [x] **Database architecture clarified** (Central DB + Store DB)
- [x] **Timezone handling specified** (Store-local to UTC conversion)
- [x] **UI/UX requirements documented** (Legend, stats, tooltips, presets)
- [x] **Analytics events complete** (All PRD tracking requirements)

---

## Constraints

**CON-1 Performance Requirements**
- Velocity calculation must complete in <5 seconds for 50+ subcategories across 100+ sockets
- Must not impact existing sales heatmap performance
- Database queries optimized with proper indexes

**CON-2 Technology Stack**
- Backend: PHP 8.x, existing HeatmapService pattern
- Frontend: Vanilla JavaScript, heatmap.js library (already in use), SyncFusion EJ2 Diagram
- Database: MySQL (store database), existing buyQueue table schema
- Browser support: Modern browsers (Chrome, Safari, Edge)

**CON-3 Existing Infrastructure**
- Must integrate with existing floor plan reports page (`/admin/:typeNum/floor-plan/reports`)
- Reuse existing socket assignment logic, percentile-based scaling infrastructure
- Follow existing HeatmapService::getSalesHeatmapData() patterns
- Must use current socket assignments only (no historical mapping)

**CON-4 Data Constraints**
- Minimum 7 calendar days baseline period required (per PRD)
- Minimum 3 calendar days recent period required (per PRD)
- Must handle stores with data gaps (store closures, holidays)
- Must handle categories with zero sales gracefully

---

## Implementation Context

### Implementation Scope

**Phase 1 (This SDD) - Must Have Features:**
- ✅ Feature 1: Velocity Heatmap Visualization Mode
- ✅ Feature 2: MACD-Style Velocity Calculation
- ✅ Feature 3: Configurable Date Ranges
- ✅ Feature 4: Velocity Legend and Stats
- ✅ Feature 5: Insufficient Data Handling

**Phase 2 (Deferred) - Should Have / Could Have:**
- ⏸️ Feature 6: Velocity vs Sales Comparison View (Should)
- ⏸️ Feature 7: Velocity Alert Thresholds (Should)
- ⏸️ Feature 8: Historical Velocity Trend (Should)
- ⏸️ Feature 9: Velocity Export for Reporting (Could)
- ⏸️ Feature 10: Velocity Benchmark Comparison (Could)
- ⏸️ Feature 11: Velocity-Based Floor Plan Suggestions (Could)

**Rationale:** Phase 1 focuses on core velocity calculation and visualization. Phase 2 features require additional UI complexity, data infrastructure, or user feedback to validate value.

### Required Context Sources

**ICO-1 Existing Floor Plan Heatmap Infrastructure**
```yaml
- file: userfrosting/src/BuyerKiosk/FloorPlan/Services/HeatmapService.php
  relevance: CRITICAL
  sections: [getSalesHeatmapData, calculatePercentileRange, mapSalesToSockets, getSocketAssignments]
  why: "Core heatmap logic pattern to extend for velocity calculations"

- file: userfrosting/routes/floor-plan/api.php
  relevance: HIGH
  sections: [lines 1377-1401 (sales heatmap route)]
  why: "Existing API route pattern to replicate for velocity endpoint"

- file: userfrosting/src/BuyerKiosk/FloorPlan/Controllers/FloorPlanApiController.php
  relevance: HIGH
  sections: [getSalesHeatmap method, lines 2415-2468]
  why: "Controller pattern for heatmap endpoints"

- file: userfrosting/templates/themes/default/admin/floor-plan/reports.html
  relevance: CRITICAL
  sections: [mode selector, date range controls, heatmap rendering logic]
  why: "Frontend integration point - mode switching, date controls, visualization"
```

**ICO-2 Data Source and Business Logic**
```yaml
- file: docs/specs/031-floor-plan-velocity-heatmap/product-requirements.md
  relevance: CRITICAL
  sections: [Feature 2 (MACD calculation), Business Rules, Edge Cases table]
  why: "Velocity calculation formula, date range validation rules, edge case handling"

- file: userfrosting/src/BuyerKiosk/FloorPlan/Services/HeatmapService.php
  relevance: HIGH
  sections: [getSalesBySubcategory, getSalesBySubcategoryFromStoreDb]
  why: "Sales data querying patterns from buyQueue table"
```

### Implementation Boundaries

- **Must Preserve**: Existing sales heatmap functionality, maintenance heatmap, audit mode
- **Can Modify**: HeatmapService (add new method), FloorPlanApiController (add new method), reports.html (add velocity mode)
- **Must Not Touch**: Core diagram rendering logic, socket assignment database schema, existing API contracts

### External Interfaces

#### System Context Diagram

```mermaid
graph TB
    User[Store Manager] --> ReportsUI[Floor Plan Reports UI]

    ReportsUI --> VelocityAPI[Velocity Heatmap API]
    VelocityAPI --> HeatmapService[HeatmapService]
    HeatmapService --> StoreDB[(Store DB - buyQueue)]
    HeatmapService --> SocketDB[(Store DB - fpSocketAssignments)]

    VelocityAPI --> AnalyticsAPI[Analytics Service]
```

#### Interface Specifications

```yaml
# Inbound Interfaces
inbound:
  - name: "Floor Plan Reports UI"
    type: HTTP/HTTPS
    format: REST
    authentication: Session + uri_floor_plans permission
    data_flow: "User requests velocity heatmap with date ranges"

# Outbound Interfaces
outbound:
  - name: "Store Database (buyQueue)"
    type: MySQL Query
    format: SQL
    doc: "Sales transactions by date and subcategory"
    data_flow: "Aggregate daily sales for velocity calculation"
    criticality: CRITICAL

  - name: "Store Database (fpSocketAssignments)"
    type: MySQL Query
    format: SQL
    doc: "Current socket-to-category mappings"
    data_flow: "Map velocity values to floor plan positions"
    criticality: CRITICAL

  - name: "Analytics Platform"
    type: HTTPS
    format: JSON
    doc: "@docs/interfaces/analytics.md"
    data_flow: "Track velocity feature usage events"
    criticality: LOW
```

### Project Commands

```bash
# Component: BuyerKiosk Floor Plan Module
Location: userfrosting/src/BuyerKiosk/FloorPlan/

## Environment Setup
Install Dependencies: cd userfrosting && composer install
Start Development: cd public_html && php -S localhost:8000

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

## Code Quality Commands
Linting: cd userfrosting && ./vendor/bin/phpstan analyse
Type Checking: cd userfrosting && ./vendor/bin/phpstan analyse src/BuyerKiosk/FloorPlan/

## Database Operations
Database Setup: php userfrosting/conductor run
Database Migration: php userfrosting/conductor run

## CSS Build Commands (for frontend changes)
Build CSS: php userfrosting/conductor build-css
Build CSS (Production): php userfrosting/conductor build-css --minify
Watch Mode: php userfrosting/conductor build-css --watch
```

---

## Solution Strategy

**Architecture Pattern:** Service-Oriented Architecture with Controller → Service → Data Access Layer separation

**Integration Approach:** Extend existing HeatmapService with new getVelocityHeatmapData() method, add new API endpoint, integrate velocity mode into existing reports UI

**Justification:**
- Follows established patterns in HeatmapService (getSalesHeatmapData, getMaintenanceHeatmapData)
- Reuses existing infrastructure: percentile-based scaling, socket assignment logic, heatmap.js visualization
- Minimal disruption to existing code (extension rather than modification)
- Natural fit with existing mode-switching UI (sales/maintenance/audit → add velocity)

**Key Decisions:**
- **Decision 1**: Velocity calculation happens server-side in HeatmapService (not frontend)
  - Rationale: Complex aggregation queries, consistent with sales heatmap pattern
- **Decision 2**: Reuse existing heatmap.js library with diverging gradient
  - Rationale: Already proven for sales heatmap, supports custom gradients
- **Decision 3**: Store separate date ranges (recent vs baseline) rather than single range
  - Rationale: MACD-style comparison requires two distinct time periods
- **Decision 4**: Use percentile-based scaling for visualization, absolute % for tooltips/exports
  - Rationale: Matches existing sales heatmap dual normalization approach (PRD ADR-4)

---

## Building Block View

### Components

```mermaid
graph LR
    User[Store Manager] --> ReportsPage[reports.html]

    ReportsPage --> VelocityMode[Velocity Mode UI]
    VelocityMode --> ModeButton[Velocity Mode Button]
    VelocityMode --> DateRangePicker[Date Range Controls with Presets]
    VelocityMode --> Legend[Diverging Gradient Legend]
    VelocityMode --> VelocityAPI[GET /api/:typeNum/floor-plan/plans/:planId/heatmap/velocity]

    VelocityAPI --> Controller[FloorPlanApiController::getVelocityHeatmap]
    Controller --> HeatmapService[HeatmapService::getVelocityHeatmapData]

    HeatmapService --> BuyQueueQuery[Query buyQueue table]
    HeatmapService --> SocketQuery[Query fpSocketAssignments]
    HeatmapService --> VelocityCalc[Calculate MACD-style velocity]
    HeatmapService --> Percentile[Apply percentile scaling]

    VelocityMode --> HeatmapJS[heatmap.js with diverging gradient]
    VelocityMode --> StatsPanel[Stats: Accelerating/Decelerating/Top Movers]
    VelocityMode --> Tooltip[Tooltip: Category + Period Sales + Velocity %]
    VelocityMode --> Analytics[Analytics events]
```

### UI/UX Requirements

**Velocity Mode Button (PRD Feature 1):**
- Location: Mode selector toolbar alongside "Sales" and "Maintenance" buttons
- Label: "Velocity"
- Icon: Speedometer or trend arrow (optional)
- Behavior: Click to switch to velocity mode, loads velocity UI components

**Date Range Controls (PRD Feature 3):**
- Two date pickers: "Recent Period" and "Baseline Period"
- Default values:
  - Recent: Last 7 calendar days (ending yesterday)
  - Baseline: Last 28 calendar days (ending yesterday)
- Presets dropdown:
  - "7 vs 28 days (Default)" - General retail
  - "3 vs 14 days (Fast-Moving)" - Fashion, perishables
  - "14 vs 56 days (Slow-Moving)" - Furniture, appliances
- "Apply" button to trigger recalculation
- Session persistence: Selected ranges persist in browser sessionStorage (not across sessions)

**Diverging Gradient Legend (PRD Feature 4):**
- Position: Below mode selector or in sidebar
- Gradient bar: Blue (left) → Gray (center) → Red (right)
- Labels:
  - Left: "-50%" (or p5 value)
  - Center: "0%"
  - Right: "+50%" (or p95 value)
- Text explanations:
  - Blue zone: "Decelerating" or "Cooling Down"
  - Gray zone: "Stable"
  - Red zone: "Accelerating" or "Heating Up"

**Stats Panel (PRD Feature 4):**
- Cards showing:
  - "Accelerating Categories" - Count with velocity > +10%
  - "Decelerating Categories" - Count with velocity < -10%
  - "Stable Categories" - Count with velocity between -5% and +5%
  - "Top Mover" - Category name, rack, velocity % (highest positive)
  - "Biggest Decline" - Category name, rack, velocity % (lowest negative)
- All stats update when date ranges change

**Tooltip on Socket Hover (PRD Feature 4):**
- Shows:
  - Category name
  - Recent period sales (total, not average)
  - Baseline period sales (total, not average)
  - Velocity percentage
- Example: "Girls Tops: +35.5% velocity, $3,150 recent vs $9,296 baseline"

**Empty State (PRD Feature 5):**
- Shows when insufficientData = true
- Message varies by reason code:
  - INSUFFICIENT_RECENT_DATA: "Insufficient data: Recent period needs at least 3 days of sales"
  - INSUFFICIENT_BASELINE_DATA: "Insufficient data: Baseline period needs at least 7 days of sales"
  - NO_SALES_DATA: "No sales data available for selected periods"
- Guidance text: "Try adjusting date ranges or check back when more data is available"

### Directory Map

**Component**: Floor Plan Heatmap Backend
```
userfrosting/
├── src/BuyerKiosk/FloorPlan/
│   ├── Services/
│   │   └── HeatmapService.php               # MODIFY: Add getVelocityHeatmapData()
│   └── Controllers/
│       └── FloorPlanApiController.php        # MODIFY: Add getVelocityHeatmap()
├── routes/floor-plan/
│   └── api.php                               # MODIFY: Add velocity API route
└── templates/themes/default/admin/floor-plan/
    └── reports.html                          # MODIFY: Add velocity mode UI
```

### Interface Specifications

#### API Contract: Velocity Heatmap Endpoint

**Endpoint:** `GET /api/:typeNum/floor-plan/plans/:planId/heatmap/velocity`

**Request Parameters:**
```yaml
path:
  planId: integer (required) - Floor plan ID
query:
  layoutId: integer (optional) - Layout ID (defaults to current layout)
  recentStartDate: string (required) - YYYY-MM-DD format (recent period start)
  recentEndDate: string (required) - YYYY-MM-DD format (recent period end)
  baselineStartDate: string (required) - YYYY-MM-DD format (baseline period start)
  baselineEndDate: string (required) - YYYY-MM-DD format (baseline period end)
```

**Response (Success):**
```json
{
  "success": true,
  "heatmap": {
    "sockets": [
      {
        "socketId": 1,
        "rackId": 10,
        "socketName": "Zone 1",
        "rackName": "Girls Rack 5",
        "rackSyncfusionId": "rack_xxx_KRCPl",
        "positionX": 100.0,
        "positionY": 200.0,
        "width": 150,
        "height": 100,
        "velocityPercent": 35.5,
        "recentAvgDailySales": 450.0,
        "baselineAvgDailySales": 332.0,
        "recentPeriodSales": 3150.0,
        "baselinePeriodSales": 9296.0,
        "hasAssignments": true,
        "subcategoryBreakdown": [
          {
            "subcategoryCode": "1234",
            "subcategoryName": "Girls Tops",
            "velocityPercent": 35.5,
            "recentAvgDailySales": 450.0,
            "baselineAvgDailySales": 332.0,
            "recentPeriodSales": 3150.0,
            "baselinePeriodSales": 9296.0
          }
        ]
      }
    ],
    "range": {
      "min": -45.2,
      "max": 78.3,
      "average": 12.1,
      "p5": -38.0,
      "p95": 65.0
    },
    "legend": {
      "gradient": "diverging",
      "colors": {
        "deceleration": "#0066CC",
        "stable": "#999999",
        "acceleration": "#CC3300"
      },
      "ranges": [
        {"label": "-50%", "value": -50, "percentile": 0.0},
        {"label": "-25%", "value": -25, "percentile": 0.25},
        {"label": "0%", "value": 0, "percentile": 0.5},
        {"label": "+25%", "value": 25, "percentile": 0.75},
        {"label": "+50%", "value": 50, "percentile": 1.0}
      ]
    },
    "periods": {
      "recent": {
        "startDate": "2026-01-19",
        "endDate": "2026-01-25",
        "dayCount": 7
      },
      "baseline": {
        "startDate": "2025-12-29",
        "endDate": "2026-01-25",
        "dayCount": 28
      }
    },
    "stats": {
      "acceleratingCount": 12,
      "deceleratingCount": 8,
      "stableCount": 15,
      "thresholds": {
        "accelerating": 10.0,
        "decelerating": -10.0,
        "stableMin": -5.0,
        "stableMax": 5.0
      },
      "topMover": {
        "socketName": "Zone 1",
        "rackName": "Girls Rack 5",
        "subcategoryCode": "1234",
        "subcategoryName": "Girls Tops",
        "velocityPercent": 78.3
      },
      "biggestDecline": {
        "socketName": "Zone 3",
        "rackName": "Mens Rack 2",
        "subcategoryCode": "5678",
        "subcategoryName": "Mens Formal",
        "velocityPercent": -45.2
      }
    },
    "insufficientData": false
  }
}
```

**Response (Insufficient Data):**
```json
{
  "success": true,
  "heatmap": {
    "sockets": [],
    "range": {"min": 0, "max": 0, "average": 0, "p5": 0, "p95": 0},
    "legend": {},
    "periods": {...},
    "stats": {
      "acceleratingCount": 0,
      "deceleratingCount": 0,
      "stableCount": 0,
      "thresholds": {...}
    },
    "insufficientData": true,
    "insufficientDataReason": "INSUFFICIENT_RECENT_DATA",
    "insufficientDataMessage": "Recent period needs at least 3 days of sales data"
  }
}
```

**Insufficient Data Reason Codes:**
- `INSUFFICIENT_RECENT_DATA`: Recent period has <3 days of actual sales data
- `INSUFFICIENT_BASELINE_DATA`: Baseline period has <7 days of actual sales data
- `NO_SALES_DATA`: No sales data found in either period
- `INVALID_DATE_RANGE`: Date validation failed (handled as 400 error, not this response)

**Error Responses:**
- `400 Bad Request` - Invalid date format or validation failure
- `404 Not Found` - Floor plan or layout not found
- `500 Internal Server Error` - Database or calculation error

#### Data Storage Changes

No database schema changes required. Uses existing tables:

```yaml
# Existing tables (no modifications)

# STORE DATABASE (e.g., kiosk_ou00) - Sales data
Table: buyQueue
  Database: Store database (kiosk_{typeNum})
  Columns: sellDate, subcategoryCode, total (sales amount)
  Usage: Aggregate daily sales by subcategory for velocity calculation
  Access: Via store DB connection ($storeDb in HeatmapService)

# CENTRAL DATABASE (kiosk_buykiosk) - Floor plan metadata
Table: fpSocketAssignments
  Database: Central database (kiosk_buykiosk)
  Columns: socketId, layoutId, subcategoryCode
  Usage: Map velocity values to socket positions (current assignments only)
  Access: Via central DB connection ($db in LayoutService)

Table: floorPlanLayouts
  Database: Central database (kiosk_buykiosk)
  Columns: id, floorPlanId, name, status, created_at, updated_at
  Usage: Get current/active layout for floor plan

Table: rackSockets
  Database: Central database (kiosk_buykiosk)
  Columns: id, rackId, name, positionX, positionY, width, height
  Usage: Socket geometry for heatmap overlay positioning
```

**Database Architecture:**
- HeatmapService requires TWO database connections:
  - `$db`: Central database connection (floor plan metadata)
  - `$storeDb`: Store database connection (sales data)
- This matches existing getSalesHeatmapData() pattern

#### Analytics Events

All events tracked per PRD Success Metrics section:

```yaml
# Event tracking (sent to existing analytics pipeline)
events:
  - name: velocity_mode_viewed
    properties:
      storeId: string
      floorPlanId: int
      layoutId: int
      recentPeriodDays: int
      baselinePeriodDays: int
      timestamp: ISO8601
    trigger: User opens velocity mode

  - name: velocity_date_range_changed
    properties:
      storeId: string
      recentDays: int
      baselineDays: int
      timestamp: ISO8601
    trigger: User changes date ranges and clicks Apply

  - name: velocity_heatmap_loaded
    properties:
      storeId: string
      floorPlanId: int
      dataPointCount: int
      loadTimeMs: int
      insufficientData: boolean
      timestamp: ISO8601
    trigger: API returns velocity heatmap data

  - name: velocity_rack_clicked
    properties:
      storeId: string
      rackId: int
      subcategoryCode: string
      velocityPercent: float
      timestamp: ISO8601
    trigger: User clicks socket/rack to view details

  - name: velocity_data_exported
    properties:
      storeId: string
      floorPlanId: int
      exportFormat: string (csv)
      timestamp: ISO8601
    trigger: User exports velocity data (Phase 2)

  - name: velocity_comparison_toggled
    properties:
      storeId: string
      comparisonType: string
      enabled: boolean
      timestamp: ISO8601
    trigger: User toggles velocity vs sales comparison (Phase 2)

  - name: velocity_alert_threshold_set
    properties:
      storeId: string
      userId: int
      accelerationThreshold: float
      decelerationThreshold: float
      timestamp: ISO8601
    trigger: User customizes alert thresholds (Phase 2)
```

**Phase 1 Events:** velocity_mode_viewed, velocity_date_range_changed, velocity_heatmap_loaded, velocity_rack_clicked

**Phase 2 Events:** velocity_data_exported, velocity_comparison_toggled, velocity_alert_threshold_set

#### Application Data Models

```php
// HeatmapService::getVelocityHeatmapData() return structure
ENTITY: VelocityHeatmapData (NEW)
  FIELDS:
    sockets: array<Socket> (socket data with velocity values)
    range: array (min, max, avg, p5, p95 velocity percentages)
    periods: array (recent and baseline date range metadata)
    stats: array (accelerating/decelerating counts, top mover, biggest decline)
    insufficientData: boolean
    insufficientDataReason: string (optional)

ENTITY: VelocitySocket (extends existing Socket structure)
  FIELDS:
    socketId: int
    rackId: int
    socketName: string
    rackName: string
    rackSyncfusionId: string (nullable)
    positionX: float
    positionY: float
    width: int
    height: int
    + velocityPercent: float (NEW) - MACD-style velocity percentage (aggregated if multiple subcategories)
    + recentAvgDailySales: float (NEW) - Sum of all assigned subcategories' recent averages
    + baselineAvgDailySales: float (NEW) - Sum of all assigned subcategories' baseline averages
    + recentPeriodSales: float (NEW) - Total sales in recent period
    + baselinePeriodSales: float (NEW) - Total sales in baseline period
    hasAssignments: boolean
    + subcategoryBreakdown: array<VelocitySubcategory> (NEW) - Individual category details

  AGGREGATION RULES (multiple subcategories per socket):
    - Sum recent sales across all assigned subcategories
    - Sum baseline sales across all assigned subcategories
    - Calculate socket-level velocity using aggregated sums
    - Socket velocity = (aggregated recent avg - aggregated baseline avg) / aggregated baseline avg * 100
    - Tooltip shows socket-level aggregated values
    - subcategoryBreakdown array shows individual category velocities for drill-down

ENTITY: VelocitySubcategory (NEW)
  FIELDS:
    subcategoryCode: string
    subcategoryName: string
    velocityPercent: float
    recentAvgDailySales: float
    baselineAvgDailySales: float
```

### Implementation Examples

#### Example: Velocity Calculation Algorithm

**Why this example**: Core business logic for MACD-style velocity calculation with edge case handling

```php
/**
 * Calculate velocity percentage using MACD-inspired formula
 *
 * @param float $recentAvgDailySales Average daily sales for recent period
 * @param float $baselineAvgDailySales Average daily sales for baseline period
 * @return float Velocity percentage
 */
private function calculateVelocity(
    float $recentAvgDailySales,
    float $baselineAvgDailySales
): float {
    // Edge case 1: Both zero = no activity
    if ($baselineAvgDailySales == 0 && $recentAvgDailySales == 0) {
        return 0.0;
    }

    // Edge case 2: New category (baseline zero, recent has sales)
    if ($baselineAvgDailySales == 0 && $recentAvgDailySales > 0) {
        return 100.0; // +100% = new category
    }

    // Edge case 3: Discontinued category (baseline has sales, recent zero)
    if ($baselineAvgDailySales > 0 && $recentAvgDailySales == 0) {
        return -100.0; // -100% = discontinued
    }

    // Standard velocity calculation: (recent - baseline) / baseline * 100
    // Example: recent=$450, baseline=$300 → (450-300)/300*100 = +50%
    return (($recentAvgDailySales - $baselineAvgDailySales) / $baselineAvgDailySales) * 100;
}

/**
 * Get daily sales aggregated by subcategory for a date range
 * Returns array with calendar days as denominator (includes zero-sales days)
 *
 * @param string $startDate Start date (Y-m-d in store local time)
 * @param string $endDate End date (Y-m-d in store local time)
 * @param string $storeTimezone Store timezone (e.g., 'America/Los_Angeles')
 * @return array Map of subcategoryCode => [avgDailySales, totalSales, calendarDays, daysWithSales]
 */
private function getDailySalesBySubcategory(
    string $startDate,
    string $endDate,
    string $storeTimezone
): array {
    // Calculate calendar days (NOT days with sales)
    $start = new DateTime($startDate, new DateTimeZone($storeTimezone));
    $end = new DateTime($endDate, new DateTimeZone($storeTimezone));
    $calendarDays = $start->diff($end)->days + 1;

    // Convert store-local dates to UTC boundaries for query
    // Day starts at midnight store-local time, ends at 23:59:59 store-local time
    $startUtc = clone $start;
    $startUtc->setTimezone(new DateTimeZone('UTC'));
    $endUtc = clone $end;
    $endUtc->setTime(23, 59, 59);
    $endUtc->setTimezone(new DateTimeZone('UTC'));

    // Query buyQueue for actual sales
    // CRITICAL: Use sellDate range (not DATE(sellDate)) to preserve indexes
    $stmt = $this->storeDb->prepare("
        SELECT
            subcategoryCode,
            SUM(total) as totalSales,
            COUNT(DISTINCT DATE(CONVERT_TZ(sellDate, 'UTC', :timezone))) as daysWithSales
        FROM buyQueue
        WHERE sellDate >= :startUtc
          AND sellDate <= :endUtc
        GROUP BY subcategoryCode
    ");
    $stmt->execute([
        'startUtc' => $startUtc->format('Y-m-d H:i:s'),
        'endUtc' => $endUtc->format('Y-m-d H:i:s'),
        'timezone' => $storeTimezone
    ]);

    $result = [];
    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
        $code = $row['subcategoryCode'];
        $totalSales = (float) $row['totalSales'];

        // CRITICAL: Use calendar days as denominator (not days with sales)
        // This handles store closures naturally (closed days = $0 sales = lower average)
        $avgDailySales = $totalSales / $calendarDays;

        $result[$code] = [
            'avgDailySales' => $avgDailySales,
            'totalSales' => $totalSales,
            'calendarDays' => $calendarDays,
            'daysWithSales' => (int) $row['daysWithSales']
        ];
    }

    return $result;
}
```

---

## Runtime View

### Primary Flow: User Views Velocity Heatmap

1. **User Action**: Store manager selects "Velocity" mode in floor plan reports
2. **Frontend**:
   - Show velocity date range controls (recent + baseline date pickers)
   - Set defaults: recent=last 7 days, baseline=last 28 days
3. **User Action**: Click "Apply Dates" (or use defaults)
4. **Frontend Validation**:
   - Validate recent period ≥ 3 calendar days
   - Validate baseline period ≥ 7 calendar days
   - Validate recent start date > baseline start date (recent is chronologically later)
5. **API Request**: `GET /api/:typeNum/floor-plan/plans/:planId/heatmap/velocity?recentStartDate=...&recentEndDate=...&baselineStartDate=...&baselineEndDate=...`
6. **Backend Processing**:
   - Controller validates parameters and permissions
   - HeatmapService fetches current layout socket assignments
   - Query buyQueue for recent period daily sales by subcategory
   - Query buyQueue for baseline period daily sales by subcategory
   - Calculate velocity for each subcategory using MACD formula
   - Map velocity values to sockets (aggregate multiple subcategories per socket)
   - Apply percentile-based scaling (5th-95th percentile for visual range)
   - Calculate stats (accelerating/decelerating counts, top mover, biggest decline)
7. **API Response**: Return velocity heatmap data with socket positions and velocity values
8. **Frontend Visualization**:
   - Initialize heatmap.js with diverging gradient (blue-gray-red)
   - Render velocity overlay on diagram using socket positions
   - Update stats panel (accelerating/decelerating counts, top/bottom movers)
   - Show tooltips on hover (category name, velocity %, recent vs baseline sales)
9. **Analytics**: Track `velocity_mode_viewed` event

```mermaid
sequenceDiagram
    actor User
    participant UI as Reports UI
    participant API as FloorPlanApiController
    participant Service as HeatmapService
    participant DB as Store Database

    User->>UI: Select "Velocity" mode
    UI->>UI: Show date range controls (recent + baseline)
    User->>UI: Click "Apply Dates" (or use defaults)
    UI->>UI: Validate date ranges (≥3 days recent, ≥7 days baseline)
    UI->>API: GET /velocity?recentStart=...&recentEnd=...&baselineStart=...&baselineEnd=...
    API->>API: Validate permissions + parameters
    API->>Service: getVelocityHeatmapData(planId, layoutId, dates...)
    Service->>DB: Query socket assignments (current layout)
    DB-->>Service: Socket-to-category mappings
    Service->>DB: Query buyQueue (recent period daily sales by subcategory)
    DB-->>Service: Recent period sales data
    Service->>DB: Query buyQueue (baseline period daily sales by subcategory)
    DB-->>Service: Baseline period sales data
    Service->>Service: Calculate velocity per subcategory (MACD formula)
    Service->>Service: Map velocities to sockets (aggregate)
    Service->>Service: Apply percentile scaling (5th-95th)
    Service->>Service: Calculate stats (accelerating/decelerating, top mover, etc.)
    Service-->>API: VelocityHeatmapData
    API-->>UI: JSON response with velocity data
    UI->>UI: Render heatmap with diverging gradient (heatmap.js)
    UI->>UI: Update stats panel
    UI->>User: Display velocity visualization
```

### Error Handling

**Invalid Date Ranges (Frontend + Backend Validation):**
- **Validation**: Recent period < 3 calendar days OR baseline period < 7 calendar days
- **Response**: 400 Bad Request with specific error message
- **User Feedback**: Show error alert with guidance ("Recent period needs at least 3 days")
- **Timing**: Frontend validates before API call; backend validates again for security

**Insufficient Sales Data (Backend Only):**
- **Detection Logic**:
  1. Query buyQueue for recent period, count distinct days with sales data
  2. If daysWithSales < 3 → Insufficient recent data
  3. Query buyQueue for baseline period, count distinct days with sales data
  4. If daysWithSales < 7 → Insufficient baseline data
  5. If both queries return 0 rows → No sales data
- **Response**: 200 OK with `insufficientData: true` flag and reason code
- **User Feedback**: Show empty state with specific message:
  - `INSUFFICIENT_RECENT_DATA`: "Insufficient data: Recent period needs at least 3 days of sales"
  - `INSUFFICIENT_BASELINE_DATA`: "Insufficient data: Baseline period needs at least 7 days of sales"
  - `NO_SALES_DATA`: "No sales data available for selected periods. Try adjusting date ranges."
- **Why This Matters**: A store could have a valid 7-day date range but only 2 days with actual sales (rest were closures). Calendar day validation passes, but data validation fails.

**Database Query Failure:**
- **Detection**: PDO exception during buyQueue query
- **Fallback**: Log error, return 500 Internal Server Error
- **User Feedback**: Show error alert: "Error loading velocity data. Please try again."

**Layout Not Found:**
- **Detection**: No current layout exists for floor plan
- **Response**: 404 Not Found
- **User Feedback**: Show error alert: "No layout found for this floor plan. Create a layout in the designer first."

### Complex Logic: Date Range Validation

```pseudocode
ALGORITHM: Validate Velocity Date Ranges
INPUT: recentStartDate, recentEndDate, baselineStartDate, baselineEndDate
OUTPUT: ValidationResult (success: boolean, error: string)

1. PARSE dates to DateTime objects:
   - If parse fails → Return error "Invalid date format (expected YYYY-MM-DD)"

2. CALCULATE period lengths:
   - recentDays = (recentEnd - recentStart).days + 1
   - baselineDays = (baselineEnd - baselineStart).days + 1

3. VALIDATE minimum periods:
   - If recentDays < 3 → Return error "Recent period needs at least 3 days"
   - If baselineDays < 7 → Return error "Baseline period needs at least 7 days"

4. VALIDATE chronological order:
   - If recentStart <= baselineStart → Return error "Recent period must start after baseline period"
   - NOTE: Recent and baseline periods CAN OVERLAP (recent can be subset of baseline)
   - Example: baseline = Dec 1-28, recent = Dec 22-28 is VALID

5. VALIDATE against current date:
   - If recentEnd > yesterday → Return error "Cannot use partial current-day data"
   - If baselineEnd > yesterday → Return error "Cannot use partial current-day data"

6. RETURN ValidationResult(success: true, error: null)
```

---

## Deployment View

### Single Application Deployment

- **Environment**: Existing PHP web application (no separate deployment)
- **Configuration**: No new environment variables (uses existing database connections)
- **Dependencies**:
  - Existing buyQueue table with sellDate and subcategoryCode indexed
  - Existing fpSocketAssignments table
  - heatmap.js library (already loaded in reports.html)
- **Performance**:
  - Expected load: <5 seconds for 50+ subcategories, 100+ sockets
  - Caching strategy: None initially (can add query result caching in Phase 2 if needed)
  - Database optimization: Ensure indexes exist on buyQueue.sellDate and buyQueue.subcategoryCode

---

## Cross-Cutting Concepts

### Pattern Documentation

```yaml
# Existing patterns used
- pattern: Service Layer Pattern (HeatmapService)
  relevance: CRITICAL
  why: "Consistent with existing getSalesHeatmapData(), getMaintenanceHeatmapData() methods"

- pattern: Controller-Service Separation
  relevance: HIGH
  why: "Controller validates/routes, Service contains business logic"

- pattern: Percentile-Based Scaling
  relevance: HIGH
  why: "Reuse existing calculatePercentileRange() method for visual normalization"

- pattern: Socket-to-Category Mapping
  relevance: CRITICAL
  why: "Reuse existing getSocketAssignments() and mapSalesToSockets() patterns"
```

### System-Wide Patterns

**Security:**
- Authentication: Session-based (existing pattern)
- Authorization: uri_floor_plans permission check (existing)
- Input Validation: Date format validation, range checks

**Error Handling:**
- Global: PHP exceptions caught in controller, logged to error_log
- Local: Validation errors return 400 with specific messages
- Propagation: Service throws InvalidArgumentException for validation, RuntimeException for business logic errors

**Performance:**
- Caching: None initially (can add Redis caching of query results in Phase 2)
- Batching: Single query per period (recent + baseline = 2 queries total)
- Async: None (synchronous request-response pattern)

**Logging/Auditing:**
- Query logging: Use existing error_log for slow queries (>5 seconds)
- Analytics: Track velocity_mode_viewed, velocity_date_range_changed events
- Audit trail: No audit log entries for read-only operations

### Implementation Patterns

#### Code Patterns and Conventions

- **Naming**: camelCase for methods (getVelocityHeatmapData), snake_case for database columns (subcategoryCode in response but subCatCode in buyQueue table)
- **Type hints**: Use PHP 8.x type hints for parameters and return types
- **Null handling**: Use nullable types (?string, ?int) for optional fields
- **Array structures**: Use associative arrays with documented keys (match existing HeatmapService patterns)

#### State Management Patterns

- **Frontend State**: Track current mode (sales/velocity/maintenance), selected date ranges, cached velocity data
- **No Server State**: Stateless API (all context in request parameters)
- **Diagram State**: Reuse existing SyncFusion diagram instance, heatmap overlay layer management

#### Performance Characteristics

- **Query Optimization**: Use DATE() function for date comparisons, GROUP BY subcategoryCode for aggregation
- **Index Requirements**: Composite index on (sellDate, subcategoryCode) for buyQueue table
- **Memory Management**: Process socket aggregation in-memory (acceptable for <1000 sockets)
- **Response Size**: Typical response ~50-200KB (acceptable for modern browsers)

#### Integration Patterns

- **API Communication**: RESTful JSON over HTTPS (existing pattern)
- **Database Access**: PDO with prepared statements (existing pattern)
- **Frontend Integration**: Fetch API with async/await (existing pattern in reports.html)

#### Component Structure Pattern

```pseudocode
# Frontend component organization (reports.html)
COMPONENT: VelocityModeUI
  INITIALIZE:
    - mode = 'velocity'
    - recentDateRange = {start: 7 days ago, end: yesterday}
    - baselineDateRange = {start: 28 days ago, end: yesterday}
    - velocityData = null

  HANDLE:
    - onModeChange: Switch between sales/velocity/maintenance modes
    - onDateChange: Update date range state
    - onApplyClick: Fetch velocity data from API
    - onSocketHover: Show velocity tooltip with details

  RENDER:
    IF loading: Show loading spinner
    IF insufficientData: Show empty state with guidance message
    IF success:
      - Render heatmap overlay with diverging gradient
      - Update stats panel (accelerating/decelerating counts)
      - Setup tooltip handlers
```

#### Data Processing Pattern

```php
# Backend service method structure
FUNCTION: HeatmapService::getVelocityHeatmapData(
    int $floorPlanId,
    int $layoutId,
    string $recentStartDate,
    string $recentEndDate,
    string $baselineStartDate,
    string $baselineEndDate,
    string $typeNum
): array

VALIDATE:
  - Date format (Y-m-d)
  - Recent period ≥ 3 days
  - Baseline period ≥ 7 days
  - Recent start > baseline start

FETCH:
  - Socket assignments for layout (getSocketAssignments)
  - Recent period sales by subcategory (getDailySalesBySubcategory)
  - Baseline period sales by subcategory (getDailySalesBySubcategory)

TRANSFORM:
  - Calculate velocity per subcategory (calculateVelocity)
  - Map velocities to sockets (aggregate multiple subcategories)
  - Apply percentile scaling for visual range (calculatePercentileRange)

CALCULATE:
  - Stats: accelerating/decelerating counts, top mover, biggest decline

RETURN: {
  sockets: array,
  range: array,
  periods: array,
  stats: array,
  insufficientData: boolean
}
```

#### Error Handling Pattern

```php
# Error classification and handling
FUNCTION: FloorPlanApiController::getVelocityHeatmap(int $planId)
  TRY:
    - Validate permissions (checkReadAuth)
    - Validate parameters (date formats, ranges)
    - Call service method
    - Return success response
  CATCH InvalidArgumentException:
    - Log error details
    - Return 400 Bad Request with user-friendly message
  CATCH RuntimeException:
    - Log error details
    - Return 500 Internal Server Error
  CATCH PDOException:
    - Log SQL error (DO NOT expose to user)
    - Return 500 Internal Server Error with generic message
```

#### Test Pattern

```php
# Unit test structure for velocity calculation
TEST_SCENARIO: "Velocity calculated correctly for accelerating category"
  SETUP:
    - Mock buyQueue data: recent avg = $450/day, baseline avg = $300/day
    - Mock socket assignments: single category mapped to single socket
  EXECUTE:
    - Call HeatmapService::getVelocityHeatmapData()
  VERIFY:
    - velocityPercent = +50% (correct: (450-300)/300*100 = 50%)
    - recentAvgDailySales = 450
    - baselineAvgDailySales = 300
    - socket.hasAssignments = true

TEST_SCENARIO: "Handles new category (baseline zero, recent has sales)"
  SETUP:
    - Mock buyQueue data: recent avg = $200/day, baseline avg = $0/day
  EXECUTE:
    - Call calculateVelocity(200, 0)
  VERIFY:
    - velocityPercent = +100% (new category)

TEST_SCENARIO: "Handles insufficient data (recent period < 3 days)"
  SETUP:
    - Date range: recent = 2 days, baseline = 7 days
  EXECUTE:
    - Call getVelocityHeatmapData()
  VERIFY:
    - insufficientData = true
    - insufficientDataReason contains "Recent period needs at least 3 days"
```

---

## Architecture Decisions

**ADR-1 Server-Side Velocity Calculation**
- **Choice**: Calculate velocity in HeatmapService (PHP backend), not frontend JavaScript
- **Rationale**:
  - Complex database aggregation queries (GROUP BY, SUM, timezone conversion, date ranges)
  - Consistent with existing sales heatmap pattern (getSalesHeatmapData)
  - Reduces frontend complexity and data transfer (send aggregated results, not raw sales data)
  - Centralized timezone handling (store-local to UTC conversion)
- **Trade-offs**: Slight increase in server load, but acceptable for <5 second response time target
- **Status**: ✅ Approved (follows existing pattern)

**ADR-2 Reuse heatmap.js with Diverging Gradient**
- **Choice**: Use existing heatmap.js library with custom diverging gradient (blue-gray-red)
- **Rationale**:
  - Already proven for sales heatmap visualization
  - Supports custom gradients via configuration
  - Avoids introducing new visualization library
  - Diverging gradient naturally communicates positive/negative velocity
- **Trade-offs**: heatmap.js uses radial blur (not rectangular zones), but acceptable for smooth velocity visualization
- **Status**: ✅ Approved (reuse proven library)

**ADR-3 Dual Date Range UI (Recent + Baseline)**
- **Choice**: Separate date pickers for recent period and baseline period with preset dropdown
- **Rationale**:
  - MACD-style comparison inherently requires two time periods
  - Makes comparison explicit to users (not hidden calculation)
  - Allows flexible period configuration (e.g., 3 vs 14 days for fast-moving categories)
  - Presets ("7 vs 28", "3 vs 14", "14 vs 56") balance flexibility with ease of use
- **Trade-offs**: Slightly more complex UI than single date range, but necessary for velocity concept
- **Status**: ✅ Approved (validated by PRD user research)

**ADR-4 Percentile Scaling for Visualization, Absolute % for Business Value**
- **Choice**: Use 5th-95th percentile for color gradient scaling, but show actual velocity % in tooltips/exports
- **Rationale**:
  - Percentile scaling prevents outliers from dominating visual scale (consistent with sales heatmap)
  - Absolute velocity % preserves business meaning for decision-making
  - Best of both approaches (per PRD Open Question resolution)
  - Legend shows actual percentage labels, not percentile values
- **Trade-offs**: Dual normalization adds complexity, but critical for both visual clarity and business utility
- **Status**: ✅ Approved (per PRD ADR-4)

**ADR-5 Central DB for Floor Plan Metadata, Store DB for Sales Data**
- **Choice**: Use two database connections - central DB (kiosk_buykiosk) for floor plan tables, store DB (kiosk_{typeNum}) for buyQueue
- **Rationale**:
  - Floor plan metadata is shared across stores (central DB per project architecture)
  - Sales data is store-specific (per-store database per project architecture)
  - Matches existing HeatmapService pattern (already uses both connections)
- **Trade-offs**: Requires joining data across two database connections in application code, but this is already established pattern
- **Status**: ✅ Approved (follows existing architecture)

**ADR-6 Phase 2 Deferral of Should/Could Features**
- **Choice**: Defer Features 6-11 (comparison view, alerts, trends, export, benchmarks, suggestions) to Phase 2
- **Rationale**:
  - MVP focuses on core velocity calculation and visualization
  - User feedback needed to validate value before investing in complex features
  - Export (Feature 9) requires CSV generation infrastructure
  - Comparison view (Feature 6) requires dual overlay visualization
  - Alerts (Feature 7) require user preferences storage and notification system
- **Trade-offs**: Reduced Phase 1 functionality, but enables faster time-to-market for core feature validation
- **Status**: ✅ Approved (explicit PRD scoping)

---

## Quality Requirements

**Performance:**
- Target: Velocity calculation completes in <5 seconds for 50+ subcategories, 100+ sockets
- Measurement: Server-side timing (log if >5 seconds), track via `velocity_heatmap_loaded` event with loadTimeMs property
- Test coverage: Load test with realistic data volume (50 subcategories, 100 sockets, 90 day baseline period)

**Usability:**
- Target: Clear visual distinction between accelerating (red), stable (gray), decelerating (blue) categories
- Measurement: Diverging gradient renders correctly, tooltips show velocity % with context
- Test coverage: Manual QA testing, screenshot comparison

**Security:**
- Target: No unauthorized access to velocity data
- Measurement: Permission checks (uri_floor_plans), store group validation (checkStoreGroup)
- Test coverage: Unit tests for permission validation, integration tests for unauthorized access attempts

**Reliability:**
- Target: Handles edge cases gracefully (zero sales, new categories, data gaps)
- Measurement: No unhandled exceptions, appropriate error messages for users
- Test coverage: Unit tests for all edge cases in PRD Edge Cases table (15+ scenarios)

---

## Risks and Technical Debt

### Known Technical Issues

**Issue 1: buyQueue table query performance with large date ranges**
- **Impact**: Slow queries (>5 seconds) if baseline period >90 days or store has millions of transactions
- **Mitigation**:
  - Add composite index on (sellDate, subcategoryCode) if not exists
  - Consider caching query results for common date ranges (Phase 2)
  - Document recommended maximum baseline period (90 days) in user guidance

**Issue 2: Diverging gradient may not render correctly on overlapping sockets**
- **Impact**: Visual confusion if multiple sockets overlap spatially
- **Mitigation**:
  - heatmap.js uses radial blur which naturally blends overlapping areas
  - Tooltips provide precise values on hover
  - Accept as known limitation for MVP (rare case in practice)

### Technical Debt

**Debt 1: No caching of velocity calculations**
- **Impact**: Repeated requests with same date ranges re-query database every time
- **Temporary Solution**: Acceptable for MVP (low usage volume)
- **Proper Solution**: Add Redis caching with cache key = hash(planId, layoutId, dateRanges)
- **Timeline**: Phase 2 if velocity feature shows high adoption (>40% weekly usage per PRD metrics)

**Debt 2: Date range validation partially duplicated between frontend and backend**
- **Impact**: Inconsistent validation logic if not kept in sync
- **Temporary Solution**: Document validation rules clearly in both locations
- **Proper Solution**: Create shared validation library or move all validation to backend
- **Timeline**: Low priority (validation logic is simple and stable)

### Implementation Gotchas

**Gotcha 1: Calendar days vs days with sales**
- **Issue**: Must use calendar days as denominator (not days with sales data)
- **Why it matters**: Store closures, holidays = zero sales days, which correctly lower average
- **Solution**: Document clearly in code comments, add unit tests to verify

**Gotcha 2: Current socket assignments only (no historical mapping)**
- **Issue**: If user changes socket assignments mid-period, velocity uses current mapping for all dates
- **Why it matters**: Historical velocity may be slightly inaccurate for changed sockets
- **Solution**: Acceptable MVP trade-off per PRD (documented as known limitation)

**Gotcha 3: heatmap.js canvas positioning with SyncFusion diagram zoom/pan**
- **Issue**: heatmap.js canvas may not align perfectly with diagram after zoom/pan
- **Why it matters**: Velocity overlay appears in wrong position
- **Solution**: Re-render heatmap on diagram scrollChange event (existing pattern in reports.html)

---

## Test Specifications

### Critical Test Scenarios

**Scenario 1: Standard Velocity Calculation (Happy Path)**
```gherkin
Given: Floor plan with 10 sockets assigned to categories
And: Recent period has 7 days of sales data (Jan 19-25)
And: Baseline period has 28 days of sales data (Dec 29 - Jan 25)
And: Girls Tops category: recent avg=$450/day, baseline avg=$300/day
When: User selects Velocity mode and applies default date ranges
Then: Girls Tops socket shows velocity = +50%
And: Heatmap renders with warm/red color for that socket
And: Tooltip shows "Girls Tops: +50% velocity, $450/day recent vs $300/day baseline"
And: Stats panel shows "1 Accelerating Category"
```

**Scenario 2: Insufficient Data Validation**
```gherkin
Given: Floor plan with sockets assigned
When: User sets recent period to 2 days (less than minimum 3)
And: User clicks "Apply Dates"
Then: Frontend shows validation error "Recent period needs at least 3 days"
And: API request is not sent
And: User can adjust date range and retry
```

**Scenario 3: Edge Case - New Category (Zero Baseline)**
```gherkin
Given: Floor plan with socket for "New Arrivals" category
And: Baseline period (Dec 29 - Jan 25): zero sales for this category
And: Recent period (Jan 19-25): $200/day average sales for this category
When: User views velocity heatmap
Then: "New Arrivals" socket shows velocity = +100%
And: Tooltip shows "New Arrivals: New category, $200/day recent vs $0/day baseline"
And: Heatmap renders with maximum heat (red) for that socket
```

**Scenario 4: Edge Case - Discontinued Category (Zero Recent)**
```gherkin
Given: Floor plan with socket for "Winter Coats" category
And: Baseline period: $500/day average sales
And: Recent period: zero sales (category discontinued)
When: User views velocity heatmap
Then: "Winter Coats" socket shows velocity = -100%
And: Tooltip shows "Winter Coats: Discontinued, $0/day recent vs $500/day baseline"
And: Heatmap renders with maximum cool (blue) for that socket
```

**Scenario 5: Performance Under Load**
```gherkin
Given: Floor plan with 100 sockets, 50 unique subcategories
And: Baseline period with 90 days of sales data
And: Store database has 100,000+ buyQueue records in period
When: User applies date ranges and requests velocity heatmap
Then: API response returns in <5 seconds
And: Response includes all 100 sockets with velocity values
And: Performance event logged with loadTimeMs < 5000
```

**Scenario 6: Database Query Failure Recovery**
```gherkin
Given: Floor plan with sockets assigned
When: User applies date ranges
And: Database query fails (connection timeout, table lock, etc.)
Then: API returns 500 Internal Server Error
And: Error is logged to error_log with details
And: User sees error message: "Error loading velocity data. Please try again."
And: User can retry request
```

### Test Coverage Requirements

- **Business Logic**:
  - Velocity calculation formula (all cases: standard, zero baseline, zero recent, both zero)
  - Date range validation (15+ test cases covering PRD validation rules)
  - Socket-to-category aggregation (single category, multiple categories per socket)
  - Percentile scaling (edge cases: all same value, single outlier, negative values)

- **User Interface**:
  - Mode switching (sales → velocity → maintenance → velocity)
  - Date range picker validation (frontend + backend)
  - Heatmap rendering with diverging gradient
  - Tooltip display (hover, leave, multiple rapid hovers)
  - Stats panel updates (accelerating/decelerating counts, top mover, biggest decline)

- **Integration Points**:
  - API endpoint (request validation, response format, error handling)
  - buyQueue database queries (aggregate by subcategory, date filtering)
  - fpSocketAssignments queries (layout-specific assignments)
  - Analytics event tracking (velocity_mode_viewed, velocity_date_range_changed, etc.)

- **Edge Cases** (per PRD Edge Cases table):
  - New product category (in recent, not in baseline)
  - Discontinued product (in baseline, not in recent)
  - Category moved between sockets during period
  - Recent period overlaps baseline period (valid scenario)
  - No sales in either period
  - Single large sale in recent period (outlier)
  - Store closed for multiple days in period
  - Seasonal transition (large velocity swings)
  - Returns exceed sales in a day (negative daily sales)
  - Late data backfill (transaction posted days later)

- **Performance**:
  - Response time <5 seconds with realistic data volume
  - Memory usage acceptable (<100MB for typical request)
  - Database query count (2 queries: recent period + baseline period)

- **Security**:
  - Permission check (uri_floor_plans)
  - Store group validation (checkStoreGroup)
  - SQL injection protection (prepared statements)
  - XSS protection (sanitize category names in tooltips)

---

## Glossary

### Domain Terms

| Term | Definition | Context |
|------|------------|---------|
| Velocity | Rate of change in sales performance (accelerating or decelerating) | MACD-inspired momentum indicator showing whether category sales are speeding up or slowing down relative to baseline |
| MACD (Moving Average Convergence Divergence) | Technical analysis indicator comparing two moving averages | Retail adaptation: compare recent sales avg vs baseline sales avg to detect acceleration/deceleration |
| Socket | Physical zone on floor plan rack where specific product categories are displayed | Unit of heatmap visualization - each socket shows velocity for assigned categories |
| Subcategory | Product classification level used for sales tracking | Maps to subcategoryCode in buyQueue table (e.g., "Girls Tops", "Mens Shoes") |
| Baseline Period | Historical time range for comparison (default: last 28 days) | "Long-term" moving average in MACD terminology |
| Recent Period | Current time range for performance measurement (default: last 7 days) | "Short-term" moving average in MACD terminology |

### Technical Terms

| Term | Definition | Context |
|------|------------|---------|
| Percentile-Based Scaling | Normalization using 5th-95th percentile range to handle outliers | Visual color mapping uses percentile scale (prevents extreme values from dominating), but tooltips show actual velocity % |
| Diverging Gradient | Color scheme with meaningful zero point (blue-gray-red) | Blue = deceleration, Gray = stable, Red = acceleration. Different from thermal gradient (cold-to-hot) used for sales |
| Calendar Days | Total days in period including days with zero sales | Critical: denominator uses calendar days, not "days with sales data". Handles store closures naturally. |
| Socket Assignment | Mapping between fpSocketAssignments.subcategoryCode and floor plan position | Current assignments only (historical changes ignored per PRD) |

### API/Interface Terms

| Term | Definition | Context |
|------|------------|---------|
| `getVelocityHeatmapData()` | HeatmapService method returning velocity data for floor plan | Core service method - calculates velocity, maps to sockets, applies scaling |
| `velocityPercent` | Velocity value as percentage (negative = deceleration, positive = acceleration) | Business value (not normalized) - shown in tooltips and exports |
| `recentAvgDailySales` | Average daily sales for recent period | Used in velocity calculation: (recent - baseline) / baseline * 100 |
| `baselineAvgDailySales` | Average daily sales for baseline period | Denominator in velocity formula |
| `insufficientData` | Boolean flag indicating insufficient data for calculation | True if recent < 3 days OR baseline < 7 days OR no sales data |
