# Solution Design Document

## Validation Checklist

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

---

## Constraints

CON-1 **Framework**: PHP 8.x, Slim 2.6.2, Twig 1.44.8, MariaDB/MySQL. Frontend uses Syncfusion EJ2 components, Bootstrap 5.3.3, vanilla JS (no React/Vue).

CON-2 **Database**: All schema changes via migration system (`userfrosting/conductor`). Central DB (`kiosk_buykiosk`) for config, store DBs (`kiosk_xx00`) for cached forecasts. No direct SQL on store databases.

CON-3 **Auth**: UserFrosting permission system (`checkAccess`, `checkStoreGroup`). Session-based for web, JWT/HybridAuth for mobile API. Reuses existing `uri_store_settings` permission for admin edit access (no new permission needed).

CON-4 **Performance**: API responses < 200ms for 7-day forecast range. Pre-computed cache must handle 365 rows per store. Nightly recompute for all stores must complete within 30 minutes.

CON-5 **Compatibility**: Server-calculated goals must be primary. Sync app pushes accepted as fallback only when server hasn't computed. KPI bar backward-compatible with existing `workbook_kpi_config` system.

CON-6 **Syncfusion**: Use Syncfusion EJ2 components per CLAUDE.md directive. Schedule (month view) for Method 2 calendar, NumericTextBox for inputs, Tab for method switching.

## Implementation Context

### Required Context Sources

- ICO-1 General Application Context
  ```yaml
  - doc: docs/systems/sync-app-goals-system.md
    relevance: CRITICAL
    why: "Complete reverse-engineering of all 3 goal methods, formulas, DB tables, known bugs. Source of truth for calculation parity."

  - doc: docs/specs/046-server-side-goals-forecasting/product-requirements.md
    relevance: CRITICAL
    why: "PRD with all 13 features, 14 business rules, 14 edge cases, and user decisions."

  - doc: docs/patterns/psr4-autoloading.md
    relevance: HIGH
    why: "Namespace conventions for new classes under BuyerKiosk\\."

  - doc: docs/patterns/namespace-structure.md
    relevance: HIGH
    why: "Namespace map for placing new services/controllers."
  ```

- ICO-2 KPI System (existing infrastructure we extend)
  ```yaml
  - file: userfrosting/src/BuyerKiosk/Workbook/KPIService.php
    relevance: CRITICAL
    why: "Primary consumer of goal data. Must integrate with GoalForecastService for enhanced variance calculations."

  - file: userfrosting/src/BuyerKiosk/Workbook/KPIConfig.php
    relevance: HIGH
    why: "Config class pattern to follow. showGoal/showComps visibility settings."

  - file: userfrosting/routes/workbook/kpi.php
    relevance: MEDIUM
    why: "API route pattern for KPI endpoints. Goal API follows same structure."
  ```

- ICO-3 Store Configuration (admin page patterns)
  ```yaml
  - file: userfrosting/src/BuyerKiosk/StoreConfig/Controllers/StoreConfigPageController.php
    relevance: HIGH
    why: "Page controller pattern: constructor($app, Store), checkAccess(), render()."

  - file: userfrosting/src/BuyerKiosk/StoreConfig/Controllers/StoreConfigController.php
    relevance: HIGH
    why: "API controller pattern: checkAuth(), sendJsonResponse(), sendErrorResponse(), lazy service initialization."

  - file: userfrosting/routes/admin/store-config.php
    relevance: HIGH
    why: "Admin route pattern: /admin/:typeNum/feature, validate store, check permissions."

  - file: userfrosting/routes/store-config.php
    relevance: MEDIUM
    why: "API route pattern: REST verbs, store validation closure."
  ```

- ICO-4 Audit System (pattern for goal audit trail)
  ```yaml
  - file: userfrosting/src/BuyerKiosk/Scheduling/Repositories/ShiftAuditRepository.php
    relevance: HIGH
    why: "Audit repository pattern: logCreate/logUpdate/logDelete with oldValueJson/newValueJson."

  - file: userfrosting/src/BuyerKiosk/Auth/Services/AuditLogger.php
    relevance: MEDIUM
    why: "Simple event-based audit pattern. Our audit is more detailed (config snapshots)."
  ```

- ICO-5 TaskEngine (cron job pattern)
  ```yaml
  - file: userfrosting/src/BuyerKiosk/TaskEngine/Domain/Job/BaseJob.php
    relevance: HIGH
    why: "Base class for TaskEngine jobs. GoalForecastComputeJob extends this."

  - file: userfrosting/src/BuyerKiosk/PosDataCatchup/Jobs/PosDataCatchupJob.php
    relevance: HIGH
    why: "Reference per-store job implementation with progress tracking, config, test injection."
  ```

- ICO-6 DRS Integration (Sync app push endpoint)
  ```yaml
  - file: userfrosting/routes/groups/drs.php
    relevance: HIGH
    sections: [lines 1109-1199]
    why: "LiveFinancials upsert where Sync app pushes goals. Must modify to check server-calculated goals before accepting Sync push."
  ```

- ICO-7 Hourly Metrics (historical data for hourly distribution)
  ```yaml
  - file: userfrosting/src/BuyerKiosk/Scheduling/Repositories/HourlyMetricsRepository.php
    relevance: MEDIUM
    why: "Provides getAveragesByDayOfWeek() for learning hourly sales patterns. Source data for hourly goal distribution."
  ```

### Implementation Boundaries

- **Must Preserve**: KPI bar behavior, LiveFinancials table structure, DRS API contract (Sync app can still push), existing `workbook_kpi_config` visibility system
- **Can Modify**: KPIService (extend with variance calculations), DRS route (add server-goal priority check), KPI bar template (add variance expansion)
- **Must Not Touch**: Sync app code (C#/.NET), scheduling solver internals (separate spec), LiveFinancials column structure (add new table instead)
- **Deferred (Could Have)**: Features 11-13 from the PRD (Goal Attainment Report, Sync Import, Cross-Store Benchmarking) are excluded from this SDD. The architecture supports them — goalConfigAudit enables attainment reports, API contract supports Sync import, and central DB config enables cross-store queries — but they are separate implementation efforts.

### External Interfaces

#### System Context Diagram

```mermaid
graph TB
    Admin[Store Admin] -->|Configure goals| GoalSettingsPage[Goal Settings Admin Page]
    Manager[Store Manager] -->|View goals & variance| KPIBar[KPI Bar / Workbook]
    Scheduler[Scheduling Admin] -->|Request forecasts| ScheduleSolver[Scheduling Solver]

    GoalSettingsPage -->|CRUD| GoalAPI[Goal API Routes]
    KPIBar -->|Read| GoalAPI
    ScheduleSolver -->|Forecast query| GoalAPI

    GoalAPI -->|Config R/W| CentralDB[(kiosk_buykiosk)]
    GoalAPI -->|Forecast R/W| StoreDB[(kiosk_xx00)]
    GoalAPI -->|Historical sales| StoreDB

    SyncApp[BuyerKioskSync Desktop] -->|Push goals fallback| DRSRoute[DRS API Route]
    DRSRoute -->|Write if no server goal| StoreDB

    TaskEngine[TaskEngine Cron] -->|Nightly recompute| GoalForecastJob[GoalForecastComputeJob]
    GoalForecastJob -->|Read config| CentralDB
    GoalForecastJob -->|Write forecast| StoreDB

    MobileApp[Mobile Apps] -->|Read goals| GoalAPI
```

#### Interface Specifications

```yaml
# Inbound Interfaces
inbound:
  - name: "Goal Settings Admin Page"
    type: HTTP/HTTPS
    format: Twig HTML + Syncfusion EJ2
    authentication: Session (uri_store_settings permission)
    data_flow: "Admin configures goal method and parameters"

  - name: "Goal API (Web)"
    type: HTTPS
    format: REST JSON
    authentication: Session + checkStoreGroup
    data_flow: "Config CRUD, forecast queries, preview calculations, variance data"

  - name: "Goal API (Mobile)"
    type: HTTPS
    format: REST JSON
    authentication: JWT/HybridAuth
    data_flow: "Read-only forecast and variance data for mobile dashboards"

  - name: "DRS API (Sync App)"
    type: HTTPS
    format: JSON (legacy format)
    authentication: API Key
    data_flow: "Sync app pushes daily goals as fallback. Server-calculated goals take priority."

# Outbound Interfaces
outbound:
  - name: "Central Database"
    type: MariaDB
    connection: PDO
    data_flow: "Goal configuration storage, audit trail, config snapshots"
    criticality: HIGH

  - name: "Store Database"
    type: MariaDB
    connection: PDO (per-store)
    data_flow: "Forecast cache (365 rows), historical sales data for Method 0, hourly metrics"
    criticality: HIGH

  - name: "KPIService Integration"
    type: Internal PHP
    format: Method calls
    data_flow: "GoalForecastService provides daily goals to KPIService for display and variance"
    criticality: HIGH

# Data Interfaces
data:
  - name: "Central DB - Goal Config"
    type: MariaDB (kiosk_buykiosk)
    connection: PDO via dbConnectByName('kiosk_buykiosk')
    data_flow: "goalConfigurations table, goalConfigAudit table"

  - name: "Store DB - Forecast Cache"
    type: MariaDB (kiosk_xx00)
    connection: PDO via dbConnectByName($store->getDbName())
    data_flow: "goalForecast table (365 rows per store)"

  - name: "Store DB - Historical Sales"
    type: MariaDB (kiosk_xx00)
    connection: PDO
    data_flow: "Read dailyCloseReports / LiveFinancials for prior year sales data"
```

### Project Commands

```bash
# Environment Setup
Install Dependencies: cd userfrosting && composer install
Start Development: Local dev served via ngrok to dev2.buyerkiosk.com

# Testing
Unit Tests: ./test.sh --testsuite unit
Integration Tests: ./test.sh --testsuite integration
Targeted Tests: cd userfrosting && ./vendor/bin/phpunit --filter "GoalCalculation"
Static Analysis: cd userfrosting && ./vendor/bin/phpstan analyse src/BuyerKiosk/Goals/

# CSS Build
Development: php userfrosting/conductor build-css
Production: php userfrosting/conductor build-css --minify

# Database Migrations
Run Migrations: php userfrosting/conductor run

# TaskEngine
Start Worker: php userfrosting/bin/task worker:start
Dispatch Job: php userfrosting/bin/task job:dispatch goal-forecast-compute --store=ou00
Queue Status: php userfrosting/bin/task queue:status
```

---

## Solution Strategy

- **Architecture Pattern**: Layered service architecture following existing BuyerKiosk patterns. Four services split by responsibility: Config, Calculation, Forecast, Variance. Controllers handle HTTP, services handle business logic, repositories handle data access.

- **Integration Approach**: Extends existing KPIService by injecting GoalForecastService as a data source for server-calculated goals. Modifies DRS route to check server-calculated goals before accepting Sync pushes. New standalone admin page at `/admin/:typeNum/goals` follows StoreConfig patterns.

- **Justification**: Split services prevent a monolithic class. Existing codebase already uses this pattern (KPIService, StoreHoursService, etc.). TaskEngine job for nightly recompute follows PosDataCatchupJob pattern. Central DB for config + store DB for cache matches the PRD decision and existing dual-DB patterns.

- **Key Decisions**:
  - Syncfusion-heavy UI (Tab, NumericTextBox, Schedule for Method 2 calendar)
  - Server-side AJAX preview (single source of truth for formulas, no JS duplication)
  - Single JSON config table in central DB (flexible, easy to snapshot)
  - One-row-per-day forecast cache with hourly JSON in store DBs
  - Dedicated goalConfigAudit table following ShiftAuditRepository pattern
  - TaskEngine job for nightly forecast recompute

---

## Building Block View

### Components

```mermaid
graph LR
    subgraph "Presentation Layer"
        AdminPage[Goal Settings Admin Page]
        KPIBarExt[KPI Bar Extension]
    end

    subgraph "API Layer"
        GoalRoutes[Goal API Routes]
        DRSMod[DRS Route Modification]
    end

    subgraph "Service Layer"
        ConfigSvc[GoalConfigService]
        CalcEngine[GoalCalculationEngine]
        ForecastSvc[GoalForecastService]
        VarianceSvc[GoalVarianceService]
    end

    subgraph "Data Layer"
        ConfigRepo[GoalConfigRepository]
        ForecastRepo[GoalForecastRepository]
        AuditRepo[GoalConfigAuditRepository]
        HistoryRepo[SalesHistoryRepository]
        HourlyRepo[HourlyMetricsRepository]
    end

    subgraph "Background"
        ForecastJob[GoalForecastComputeJob]
    end

    AdminPage --> GoalRoutes
    KPIBarExt --> GoalRoutes
    GoalRoutes --> ConfigSvc
    GoalRoutes --> ForecastSvc
    GoalRoutes --> VarianceSvc
    DRSMod --> ForecastRepo

    ConfigSvc --> ConfigRepo
    ConfigSvc --> AuditRepo
    ConfigSvc --> CalcEngine

    ForecastSvc --> CalcEngine
    ForecastSvc --> ForecastRepo
    ForecastSvc --> HistoryRepo

    CalcEngine --> HistoryRepo
    CalcEngine --> HourlyRepo

    VarianceSvc --> ForecastRepo
    VarianceSvc --> HistoryRepo

    ForecastJob --> ForecastSvc
```

### Directory Map

```
userfrosting/src/BuyerKiosk/Goals/                          # NEW module
├── Controllers/
│   ├── GoalSettingsPageController.php                       # NEW: Admin page rendering
│   └── GoalApiController.php                                # NEW: REST API endpoints
├── Services/
│   ├── GoalConfigService.php                                # NEW: Config CRUD + audit
│   ├── GoalCalculationEngine.php                            # NEW: Method 0/1/2 formulas
│   ├── GoalForecastService.php                              # NEW: Pre-compute + cache R/W
│   └── GoalVarianceService.php                              # NEW: Period variance calculations
├── Repositories/
│   ├── GoalConfigRepository.php                             # NEW: Central DB config table
│   ├── GoalForecastRepository.php                           # NEW: Store DB forecast cache
│   ├── GoalConfigAuditRepository.php                        # NEW: Central DB audit trail
│   └── SalesHistoryRepository.php                           # NEW: Prior year sales lookups
├── Models/
│   ├── GoalConfiguration.php                                # NEW: Config value object
│   ├── DailyGoal.php                                        # NEW: Computed goal value object
│   └── GoalVariance.php                                     # NEW: Variance result value object
├── Jobs/
│   └── GoalForecastComputeJob.php                           # NEW: TaskEngine nightly job
└── Domain/
    ├── GoalMethod.php                                       # NEW: Enum (METHOD_0, METHOD_1, METHOD_2)
    └── ConfidenceLevel.php                                  # NEW: Enum (HIGH, MEDIUM, LOW)

userfrosting/routes/
├── admin/
│   └── goals.php                                            # NEW: Admin page route
└── groups/
    ├── goals-api.php                                        # NEW: REST API routes
    └── drs.php                                              # MODIFY: Add server-goal priority check

userfrosting/templates/themes/default/
└── admin/
    └── goals/
        ├── settings.html                                    # NEW: Goal settings admin page
        └── partials/
            ├── method0-panel.html                           # NEW: % of Prior Year settings
            ├── method1-panel.html                           # NEW: Annual Target settings
            ├── method2-panel.html                           # NEW: Monthly Calendar settings
            ├── preview-panel.html                           # NEW: Live preview sidebar
            └── audit-history.html                           # NEW: Change history panel

public_html/js/admin/
└── goals/
    ├── goal-settings.js                                     # NEW: Main page controller
    ├── method0-controller.js                                # NEW: Method 0 UI logic
    ├── method1-controller.js                                # NEW: Method 1 UI logic (sum validation)
    ├── method2-controller.js                                # NEW: Method 2 Schedule calendar
    └── preview-controller.js                                # NEW: AJAX preview logic

public_html/css/admin/modules/
└── goals.css                                                # NEW: Goal settings page styles

userfrosting/migrations/input/
├── 20260413_001_goal_configurations_table.json              # NEW: Central DB config table
├── 20260413_002_goal_config_audit_table.json                # NEW: Central DB audit table
├── 20260413_003_goal_forecast_table.json                    # NEW: Store DB forecast cache
└── 20260413_004_goal_hourly_time_bands.json                 # NEW: Store DB time band overrides
```

### Interface Specifications

#### Data Storage Changes

```yaml
# Central Database: kiosk_buykiosk

Table: goalConfigurations (NEW)
  id: INT UNSIGNED AUTO_INCREMENT PRIMARY KEY
  storeId: INT UNSIGNED NOT NULL (FK to stores.id)
  typeNum: VARCHAR(10) NOT NULL
  activeMethod: TINYINT UNSIGNED NOT NULL DEFAULT 0  # 0, 1, or 2
  methodSettings: JSON NOT NULL                       # All 3 methods' parameters
  hourlyTimeBands: JSON DEFAULT NULL                  # Manager time-band adjustments
  updatedAt: DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
  updatedBy: INT UNSIGNED DEFAULT NULL                # users.id
  UNIQUE KEY uk_storeId (storeId)
  UNIQUE KEY uk_typeNum (typeNum)
  INDEX idx_activeMethod (activeMethod)

# methodSettings JSON structure:
# {
#   "method0": {
#     "salesGrowthPct": 5.00,
#     "buysGrowthPct": 5.00
#   },
#   "method1": {
#     "annualTarget": 500000.00,
#     "monthlyPcts": { "jan": 6.50, "feb": 7.00, ... "dec": 6.50 },
#     "dailySalesPcts": { "sun": 13.00, "mon": 10.00, ... "sat": 30.50 },
#     "dailyBuysAmounts": { "sun": 1000.00, "mon": 1000.00, ... "sat": 2000.00 }
#   },
#   "method2": {
#     "configuredMonths": {
#       "2026-10": {
#         "1": { "sales": 5000.00, "buys": 1500.00 },
#         "2": { "sales": 4500.00, "buys": 1200.00 },
#         ... "31": { "sales": 6000.00, "buys": 2000.00 }
#       },
#       "2026-11": {
#         "1": { "sales": 4000.00, "buys": 1000.00 }, ...
#       }
#     },
#     "fallbackMethod": 0  # Auto-set: when switching TO Method 2, this captures the
#                          # previously active method (0 or 1). Updated on each method
#                          # switch TO Method 2. If Method 2 is first configured,
#                          # defaults to 0. Never set to 2 (no self-reference).
#   }
# }

# hourlyTimeBands JSON structure:
# {
#   "bands": [
#     { "label": "Morning", "startHour": 9, "endHour": 12, "weight": 25.0 },
#     { "label": "Midday", "startHour": 12, "endHour": 15, "weight": 35.0 },
#     { "label": "Afternoon", "startHour": 15, "endHour": 18, "weight": 25.0 },
#     { "label": "Evening", "startHour": 18, "endHour": 21, "weight": 15.0 }
#   ]
# }

Table: goalConfigAudit (NEW)
  id: INT UNSIGNED AUTO_INCREMENT PRIMARY KEY
  storeId: INT UNSIGNED NOT NULL
  typeNum: VARCHAR(10) NOT NULL
  actorUserId: INT UNSIGNED NOT NULL
  action: ENUM('create', 'update', 'method_switch') NOT NULL
  changedFields: JSON DEFAULT NULL                    # ["salesGrowthPct", "buysGrowthPct"]
  oldValueJson: JSON DEFAULT NULL                     # Previous values of changed fields
  newValueJson: JSON DEFAULT NULL                     # New values of changed fields
  configSnapshotJson: JSON NOT NULL                   # Full methodSettings at time of change
  calculatedGoalForDate: JSON DEFAULT NULL            # { "date": "2026-04-13", "salesGoal": 5200.00, "buysGoal": 3000.00 }
  occurredAt: DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
  INDEX idx_storeId_occurredAt (storeId, occurredAt DESC)
  INDEX idx_typeNum (typeNum)
  INDEX idx_actorUserId (actorUserId)

# Store Databases: kiosk_xx00

Table: goalForecast (NEW)
  id: INT UNSIGNED AUTO_INCREMENT PRIMARY KEY
  forecastDate: DATE NOT NULL
  salesGoal: DECIMAL(12,2) NOT NULL DEFAULT 0.00
  buysGoal: DECIMAL(12,2) NOT NULL DEFAULT 0.00
  confidence: ENUM('high', 'medium', 'low') NOT NULL DEFAULT 'low'
  goalMethod: TINYINT UNSIGNED NOT NULL DEFAULT 0
  hourlySalesDistribution: JSON DEFAULT NULL           # [0.02, 0.03, 0.04, ... ] (24 floats)
  source: ENUM('server', 'sync_fallback') NOT NULL DEFAULT 'server'
  computedAt: DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
  UNIQUE KEY uk_forecastDate (forecastDate)
  INDEX idx_dateRange (forecastDate, source)
```

#### Internal API Changes

```yaml
# Goal Configuration Endpoints
Endpoint: Get Goal Configuration
  Method: GET
  Path: /api/:typeNum/goals/config
  Auth: Session + checkStoreGroup (view-only for managers)
  Response:
    success: boolean
    config:
      storeId: int
      typeNum: string
      activeMethod: int (0, 1, or 2)
      methodSettings: object (full JSON)
      hourlyTimeBands: object|null
      updatedAt: string (ISO 8601)
      updatedBy: int|null
    hasServerGoals: boolean

Endpoint: Update Goal Configuration
  Method: PUT
  Path: /api/:typeNum/goals/config
  Auth: Session + checkStoreGroup + uri_store_settings (admin only)
  Request:
    activeMethod: int (required, 0|1|2)
    methodSettings: object (required, full settings blob)
    hourlyTimeBands: object (optional)
  Response:
    success: boolean
    config: object (updated config)
    audit:
      auditId: int
      changedFields: string[]
  Validation:
    - Method 1: monthlyPcts must sum to exactly 100.00
    - Method 1: dailySalesPcts must sum to exactly 100.00
    - Method 0: growthPct values must be numeric
    - Method 2: per-day values must be non-negative
  Error:
    400: { success: false, error: string, code: "VALIDATION_ERROR", validationErrors: object[] }
    403: { success: false, error: string, code: "FORBIDDEN" }

Endpoint: Preview Goal Calculation
  Method: POST
  Path: /api/:typeNum/goals/preview
  Auth: Session + checkStoreGroup
  Request:
    method: int (0|1|2)
    settings: object (method-specific parameters)
    dates: string[] (optional, default: today + next 7 days)
  Response:
    success: boolean
    preview:
      - date: string (YYYY-MM-DD)
        dayOfWeek: string
        salesGoal: number
        buysGoal: number
        confidence: string
      - ...
    currentGoals:
      - date: string
        salesGoal: number
        buysGoal: number
      - ...

# Goal Forecast Endpoints
Endpoint: Get Goal Forecast
  Method: GET
  Path: /api/:typeNum/goals/forecast
  Auth: Session + checkStoreGroup OR JWT/HybridAuth
  Query Params:
    startDate: string (YYYY-MM-DD, required)
    endDate: string (YYYY-MM-DD, required)
    includeHourly: boolean (default: false)
  Response:
    success: boolean
    forecast:
      - date: string (YYYY-MM-DD)
        salesGoal: number
        buysGoal: number
        confidence: string (high|medium|low)
        goalMethod: int
        source: string (server|sync_fallback)
        fallbackReason: string|null  # null if no fallback; otherwise: "no_prior_year", "limited_history", "method2_unconfigured"
        hourlyGoals: object|null  # Only if includeHourly=true
          "9": number
          "10": number
          ...
      - ...
    metadata:
      storeTimezone: string
      computedAt: string (ISO 8601)
      totalDays: int
  Error:
    400: { success: false, error: "Date range exceeds 365 days", code: "INVALID_RANGE" }

# Goal Variance Endpoints
Endpoint: Get Goal Variance
  Method: GET
  Path: /api/:typeNum/goals/variance
  Auth: Session + checkStoreGroup
  Query Params:
    periods: string (comma-separated: wtd,mtd,ytd,trailing7,trailing30,trailing90,yoy_wtd,yoy_mtd,yoy_ytd)
  Response:
    success: boolean
    variance:
      wtd:
        goalTotal: number
        actualTotal: number
        varianceDollars: number
        variancePercent: number
        daysIncluded: int
      mtd: { ... }
      ytd: { ... }
      trailing7: { ... }
      trailing30: { ... }
      trailing90: { ... }
      yoy_wtd: { ... }  # Same-period-last-year comparison
      yoy_mtd: { ... }
      yoy_ytd: { ... }
    metadata:
      storeTimezone: string
      asOfDate: string
      weekStartDay: string ("Monday")

# Audit History Endpoint
Endpoint: Get Goal Audit History
  Method: GET
  Path: /api/:typeNum/goals/audit
  Auth: Session + checkStoreGroup + uri_store_settings
  Query Params:
    limit: int (default: 50, max: 200)
    offset: int (default: 0)
    includeSnapshot: boolean (default: false)  # Include full configSnapshotJson
    compareToId: int (optional)                # Return diff between this audit and current config
  Response:
    success: boolean
    audits:
      - id: int
        actorUserId: int
        actorName: string
        action: string
        changedFields: string[]
        oldValues: object
        newValues: object
        calculatedGoalForDate: object|null
        configSnapshot: object|null          # Only when includeSnapshot=true
        occurredAt: string (ISO 8601)
      - ...
    comparison: object|null                  # Only when compareToId is provided
      snapshotConfig: object                 # Config at snapshot time
      currentConfig: object                  # Current live config
      diff: object[]                         # Field-level differences
    total: int
    hasMore: boolean
```

#### Application Data Models

```pseudocode
ENTITY: GoalConfiguration (NEW)
  FIELDS:
    id: int
    storeId: int
    typeNum: string
    activeMethod: int (0|1|2)
    methodSettings: array (decoded JSON)
    hourlyTimeBands: array|null
    updatedAt: DateTime
    updatedBy: int|null

  BEHAVIORS:
    getMethod0Settings(): array { salesGrowthPct, buysGrowthPct }
    getMethod1Settings(): array { annualTarget, monthlyPcts, dailySalesPcts, dailyBuysAmounts }
    getMethod2Settings(): array { configuredMonths, fallbackMethod }
    getActiveMethodSettings(): array  # Returns settings for activeMethod only
    toArray(): array
    toJson(): string

ENTITY: DailyGoal (NEW)
  FIELDS:
    date: DateTime
    salesGoal: float
    buysGoal: float
    confidence: string (high|medium|low)
    goalMethod: int
    hourlySalesDistribution: float[] (24 elements)
    source: string (server|sync_fallback)
    computedAt: DateTime

  BEHAVIORS:
    getHourlyGoal(int hour): float  # salesGoal * hourlySalesDistribution[hour]
    toArray(): array

ENTITY: GoalVariance (NEW)
  FIELDS:
    period: string (wtd|mtd|ytd|trailing7|trailing30|trailing90)
    goalTotal: float
    actualTotal: float
    varianceDollars: float
    variancePercent: float
    daysIncluded: int
    startDate: DateTime
    endDate: DateTime

  BEHAVIORS:
    isOnTrack(): bool  # actualTotal >= goalTotal
    getStatus(): string  # good (>=100%), warning (90-99%), danger (<90%)
    toArray(): array
```

#### Integration Points

```yaml
# Internal Integration
- from: KPIService
  to: GoalForecastService
  protocol: PHP method call
  data_flow: "KPIService calls GoalForecastService::getDailyGoal(date) to get server-calculated goals instead of only reading LiveFinancials"

- from: DRS Route (drs.php)
  to: GoalForecastRepository
  protocol: PHP method call
  data_flow: "Before writing Sync-pushed goals to LiveFinancials, check GoalForecastRepository::hasServerGoal(date). If yes, skip Sync push."

- from: GoalForecastComputeJob
  to: GoalForecastService
  protocol: PHP method call
  data_flow: "Nightly job calls GoalForecastService::recomputeFullYear(store) for each store"

- from: GoalConfigService
  to: TaskEngine (via GoalForecastComputeJob)
  protocol: Job dispatch
  data_flow: "After config save, GoalConfigService dispatches GoalForecastComputeJob (high priority) to recompute forecast asynchronously"

# External Integration
- from: Scheduling Solver (specs 026/038)
  to: Goal Forecast API
  protocol: HTTPS REST
  endpoints: [GET /api/:typeNum/goals/forecast]
  data_flow: "Solver requests daily + hourly goals for scheduling period"

- from: Mobile Apps
  to: Goal Forecast API
  protocol: HTTPS REST (JWT auth)
  endpoints: [GET /api/:typeNum/goals/forecast, GET /api/:typeNum/goals/variance]
  data_flow: "Mobile dashboards display goals and variance"
```

### Implementation Examples

#### Example: Goal Calculation Engine (Method 0)

**Why this example**: The prior-year day-of-week matching algorithm is the most complex business logic and the most likely source of parity errors with the Sync app.

```php
// GoalCalculationEngine::calculateMethod0(DateTime $targetDate, array $settings, Store $store): DailyGoal

// Step 1: Find matching prior year date (same day-of-week, same week-of-month)
// "3rd Saturday of October 2026" → "3rd Saturday of October 2025"
$targetDayOfWeek = (int) $targetDate->format('N');  // 1=Mon, 7=Sun
$targetWeekOfMonth = $this->getWeekOfMonth($targetDate);

$priorYearMonth = (clone $targetDate)->modify('-1 year');
$firstDayOfPriorMonth = new DateTime($priorYearMonth->format('Y-m-01'));

// Find the Nth occurrence of this day-of-week in the prior year's same month
$matchDate = $this->findNthDayOfWeekInMonth(
    $firstDayOfPriorMonth,
    $targetDayOfWeek,
    $targetWeekOfMonth
);

// Leap year offset: +2 days when crossing leap year boundary
if ($this->crossesLeapYearBoundary($targetDate, $matchDate)) {
    $matchDate->modify('+2 days');
}

// Step 2: Get prior year sales data
$priorSales = $this->salesHistoryRepo->getSalesForDate($store, $matchDate);

// Step 3: Apply growth percentages (BUG FIX: separate sales/buys growth)
$salesGoal = $priorSales['netSalesRetail'] * (1 + $settings['salesGrowthPct'] / 100);
$buysGoal  = $priorSales['buysCost'] * (1 + $settings['buysGrowthPct'] / 100);

// Step 4: Determine confidence
$confidence = $this->determineConfidence($store, $matchDate, $priorSales);

return new DailyGoal(
    date: $targetDate,
    salesGoal: round($salesGoal, 2),
    buysGoal: round($buysGoal, 2),
    confidence: $confidence,
    goalMethod: 0
);
```

#### Example: Hourly Distribution Interpolation

**Why this example**: The time-band → hourly interpolation is a new algorithm not in the Sync app. It needs clear documentation.

```php
// GoalCalculationEngine::distributeGoalToHours(float $dailyGoal, string $dayOfWeek, Store $store): float[]

// Step 1: Get historical hourly sales pattern (90-day lookback)
$historicalPattern = $this->hourlyMetricsRepo->getAveragesByDayOfWeek(
    $store, $dayOfWeek, lookbackDays: 90
);
// Returns: [0 => 0.0, 1 => 0.0, ..., 9 => 120.50, 10 => 340.00, ..., 23 => 0.0]

// Step 2: Normalize to percentages
$totalSales = array_sum($historicalPattern);
$baseWeights = array_map(fn($v) => $totalSales > 0 ? $v / $totalSales : 1/24, $historicalPattern);
// Now sums to 1.0

// Step 3: Apply manager time-band adjustments (if any)
$timeBands = $this->configRepo->getTimeBands($store);
if ($timeBands !== null) {
    $adjustedWeights = $this->interpolateTimeBands($baseWeights, $timeBands);
    // Redistributes weights within each band while preserving the historical
    // shape WITHIN the band. Band-level percentages set the envelope;
    // historical pattern sets the intra-band distribution.
} else {
    $adjustedWeights = $baseWeights;
}

// Step 4: Calculate hourly goals
$hourlyGoals = array_map(fn($w) => round($dailyGoal * $w, 2), $adjustedWeights);

return $hourlyGoals; // 24-element array
```

#### Example: DRS Route Server-Goal Priority Check

**Why this example**: This is the critical integration point where we modify existing behavior. Wrong implementation could break Sync app compatibility.

```php
// In routes/groups/drs.php, BEFORE the existing LiveFinancials upsert:

// Check if server has already calculated goals for today
$forecastRepo = new GoalForecastRepository($storeDB);
$serverGoal = $forecastRepo->getByDate($today);

if ($serverGoal !== null && $serverGoal['source'] === 'server') {
    // Server-calculated goal exists — DON'T overwrite with Sync push
    // Still update actuals (salesCurrent, buysCurrent, etc.) but preserve server goals
    $goalOverrides = [
        'buysGoal' => $serverGoal['buysGoal'],
        'salesGoal' => $serverGoal['salesGoal'],
    ];
    // Log that we blocked a Sync push
    error_log("Goals: Blocked Sync push for {$typeNum} on {$today->format('Y-m-d')} — server goal exists");
} else {
    // No server goal — accept Sync push as fallback
    $goalOverrides = null;

    // Also write to forecast cache as sync_fallback source
    $forecastRepo->upsert([
        'forecastDate' => $today->format('Y-m-d'),
        'salesGoal' => $parsedSalesGoal,
        'buysGoal' => $parsedBuysGoal,
        'confidence' => 'high',  // Sync app calculated from real data
        'goalMethod' => -1,      // Sync-originated, method unknown
        'source' => 'sync_fallback',
        'computedAt' => (new DateTime())->format('Y-m-d H:i:s'),
    ]);
}
```

---

## Runtime View

### Primary Flow: Goal Configuration Save

1. Admin navigates to `/admin/:typeNum/goals`
2. System loads current config from `goalConfigurations` table via GoalConfigRepository
3. Template renders with Syncfusion Tab (3 method panels) and current values pre-filled
4. Admin adjusts parameters (e.g., changes Method 0 sales growth from 5% to 8%)
5. Admin clicks "Preview" → JS sends POST `/api/:typeNum/goals/preview` with proposed settings
6. Server calculates today + 7 days under proposed settings, returns preview JSON
7. Preview panel updates with side-by-side comparison (current vs proposed)
8. Admin clicks "Save" → JS sends PUT `/api/:typeNum/goals/config` with full settings
9. GoalConfigService validates inputs (percentage sums, required fields)
10. GoalConfigService creates audit entry with old/new values and config snapshot
11. GoalConfigService dispatches GoalForecastComputeJob via TaskEngine (high priority queue)
12. Response returns success with updated config immediately (recompute happens async in background)
13. Background job deletes existing server-computed forecast rows, recomputes 365 days, inserts new rows
14. Until recompute completes (~5s), prior cached values remain available (stale but non-zero)

```mermaid
sequenceDiagram
    actor Admin
    participant Page as Goal Settings Page
    participant API as Goal API Controller
    participant Config as GoalConfigService
    participant Audit as GoalConfigAuditRepo
    participant Calc as GoalCalculationEngine
    participant Forecast as GoalForecastService
    participant CentralDB as Central DB
    participant StoreDB as Store DB

    Admin->>Page: Adjust parameters
    Admin->>Page: Click "Preview"
    Page->>API: POST /goals/preview
    API->>Calc: calculate(method, settings, dates[])
    Calc->>StoreDB: Query prior year sales
    Calc-->>API: DailyGoal[]
    API-->>Page: Preview JSON
    Page->>Page: Render side-by-side comparison

    Admin->>Page: Click "Save"
    Page->>API: PUT /goals/config
    API->>Config: updateConfig(storeId, settings)
    Config->>Config: Validate inputs
    Config->>CentralDB: UPDATE goalConfigurations
    Config->>Audit: logUpdate(oldConfig, newConfig, snapshot)
    Audit->>CentralDB: INSERT goalConfigAudit
    Config->>Forecast: enqueueRecompute(store)
    Forecast->>TaskEngine: Dispatch GoalForecastComputeJob (high priority)
    Forecast-->>Config: Recompute queued
    Config-->>API: Updated config + recomputeStatus: 'queued'
    API-->>Page: Success response
    Page->>Page: Show success toast ("Settings saved. Forecast updating...")

    Note over TaskEngine,StoreDB: Async recompute (background)
    TaskEngine->>Forecast: recomputeFullYear(store)
    Forecast->>StoreDB: DELETE FROM goalForecast WHERE source='server'
    Forecast->>Calc: calculate(365 dates)
    Calc->>StoreDB: Query historical data
    Calc-->>Forecast: DailyGoal[]
    Forecast->>StoreDB: BATCH INSERT goalForecast
```

### Secondary Flow: KPI Bar Goal Display

1. Workbook page loads, KPI bar requests `/api/:typeNum/workbook/kpi/`
2. KPIService calls GoalForecastService::getDailyGoal(today)
3. GoalForecastService checks goalForecast table for today's row
4. If found (source=server): returns server-calculated goal
5. If found (source=sync_fallback): returns Sync-pushed goal with fallback indicator
6. If not found AND store has goalConfigurations: triggers on-demand calculation for today, caches result, returns it (self-healing cache miss)
7. If not found AND store has NO goalConfigurations: returns null (KPIService falls back to LiveFinancials.salesGoal as-is — store hasn't configured server goals yet)
8. KPI bar displays goal with source indicator badge

### Error Handling

- **Invalid input (percentage sums != 100)**: 400 response with `validationErrors` array. Frontend highlights invalid fields in red, shows specific error messages.
- **Invalid input ($0 annual target)**: Accepted as valid. All daily goals calculate to $0. Variance calculations handle the zero-goal case: variance percentage shows "N/A" (not divide-by-zero). Variance dollars still show actual as positive "over goal."
- **Missing historical data (Method 0)**: Calculation engine cascades through fallback tiers. Returns goal with `confidence: 'low'`. UI shows "Limited historical data" notice.
- **Database connection failure**: Services catch PDOException, log error, return graceful fallback. KPI bar falls back to LiveFinancials. Forecast API returns 503 with retry-after.
- **Concurrent edits**: Last save wins. Both saves recorded in audit trail. No optimistic locking needed (goal settings change infrequently).
- **TaskEngine job failure**: Job returns JobResult::failure with error details. Next nightly run retries. Stale forecast cache still serves last-computed values.
- **Store timezone not configured**: Falls back to America/New_York with warning logged. Confidence level includes timezone caveat.
- **Feb 29 target date**: When target date is Feb 29 and prior year is not a leap year, match to Feb 28 of the prior year. Confidence downgrades by one level (high→medium, medium→low) to flag the imprecise match.
- **Negative/voided sales in variance**: Actuals can be negative (returns exceed sales). Variance dollars = actual - goal (negative result). Variance percentage = `(actual - goal) / goal * 100` when goal > 0; when goal = 0, percentage shows "N/A" to avoid divide-by-zero. Goals are never negative — they represent targets.
- **Store closures**: No automatic holiday detection. Method 2 calendar allows manual $0 override for known closures. Method 0/1 calculate the normal goal for closed days (no system-level closure data source). Variance for unplanned closures shows the full shortfall against goal. A future enhancement could integrate with store hours/holiday calendar.

### Complex Logic: Fallback Cascade

```
ALGORITHM: Calculate Method 0 Goal with Fallback
INPUT: targetDate, salesGrowthPct, buysGrowthPct, store
OUTPUT: DailyGoal with confidence level

1. FIND matching prior year date (day-of-week + week-of-month)
2. QUERY prior year sales from dailyCloseReports or LiveFinancials
3. IF prior year data found AND non-zero:
     confidence = 'high'
     salesGoal = priorSales * (1 + salesGrowthPct/100)
     buysGoal  = priorBuys * (1 + buysGrowthPct/100)
     IF priorSales was $0 (anomalous): confidence = 'medium'
4. ELSE IF 3-11 months of same-day-of-week data available:
     confidence = 'medium'
     avgSales = AVERAGE of available same-day-of-week sales
     avgBuys  = AVERAGE of available same-day-of-week buys
     salesGoal = avgSales * (1 + salesGrowthPct/100)
     buysGoal  = avgBuys * (1 + buysGrowthPct/100)
5. ELSE (< 3 months data):
     confidence = 'low'
     FALL BACK to Method 1 with system defaults
     salesGoal = calculateMethod1(targetDate, DEFAULT_METHOD1_SETTINGS)
     buysGoal  = DEFAULT_BUYS_FOR_DAY_OF_WEEK[targetDate.dayOfWeek]
6. RETURN DailyGoal(targetDate, salesGoal, buysGoal, confidence)
```

---

## Deployment View

### Single Application Deployment

- **Environment**: Existing PHP application on local dev machine served via ngrok to dev2.buyerkiosk.com. No separate deployment needed — code changes are live immediately.
- **Configuration**: No new environment variables required. Uses existing database connections and Redis.
- **Dependencies**: Existing Syncfusion EJ2 CDN for UI components. Existing TaskEngine infrastructure for cron job.
- **Performance**:
  - Forecast cache reads: < 5ms (simple date range query on indexed table)
  - Preview calculations: < 100ms (7-day calculation is lightweight)
  - Full 365-day recompute: < 5s per store (bound by historical data lookups)
  - Nightly job for 200 stores: < 20 minutes total

### Database Migrations

- **Deployment Order**:
  1. Central DB migration: Create `goalConfigurations` and `goalConfigAudit` tables
  2. Store DB migration: Create `goalForecast` table in all store databases
  3. Deploy PHP code (services, controllers, routes, templates)
  4. Register TaskEngine job definition
  5. First manual job dispatch to seed forecast cache

- **Rollback Strategy**: Migrations use `check_query` to skip if already applied. PHP code can be reverted. goalForecast table is a cache — dropping it doesn't lose data (config is in central DB, cache rebuilds on next job run).

---

## Cross-Cutting Concepts

### System-Wide Patterns

- **Security**: Admin edit requires `uri_store_settings` permission. Manager view requires `checkStoreGroup`. All API endpoints validate store access. CSRF protection on admin page form submissions.
- **Error Handling**: Services throw typed exceptions. Controllers catch and map to HTTP status codes. All errors logged with `error_log()`. PDO exceptions caught with graceful fallbacks.
- **Performance**: Forecast data pre-computed and cached in store DB. Redis caching for hot-path reads (KPI bar polling). Preview endpoint debounced on frontend (300ms).
- **Logging/Auditing**: All config changes recorded in goalConfigAudit with full snapshots. Goal source (server/sync_fallback) tracked per forecast row.

### Implementation Patterns

#### Code Patterns and Conventions

- PSR-4 autoloading under `BuyerKiosk\Goals\` namespace
- Services follow constructor dependency injection (Store object, PDO connections)
- Repositories handle all SQL — services never write raw SQL
- Controllers use `sendJsonResponse()` / `sendErrorResponse()` helpers from StoreConfigController pattern
- Value objects (GoalConfiguration, DailyGoal, GoalVariance) are immutable with `toArray()` methods

#### State Management Patterns

- **Server state**: Goal configuration persisted in central DB. Forecast cache in store DB. Audit trail append-only.
- **Frontend state**: Syncfusion component instances managed per-tab. Preview data held in JS controller. No global state — page reloads on save.
- **Cache invalidation**: Config save triggers immediate forecast recompute. Nightly job refreshes all stores. Redis cache for KPI bar expires after 30 seconds (existing TTL).

#### Component Structure Pattern

```pseudocode
COMPONENT: GoalSettingsPage
  INITIALIZE:
    typeNum from meta tag
    Syncfusion Tab for method selector
    Per-method controllers (Method0Controller, Method1Controller, Method2Controller)
    PreviewController for side-by-side preview panel

  ON TAB CHANGE:
    Activate selected method controller
    Initialize Syncfusion components for that tab (destroy + recreate pattern)
    Load current settings into form fields

  ON PREVIEW:
    Collect current form values from active method controller
    POST to /api/:typeNum/goals/preview
    Render response in preview panel (current vs proposed, 7-day forecast)

  ON SAVE:
    Validate locally (percentage sums, required fields)
    PUT to /api/:typeNum/goals/config
    IF success: Show toast, refresh preview
    IF validation error: Highlight fields, show error messages
```

#### Error Handling Pattern

```pseudocode
FUNCTION: handleGoalApiError(operation, error)
  CLASSIFY:
    PDOException → 500 SERVER_ERROR, log details
    InvalidArgumentException → 400 VALIDATION_ERROR, return field-level errors
    PermissionException → 403 FORBIDDEN
    NotFoundException → 404 NOT_FOUND

  LOG: error_log("GoalApiController::{operation} error: {error.message}")

  RESPOND:
    { success: false, error: user-safe message, code: error_code }
    Include validationErrors[] for 400s
```

#### Test Pattern

```pseudocode
TEST_SCENARIO: "Method 0 calculates goal with correct growth percentage"
  SETUP:
    Create mock SalesHistoryRepository returning known prior year sales ($10,000)
    Create GoalCalculationEngine with mock repo
    Set salesGrowthPct = 5.0, buysGrowthPct = 10.0

  EXECUTE:
    result = engine.calculateMethod0(targetDate, settings, store)

  VERIFY:
    result.salesGoal == 10500.00  (10000 * 1.05)
    result.buysGoal  == priorBuys * 1.10  (uses buysGrowthPct, NOT salesGrowthPct)
    result.confidence == 'high'
    result.goalMethod == 0

TEST_SCENARIO: "Method 0 fallback cascade with no history"
  SETUP:
    Mock SalesHistoryRepository returning empty for all lookups
    Create GoalCalculationEngine

  EXECUTE:
    result = engine.calculateMethod0(targetDate, settings, store)

  VERIFY:
    result.confidence == 'low'
    result.salesGoal > 0  (fell back to Method 1 defaults)
    result.goalMethod == 0  (still reports Method 0 as the configured method)

TEST_SCENARIO: "Percentage validation rejects non-100 sums"
  SETUP:
    settings.monthlyPcts = { jan: 10.0, feb: 10.0, ... }  // sum = 98.0

  EXECUTE:
    result = configService.validateSettings(settings)

  VERIFY:
    result.valid == false
    result.errors includes "Monthly percentages must sum to exactly 100.00%"

TEST_SCENARIO: "DRS route blocks Sync push when server goal exists"
  SETUP:
    Insert server-calculated goal in goalForecast for today
    Simulate DRS POST with Sync-pushed goals

  VERIFY:
    LiveFinancials.salesGoal unchanged (server goal preserved)
    LiveFinancials.salesCurrent updated (actuals always update)
    Log entry records blocked Sync push
```

---

## Architecture Decisions

- [x] ADR-1 **Syncfusion-heavy UI**: Use Syncfusion Tab, NumericTextBox, and Schedule (month view) for the goal settings page.
  - Rationale: Better input validation UX (built-in min/max/step), consistent with project directive to prefer Syncfusion. Schedule provides month navigation and day-cell editing for Method 2.
  - Trade-offs: CDN dependency, Syncfusion-specific patterns (destroy/recreate on tab switch, hidden container gotchas per MEMORY.md).
  - User confirmed: Yes

- [x] ADR-2 **Server-side AJAX preview**: All goal calculations happen on the server. Preview sends parameters to POST `/goals/preview` endpoint.
  - Rationale: Single source of truth for formulas. No risk of JS/PHP calculation divergence. ~100ms latency is acceptable with debounce.
  - Trade-offs: Network round-trip on each parameter change. Mitigated by 300ms debounce.
  - User confirmed: Yes

- [x] ADR-3 **Single JSON config table**: One `goalConfigurations` row per store with `methodSettings` JSON column containing all 3 methods' parameters.
  - Rationale: Flexible schema accommodates all methods. Easy to snapshot (copy the JSON blob to audit table). Easy to extend with new methods later.
  - Trade-offs: Cannot query individual settings with SQL WHERE clauses (must parse JSON or use JSON_EXTRACT). Acceptable because config is always loaded as a complete object.
  - User confirmed: Yes

- [x] ADR-4 **One-row-per-day forecast cache with hourly JSON**: `goalForecast` table in store DB with 365 rows per store. Hourly distribution stored as JSON array of 24 floats.
  - Rationale: Hourly data always consumed as a complete set (never queried by individual hour). 365 rows per store is trivial. JSON array is ~200 bytes. No JOIN overhead.
  - Trade-offs: Cannot SQL-filter by individual hour. Acceptable — hourly data is decoded in PHP/JS before use.
  - User confirmed: Yes

- [x] ADR-5 **Dedicated goalConfigAudit table**: Follows ShiftAuditRepository pattern with oldValueJson, newValueJson, and configSnapshotJson columns.
  - Rationale: Full audit trail with config snapshots enables retrospective analysis. Matches existing audit patterns.
  - Trade-offs: Storage grows with each config change. Negligible at expected change frequency (monthly).
  - User confirmed: Yes

- [x] ADR-6 **TaskEngine job for nightly recompute**: Per-store scope job following PosDataCatchupJob pattern.
  - Rationale: Progress tracking, abort support, structured logging. Fits existing infrastructure. Scheduler dispatches nightly.
  - Trade-offs: More complex than simple cron script. Worth it for observability and error recovery.
  - User confirmed: Yes

- [x] ADR-7 **Four-service architecture**: GoalConfigService, GoalCalculationEngine, GoalForecastService, GoalVarianceService.
  - Rationale: Single Responsibility Principle. Config CRUD is separate from calculation math. Forecast caching is separate from variance analysis. Each service is testable in isolation.
  - Trade-offs: More files to navigate. Mitigated by clear naming and directory structure.
  - User confirmed: Yes

- [x] ADR-8 **New route group /api/:typeNum/goals/**: Dedicated namespace for all goal endpoints.
  - Rationale: Clear API boundary. Not mixed with KPI endpoints. Easier to document and version independently.
  - Trade-offs: Additional route file. Negligible overhead.
  - User confirmed: Yes

- [x] ADR-9 **Syncfusion Schedule for Method 2 calendar**: Month view with inline dollar values in day cells.
  - Rationale: Built-in month navigation, day-cell rendering, responsive layout. Inline editing avoids extra click.
  - Trade-offs: Schedule component is designed for events, not data entry. Need custom `cellTemplate` and `cellClick` handlers. Must handle day cells that aren't operating days.
  - User confirmed: Yes

- [x] ADR-10 **Full page with smart defaults (no wizard)**: Pre-fill Method 0 with 5% growth on first visit. Contextual help via tooltips.
  - Rationale: Managers coming from Sync app already understand the concepts. Wizard adds UX complexity without proportional value.
  - Trade-offs: First-time users see a full form. Mitigated by sensible defaults and tooltip help.
  - User confirmed: Yes

---

## Quality Requirements

- **Performance**: Forecast API < 200ms for 7-day range. Full 365-day recompute < 5s per store. Preview endpoint < 100ms.
- **Usability**: Method 1 percentage fields validate sum in real-time (red indicator if != 100%). Method 2 calendar pre-fills from active method. Save disabled until validation passes.
- **Security**: Admin-only config editing. Manager read-only access. CSRF protection on forms. All inputs sanitized before SQL.
- **Reliability**: Forecast cache is self-healing — nightly job rebuilds all 365 days; cache misses trigger on-demand single-day calculation and cache write. Config saves dispatch async recompute via TaskEngine (high priority queue, ~5s). During recompute, prior cached values remain available. Sync app fallback ensures goals always available even if server computation fails. Audit trail is append-only (never deleted).
- **Data Integrity**: Percentage validations enforced both client-side (Syncfusion) and server-side (PHP). Rounding to 2 decimal places with half-up. Transactions for multi-row operations.
- **Display Formatting**: Daily/hourly goals displayed as whole dollars (no cents) in KPI bar and admin page (e.g., "$8,388"). Variance amounts displayed with cents (e.g., "-$1,234.56"). Variance percentages displayed to 1 decimal place (e.g., "-12.3%"). When goal is $0, variance percentage shows "N/A" instead of divide-by-zero. API responses always return full precision (2 decimal places); UI formatting is client-side.

---

## Risks and Technical Debt

### Known Technical Issues

- KPIService currently reads goals only from LiveFinancials. Must be modified to also check goalForecast table, with clear priority order.
- DRS route uses `preg_replace('/\D/', '', $value)` to parse currency strings. This strips decimal points, storing values as integers (cents). The goalForecast table uses DECIMAL(12,2) — must align formats.

### Technical Debt

- The Sync app's Method 0 has a known bug (buys uses sales growth %). Our server implementation fixes this. Parity tests must account for intentional divergence.
- Method 2 was never completed in the Sync app. Our implementation is new functionality, not a port. No parity testing possible for Method 2.

### Implementation Gotchas

- **Syncfusion Schedule in month view**: Day cells need custom `cellTemplate` to show dollar inputs. The Schedule component fires `cellClick` events — must prevent default behavior (opening event editor) and instead activate inline editing.
- **Syncfusion hidden tab initialization**: Per MEMORY.md, Syncfusion components initialized in hidden tabs get null errors. Initialize Method 1 and Method 2 Syncfusion components only when their tab becomes active (destroy + recreate pattern).
- **PDO named parameter reuse**: Per MEMORY.md, PDO doesn't allow reusing named params. Use unique names (`:salesGrowth1`, `:salesGrowth2`) and bind the same value to both.
- **LiveFinancials goal values are integers** (currency in cents, stripped of formatting). GoalForecast uses DECIMAL(12,2). Ensure consistent formatting when KPIService reads from either source.
- **Store timezone**: Use `$store->getTimezone()` for all date calculations. Never use server time or UTC for "today" determination.

---

## Test Specifications

### Critical Test Scenarios

**Scenario 1: Method 0 Parity with Sync App**
```gherkin
Given: A store with known prior year sales data ($10,000 net sales on 3rd Saturday of October 2025)
And: Sales growth set to 5%, Buys growth set to 10%
When: Server calculates goal for 3rd Saturday of October 2026
Then: Sales goal = $10,500.00 (10000 * 1.05)
And: Buys goal uses buys growth 10% (NOT sales growth 5% — verifying bug fix)
And: Confidence = 'high'
```

**Scenario 2: Method 1 Annual Distribution**
```gherkin
Given: Annual target = $1,000,000, October = 11%, Saturday = 30.5%, 4 Saturdays in October
When: Server calculates goal for a Saturday in October
Then: Monthly = $110,000
And: Daily Saturday = $110,000 * 0.305 / 4 = $8,387.50
And: Confidence = 'high' (Method 1 is deterministic, no history needed)
```

**Scenario 3: Method 1 Percentage Validation**
```gherkin
Given: Monthly percentages summing to 98.5%
When: Admin attempts to save configuration
Then: Validation error returned with code VALIDATION_ERROR
And: Error message specifies "Monthly percentages must sum to exactly 100.00%, currently 98.50%"
And: Configuration is NOT saved
```

**Scenario 4: Fallback Cascade (New Store)**
```gherkin
Given: A new store with zero historical sales data
And: Method 0 selected with 5% growth
When: Server calculates goal for any date
Then: Goal falls back to Method 1 with system defaults
And: Confidence = 'low'
And: API response includes fallbackReason field
```

**Scenario 5: Server Goal Blocks Sync Push**
```gherkin
Given: Server has computed today's goal (source='server', salesGoal=$5,000)
When: Sync app pushes goals via DRS route (salesGoal=$4,500)
Then: LiveFinancials.salesGoal remains $5,000 (server wins)
And: LiveFinancials.salesCurrent is updated with actual sales from Sync push
And: Log entry records blocked Sync push
```

**Scenario 6: Config Save Triggers Recompute**
```gherkin
Given: Store has 365 forecast rows computed
When: Admin changes Method 0 growth from 5% to 10% and saves
Then: Audit entry created with old=5%, new=10%, full config snapshot
And: All 365 forecast rows deleted and recomputed with new growth %
And: KPI bar shows updated goal on next refresh
```

**Scenario 7: Forecast API Date Range**
```gherkin
Given: Forecast cache populated for next 365 days
When: API called with startDate=2026-04-20, endDate=2026-04-26, includeHourly=true
Then: Response contains 7 days of forecasted goals
And: Each day includes hourlyGoals object with 24 hour entries
And: Response time < 200ms
```

**Scenario 8: Method 2 Unconfigured Month Fallback**
```gherkin
Given: Method 2 active, October configured with manual targets, November NOT configured
When: Server forecasts November goals
Then: November goals calculated using store's fallbackMethod (Method 0 or 1)
And: October goals use the manually configured per-day values
And: Confidence for November reflects the fallback method's data quality
```

### Test Coverage Requirements

- **Business Logic**: All 3 calculation methods, fallback cascade, confidence determination, hourly distribution, percentage validation, leap year handling
- **User Interface**: Tab switching preserves state, NumericTextBox validation, Schedule calendar inline editing, preview updates, audit history display
- **Integration Points**: DRS route server-goal priority, KPIService integration, TaskEngine job execution, variance calculation accuracy
- **Edge Cases**: Zero history, $0 prior year sales, leap year dates, DST transitions, negative actuals, concurrent edits, Method 2 partial month configuration
- **Performance**: Forecast API response time, 365-day recompute duration, preview endpoint latency
- **Security**: Permission checks (admin vs manager), CSRF validation, input sanitization

---

## Glossary

### Domain Terms

| Term | Definition | Context |
|------|------------|---------|
| Method 0 | % of Prior Year — calculates goals based on same-day-of-week from previous year + growth percentage | Primary goal calculation method, most commonly used |
| Method 1 | Annual Target Distribution — distributes annual dollar target across months and days-of-week using percentages | Used by stores wanting seasonal control over goal distribution |
| Method 2 | Monthly Target Calendar — manual per-day goal entry with smart prefill from Method 0/1 | Used for irregular sales patterns (events, promotions) |
| Confidence Level | Indicator (high/medium/low) of how much historical data backs a calculated goal | Exposed to scheduling solver and UI for decision-making |
| Over/Short | Variance between goal and actual performance, expressed in dollars and percentage | Displayed in KPI bar and variance reports |
| Time Bands | Manager-defined hourly groupings (e.g., Morning, Midday, Afternoon, Evening) with weight percentages | Used to adjust hourly goal distribution from historical patterns |

### Technical Terms

| Term | Definition | Context |
|------|------------|---------|
| goalForecast | Pre-computed cache table in store DB containing 365 days of daily goals | Read by KPI bar, forecast API, and scheduling solver |
| goalConfigurations | Central DB table storing goal method and settings per store | Single row per store with JSON methodSettings |
| goalConfigAudit | Central DB table recording all config changes with full JSON snapshots | Append-only audit trail for accountability |
| Sync Fallback | When server hasn't calculated goals and the Sync app's pushed values are used | Indicated by source='sync_fallback' in goalForecast |
| Forecast Window | 365-day rolling window of pre-computed daily goals | Refreshed nightly by TaskEngine job |

### API/Interface Terms

| Term | Definition | Context |
|------|------------|---------|
| typeNum | Store identifier pattern [a-z][a-z]\d+ (e.g., ou00, pc00) | Used in all API paths and store scoping |
| source | Enum field indicating whether a goal was 'server'-calculated or 'sync_fallback' | Displayed as badge in KPI bar |
| includeHourly | Query parameter that adds hourly distribution to forecast response | Used by scheduling solver for demand-based staffing |
