# 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 & Language Requirements**
- PHP 8.x with Slim 2.6.2 framework
- Twig 1.44.8 templating
- Bootstrap 5.3.3 CSS framework with custom design tokens
- MySQL database with multi-store architecture (central + per-store DBs)

**CON-2 Coding Standards & Patterns**
- PSR-4 autoloading under `BuyerKiosk\` namespace
- Controller pattern: PageController for rendering, Controller for API endpoints
- Route pattern: `/admin/:typeNum/{feature}` for pages, `/api/:typeNum/{feature}` for APIs
- JSON migrations in `userfrosting/migrations/input/` directory

**CON-3 Auth & Data Requirements**
- Three-layer auth: Login → Permission (`uri_store_settings`) → Store group access
- Store hours stored in central database (`kiosk_buykiosk.stores`)
- Redis caching (1hr TTL) for Store objects - must invalidate on config change
- Times stored as wall-clock (HH:MM format), not UTC timestamps

**CON-4 Browser & Performance**
- Modern browsers (Chrome, Firefox, Safari, Edge - latest 2 versions)
- Page load target: <2s for configuration page
- API response target: <500ms for config read/write

## Implementation Context

### Required Context Sources

```yaml
# Internal documentation and patterns
- doc: CLAUDE.md
  relevance: HIGH
  why: "Project commands, coding standards, architecture overview"

- doc: docs/patterns/psr4-autoloading.md
  relevance: MEDIUM
  why: "Namespace and autoloading patterns for new classes"

# Source code files that must be understood
- file: userfrosting/src/BuyerKiosk/Core/Store.php
  relevance: CRITICAL
  sections: [properties, getters/setters, createStoreFromRowArray]
  why: "Store entity - will be extended with hours properties"

- file: userfrosting/src/BuyerKiosk/Scheduling/Controllers/SchedulingController.php
  relevance: HIGH
  sections: [getConfig(), updateConfig()]
  why: "Reference pattern for config API endpoints"

- file: userfrosting/src/BuyerKiosk/Scheduling/Controllers/SchedulingPageController.php
  relevance: HIGH
  sections: [settings()]
  why: "Reference pattern for admin settings page rendering"

- file: userfrosting/routes/admin/scheduling.php
  relevance: HIGH
  why: "Route definition pattern for admin pages"

- file: userfrosting/templates/themes/default/scheduling/settings.html
  relevance: HIGH
  why: "Template pattern for settings pages with tab navigation"

- file: userfrosting/templates/themes/default/menus/sidebar.html
  relevance: MEDIUM
  why: "Admin sidebar navigation structure"

- file: userfrosting/models/BaseModel.php
  relevance: MEDIUM
  sections: [dbConnectByName(), getStoreInfo()]
  why: "Database connection and store info helper patterns"

- file: public_html/js/scheduling/ScheduleCalendar.js
  relevance: HIGH
  sections: [getShiftType(), setColorMode()]
  why: "Integration point for consuming store hours in calendar"

- file: userfrosting/templates/themes/default/scheduling/calendar.html
  relevance: HIGH
  sections: [lines 196-244 localStorage handling]
  why: "Current localStorage implementation to migrate from"
```

### Implementation Boundaries

- **Must Preserve**:
  - Existing scheduling calendar functionality
  - Existing Store.php public interface (additive changes only)
  - Existing permission model (`uri_store_settings`)
  - localStorage fallback during migration period

- **Can Modify**:
  - Add new properties/methods to Store.php
  - Add new columns to `stores` table
  - Add new tables to `kiosk_buykiosk` database
  - Extend calendar template to use server-provided hours
  - Add new admin page and routes

- **Must Not Touch**:
  - Existing `stores` table columns (no modifications, only additions)
  - Other scheduling config (overtime rules, clock rules, etc.)
  - WhenIWork/Homebase external provider integrations

### External Interfaces

#### System Context Diagram

```mermaid
graph TB
    subgraph Actors
        SM[Store Manager]
        AM[Assistant Manager]
        RM[Regional Manager]
    end

    subgraph BuyerKiosk System
        SCP[Store Configuration Page]
        API[Configuration API]
        Store[Store Model]
        Cache[Redis Cache]
        DB[(MySQL Central DB)]
    end

    subgraph Consumers
        Calendar[Scheduling Calendar]
        Reports[Future: Reports]
    end

    SM --> SCP
    AM --> SCP
    RM --> SCP

    SCP --> API
    API --> Store
    Store --> Cache
    Store --> DB

    Calendar --> Store
    Reports -.-> Store
```

#### Interface Specifications

```yaml
# Inbound Interfaces
inbound:
  - name: "Admin Web Interface"
    type: HTTP/HTTPS
    format: HTML + REST API
    authentication: Session-based (UserFrosting)
    data_flow: "Store managers configure store hours via forms"

  - name: "Scheduling Calendar Consumer"
    type: Internal PHP/JS
    format: Store object properties + Twig variables
    authentication: N/A (internal)
    data_flow: "Calendar reads store hours for shift type classification"

# Data Interfaces
data:
  - name: "Central Database (kiosk_buykiosk)"
    type: MySQL
    connection: PDO via dbConnectByName()
    tables: stores, storeOperatingHours, storeHolidayHours
    data_flow: "Store configuration persistence"

  - name: "Redis Cache"
    type: Redis
    connection: Predis client
    key_pattern: "{typeNum}_store"
    ttl: 3600 (1 hour)
    data_flow: "Store object caching, must invalidate on config change"
```

### Project Commands

```bash
# Environment Setup
cd userfrosting && composer install      # Install PHP dependencies

# Testing Commands
./test.sh                                # Run all tests
./test.sh --testsuite unit               # Run unit tests only
./test.sh --testsuite integration        # Run integration tests only
./test.sh --coverage                     # Run with coverage report
./test.sh --stan                         # Run tests + PHPStan analysis

# Code Quality
cd userfrosting && ./vendor/bin/phpstan analyse    # Static analysis

# CSS Build (for admin page styling)
php userfrosting/conductor build-css               # Development build
php userfrosting/conductor build-css --minify      # Production build

# Database Migrations
php userfrosting/conductor run                     # Run pending migrations

# Deployment
./deploy.sh                                        # Test + deploy
```

## Solution Strategy

### Architecture Pattern: **Layered Architecture with Repository Pattern**

Following the existing codebase patterns, this solution uses:
- **Presentation Layer**: Twig templates + Bootstrap 5 forms
- **Controller Layer**: PageController (HTML) + Controller (API)
- **Domain Layer**: Store model extended with hours-related properties/methods
- **Data Access Layer**: Direct PDO queries (following existing pattern, no ORM)

### Integration Approach

1. **Additive Extension**: Extend existing Store.php with new properties rather than creating separate entities
2. **Centralized Storage**: All store-level config in `kiosk_buykiosk.stores` table (following scheduling config pattern)
3. **Related Tables**: Separate tables for per-day hours and holiday overrides for normalization
4. **Cache Invalidation**: Clear Redis cache on config save to ensure consistency

### Key Decisions

| Decision | Choice | Rationale |
|----------|--------|-----------|
| Storage location | Central DB (`kiosk_buykiosk`) | Follows scheduling config pattern, single source of truth |
| Hours format | VARCHAR(5) `HH:MM` (24-hour, zero-padded) | Wall-clock times, timezone-agnostic, matches existing calendar code and `<input type="time">` output |
| Per-day model | Separate `storeOperatingHours` table | 7-row normalized structure, extensible for future needs |
| Holiday model | Separate `storeHolidayHours` table | Date-keyed overrides, supports closed days |
| UI location | Replace existing `/admin/:typeNum/store/settings` page | Replaces legacy placeholder page (password/Facebook) with store hours configuration, under existing "Store Configuration" sidebar link |

## Building Block View

### Components

```mermaid
graph TB
    subgraph Presentation
        ConfigPage[Store Configuration Page]
        CalendarPage[Scheduling Calendar]
    end

    subgraph Controllers
        ConfigPageCtrl[StoreConfigPageController]
        ConfigAPICtrl[StoreConfigController]
        CalendarPageCtrl[SchedulingPageController]
    end

    subgraph Domain
        Store[Store Model]
        HoursService[StoreHoursService]
    end

    subgraph Data Access
        HoursRepo[StoreHoursRepository]
        HolidayRepo[StoreHolidayRepository]
    end

    subgraph Storage
        Redis[(Redis Cache)]
        MySQL[(MySQL Central DB)]
    end

    ConfigPage --> ConfigPageCtrl
    ConfigPage -->|AJAX| ConfigAPICtrl
    CalendarPage --> CalendarPageCtrl

    ConfigPageCtrl --> Store
    ConfigAPICtrl --> HoursService
    CalendarPageCtrl --> Store

    HoursService --> HoursRepo
    HoursService --> HolidayRepo
    HoursService --> Store

    Store --> Redis
    HoursRepo --> MySQL
    HolidayRepo --> MySQL
```

### Directory Map

```
userfrosting/
├── routes/
│   ├── admin/
│   │   └── store-config.php                    # NEW: Admin page routes
│   └── store-config.php                        # NEW: API routes
├── src/BuyerKiosk/
│   ├── Core/
│   │   └── Store.php                           # MODIFY: Add hours properties
│   └── StoreConfig/                            # NEW: Feature directory
│       ├── Controllers/
│       │   ├── StoreConfigPageController.php   # NEW: Page rendering
│       │   └── StoreConfigController.php       # NEW: API endpoints
│       ├── Services/
│       │   └── StoreHoursService.php           # NEW: Business logic
│       └── Repositories/
│           ├── StoreHoursRepository.php        # NEW: Operating hours CRUD
│           └── StoreHolidayRepository.php      # NEW: Holiday hours CRUD
├── templates/themes/default/
│   ├── store/
│   │   └── settings.html                       # REPLACE: Config page template (replaces legacy password/FB page)
│   └── menus/
│       └── sidebar.html                        # NO CHANGE: Existing link already points to store/settings
└── migrations/input/
    └── 20251216_018_001_store_hours.json       # NEW: Schema migration

public_html/
├── js/
│   └── admin/
│       └── store-config.js                     # NEW: Page JavaScript
└── css/
    └── admin/
        └── modules/
            └── store-config.css                # NEW: Page-specific styles (if needed)
```

### Interface Specifications

#### Data Storage Changes

**Table: `stores` (MODIFY - kiosk_buykiosk database)**
```yaml
# New columns to add
ADD COLUMN: defaultOpenTime VARCHAR(5) DEFAULT '09:00' COMMENT 'Default store open time HH:MM'
ADD COLUMN: defaultCloseTime VARCHAR(5) DEFAULT '21:00' COMMENT 'Default store close time HH:MM'
ADD COLUMN: hoursLastUpdated TIMESTAMP NULL COMMENT 'Last time hours were modified'
ADD COLUMN: hoursUpdatedByUserId INT UNSIGNED NULL COMMENT 'User who last modified hours'
```

**Table: `storeOperatingHours` (NEW - kiosk_buykiosk database)**
```yaml
# Per-day operating hours
id: INT UNSIGNED PRIMARY KEY AUTO_INCREMENT
typeNum: VARCHAR(10) NOT NULL
dayOfWeek: TINYINT UNSIGNED NOT NULL COMMENT '0=Sunday, 6=Saturday'
openTime: VARCHAR(5) NULL COMMENT 'HH:MM format, NULL means use default'
closeTime: VARCHAR(5) NULL COMMENT 'HH:MM format, NULL means use default'
isClosed: TINYINT(1) NOT NULL DEFAULT 0 COMMENT '1=closed on this day'
created_at: TIMESTAMP DEFAULT CURRENT_TIMESTAMP
updated_at: TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP

UNIQUE KEY: uk_store_day (typeNum, dayOfWeek)
INDEX: idx_typenum (typeNum)
```

**Table: `storeHolidayHours` (NEW - kiosk_buykiosk database)**
```yaml
# Holiday/special date overrides
id: INT UNSIGNED PRIMARY KEY AUTO_INCREMENT
typeNum: VARCHAR(10) NOT NULL
holidayDate: DATE NOT NULL COMMENT 'Specific date for override'
holidayName: VARCHAR(100) NULL COMMENT 'Optional name (e.g., Christmas)'
openTime: VARCHAR(5) NULL COMMENT 'HH:MM format, NULL with isClosed=1 means closed'
closeTime: VARCHAR(5) NULL COMMENT 'HH:MM format'
isClosed: TINYINT(1) NOT NULL DEFAULT 0 COMMENT '1=closed on this date'
created_at: TIMESTAMP DEFAULT CURRENT_TIMESTAMP
updated_at: TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP

UNIQUE KEY: uk_store_date (typeNum, holidayDate)
INDEX: idx_typenum (typeNum)
INDEX: idx_date (holidayDate)
```

#### Internal API Changes

**Endpoint: Get Store Configuration**
```yaml
Method: GET
Path: /api/:typeNum/store/config
Auth: uri_store_settings + checkStoreGroup
Request: None
Response (200):
  success: true
  storeInfo:
    typeNum: string
    name: string
    address: string
    city: string
    state: string
    timezone: string
  hours:
    default:
      openTime: string (HH:MM)
      closeTime: string (HH:MM)
    byDay:
      - dayOfWeek: int (0-6)
        dayName: string
        openTime: string|null
        closeTime: string|null
        isClosed: boolean
        usesDefault: boolean
    holidays:
      - id: int
        date: string (YYYY-MM-DD)
        name: string|null
        openTime: string|null
        closeTime: string|null
        isClosed: boolean
  meta:
    lastUpdated: string (ISO8601)|null
    lastUpdatedBy: string|null
Response (4xx/5xx):
  success: false
  error: string
  code: string
```

**Endpoint: Update Store Hours**
```yaml
Method: PUT
Path: /api/:typeNum/store/hours
Auth: uri_store_settings + checkStoreGroup
Request:
  default:
    openTime: string (HH:MM) - required
    closeTime: string (HH:MM) - required
  byDay:  # Optional array (when present, treated as a full replacement)
    - dayOfWeek: int (0-6)  # must include exactly one entry for each day 0..6
      openTime: string|null  # null = use default
      closeTime: string|null
      isClosed: boolean
Response (200):
  success: true
  message: string
  hours: object (same shape as GET response `hours`)
Response (400):
  success: false
  error: string
  code: VALIDATION_ERROR
  validationErrors: object
Response (403):
  success: false
  error: string
  code: FORBIDDEN
```

**Endpoint: Manage Holiday Hours**
```yaml
# Create Holiday
Method: POST
Path: /api/:typeNum/store/holidays
Auth: uri_store_settings + checkStoreGroup
Request:
  date: string (YYYY-MM-DD) - required
  name: string|null
  openTime: string|null (HH:MM)
  closeTime: string|null (HH:MM)
  isClosed: boolean
Response (200):
  success: true
  holiday: object
Response (400/403/500):
  success: false
  error: string
  code: string

# Update Holiday
Method: PUT
Path: /api/:typeNum/store/holidays/:holidayId
Auth: uri_store_settings + checkStoreGroup
Request:
  name: string|null
  openTime: string|null (HH:MM)
  closeTime: string|null (HH:MM)
  isClosed: boolean
Response (200):
  success: true
  holiday: object
Response (400/403/404/500):
  success: false
  error: string
  code: string

# Delete Holiday
Method: DELETE
Path: /api/:typeNum/store/holidays/:holidayId
Auth: uri_store_settings + checkStoreGroup
Response (200):
  success: true
Response (403/404/500):
  success: false
  error: string
  code: string
```

#### Application Data Models

```pseudocode
ENTITY: Store (MODIFIED)
  FIELDS:
    # Existing fields preserved...
    + defaultOpenTime: string (NEW)
    + defaultCloseTime: string (NEW)
    + hoursLastUpdated: DateTime|null (NEW)
    + hoursUpdatedByUserId: int|null (NEW)

  BEHAVIORS:
    # Existing methods preserved...
    + getDefaultOpenTime(): string
    + setDefaultOpenTime(time: string): void
    + getDefaultCloseTime(): string
    + setDefaultCloseTime(time: string): void
    + getHoursLastUpdated(): DateTime|null
    # NOTE: Do not embed repository access into Store.php. Effective-hours resolution
    # is performed in StoreHoursService to keep Store an entity (SOLID: SRP).

VALUE_OBJECT: EffectiveHours (NEW)
  FIELDS:
    openTime: string (HH:MM)
    closeTime: string (HH:MM)
    isClosed: boolean
    source: enum('default', 'day_override', 'holiday')
```

### Integration Points

```yaml
# Scheduling Calendar Integration
Calendar_Template:
  - file: userfrosting/templates/themes/default/scheduling/calendar.html
  - integration: "Pass server-provided store hours as Twig variables"
  - data_flow: "storeOpenTime, storeCloseTime from Store model to template"

Calendar_JavaScript:
  - file: public_html/js/scheduling/ScheduleCalendar.js
  - integration: "Initialize with server hours; do not read localStorage for store hours"
  - data_flow: "Use Store defaultOpenTime/defaultCloseTime for shift-type classification; localStorage is only for colorMode preference + one-time migration prompt"

# Redis Cache Integration
Store_Cache:
  - integration: "Invalidate {typeNum}_store key on hours update"
  - method: "StoreController->clearCache() after successful save"
```

### Implementation Examples

#### Example: Effective Hours Resolution

**Why this example**: The logic for resolving which hours apply on a given date involves a priority cascade that's critical to get right.

```php
// Example: Resolving effective hours for a specific date
// This demonstrates the priority cascade: holiday > day override > default

class StoreHoursService
{
    public function getEffectiveHours(string $typeNum, DateTimeInterface $date): EffectiveHours
    {
        // IMPORTANT: `$date` must represent a store-local calendar date (or be converted
        // into the store timezone before deriving day-of-week/holidayDate). Do not
        // compute `format('w')` on a UTC DateTime when the store timezone differs.

        // Priority 1: Check for holiday override on this specific date
        $holiday = $this->holidayRepo->findByDate($typeNum, $date);
        if ($holiday) {
            return new EffectiveHours(
                openTime: $holiday->isClosed ? null : $holiday->openTime,
                closeTime: $holiday->isClosed ? null : $holiday->closeTime,
                isClosed: $holiday->isClosed,
                source: 'holiday'
            );
        }

        // Priority 2: Check for day-of-week override
        $dayOfWeek = (int) $date->format('w'); // 0=Sun, 6=Sat
        $dayHours = $this->hoursRepo->findByDay($typeNum, $dayOfWeek);
        if ($dayHours && !$dayHours->usesDefault()) {
            return new EffectiveHours(
                openTime: $dayHours->isClosed ? null : $dayHours->openTime,
                closeTime: $dayHours->isClosed ? null : $dayHours->closeTime,
                isClosed: $dayHours->isClosed,
                source: 'day_override'
            );
        }

        // Priority 3: Use default store hours
        $store = $this->storeController->getStore();
        return new EffectiveHours(
            openTime: $store->getDefaultOpenTime(),
            closeTime: $store->getDefaultCloseTime(),
            isClosed: false,
            source: 'default'
        );
    }
}
```

#### Example: Time Validation Pattern

**Why this example**: Validating that close time is after open time (same-day constraint) is a key business rule.

```php
// Example: Validating store hours input
private function validateHours(?string $openTime, ?string $closeTime, bool $isClosed): array
{
    $errors = [];

    // If closed, times can be null
    if ($isClosed) {
        return $errors;
    }

    // Both times required if not closed
    if (empty($openTime) || empty($closeTime)) {
        $errors[] = 'Both open and close times are required when store is open';
        return $errors;
    }

    // Validate format HH:MM
    $timePattern = '/^([01]\d|2[0-3]):([0-5]\d)$/';
    if (!preg_match($timePattern, $openTime)) {
        $errors[] = 'Open time must be in HH:MM format (24-hour)';
    }
    if (!preg_match($timePattern, $closeTime)) {
        $errors[] = 'Close time must be in HH:MM format (24-hour)';
    }

    // Validate close > open (same-day constraint)
    if (empty($errors)) {
        $openMinutes = $this->timeToMinutes($openTime);
        $closeMinutes = $this->timeToMinutes($closeTime);

        if ($closeMinutes <= $openMinutes) {
            $errors[] = 'Close time must be after open time (overnight hours not supported)';
        }
    }

    return $errors;
}

private function timeToMinutes(string $time): int
{
    [$hours, $minutes] = explode(':', $time);
    return (int)$hours * 60 + (int)$minutes;
}
```

## Runtime View

### Primary Flow: Saving Store Hours

1. User navigates to Store Configuration page
2. System loads current config from database via API
3. User modifies hours (default, per-day, or holidays)
4. User clicks "Save"
5. Frontend validates inputs client-side
6. AJAX PUT request to `/api/:typeNum/store/hours`
7. Backend validates inputs, checks permissions
8. Backend saves to database (transaction for atomic update)
9. Backend invalidates Redis cache
10. Backend returns success response with updated hours
11. Frontend shows success toast notification
12. Other open tabs (calendar/config) refresh hours on next API call (or via a lightweight client-side broadcast)

**Recommended tab-sync mechanism (no server push required)**:
- Emit a `localStorage` event (e.g., set `store_hours_updated_{typeNum}` to a timestamp) or use `BroadcastChannel`.
- Calendar listens and triggers a `GET /api/:typeNum/store/config` refresh when it receives the event.

```mermaid
sequenceDiagram
    actor SM as Store Manager
    participant UI as Config Page
    participant API as StoreConfigController
    participant SVC as StoreHoursService
    participant DB as MySQL
    participant Cache as Redis

    SM->>UI: Click "Save Hours"
    UI->>UI: Validate form inputs
    UI->>API: PUT /api/:typeNum/store/hours
    API->>API: Check permissions
    API->>SVC: updateHours(data)
    SVC->>SVC: Validate business rules
    SVC->>DB: BEGIN TRANSACTION
    SVC->>DB: UPDATE stores (defaults)
    SVC->>DB: UPSERT storeOperatingHours (per-day)
    SVC->>DB: COMMIT
    SVC->>Cache: DELETE {typeNum}_store
    SVC-->>API: Updated hours
    API-->>UI: JSON success response
    UI->>UI: Show success toast
    UI->>UI: Update form with saved values
```

### Error Handling

| Error Type | Response | User Guidance |
|------------|----------|---------------|
| Validation: Invalid time format | 400 Bad Request | "Time must be in HH:MM format (e.g., 09:00)" |
| Validation: Close before open | 400 Bad Request | "Close time must be after open time" |
| Validation: Duplicate holiday date | 400 Bad Request | "A holiday override already exists for this date" |
| Auth: Not logged in | 401 Unauthorized | Redirect to login |
| Auth: No permission | 403 Forbidden | "You don't have permission to modify store settings" |
| Auth: Wrong store | 403 Forbidden | "You don't have access to this store" |
| Network failure | 500/timeout | "Failed to save. Please check your connection and try again." |
| Database error | 500 Internal Server Error | "An error occurred. Please try again or contact support." |

### localStorage Migration Flow

The scheduling calendar previously stored hours in browser localStorage. After introducing persistent store configuration, localStorage must never silently override the database.

**Canonical rule**: Database-backed store configuration is the source of truth. localStorage is only consulted for a one-time migration prompt and should be cleared after the user decides.

**Detecting “not configured yet”**: The config API always returns default hours (because the `stores` columns have defaults). Use `meta.lastUpdated == null` (or `hoursLastUpdated` being NULL) as the indicator that hours have not been explicitly configured by a manager yet.

```mermaid
sequenceDiagram
    actor User
    participant Calendar as Calendar Page
    participant Server as Web Server
    participant Config as Store Config Page
    participant API as Config API
    participant DB as Database

    User->>Calendar: Open scheduling calendar
    Calendar->>Server: GET /admin/:typeNum/schedule
    Server->>DB: Load Store (hours + hoursLastUpdated; may be Redis-cached)
    DB-->>Server: Return storeOpenTime/storeCloseTime + storeHoursLastUpdated
    Server-->>Calendar: HTML + Twig vars (storeOpenTime/storeCloseTime + storeHoursLastUpdated)
    Calendar->>Calendar: Use database hours (always)

    alt storeHoursLastUpdated is null AND legacy localStorage hours exist
        Calendar->>Calendar: Show non-blocking banner linking to Store Config page (no auto-import)
        User->>Config: Visit Store Configuration page
        Config->>API: GET /api/:typeNum/store/config
        API-->>Config: {success: true, hours: {...}, meta.lastUpdated: null}
        Config->>Config: Read legacy localStorage hours
        Config->>Config: Prompt: "Import hours from this browser?" (Import / Discard)
        alt User imports
            Config->>API: PUT /api/:typeNum/store/hours (payload from localStorage)
            API->>DB: Save hours + set hoursLastUpdated
            Config->>Config: Clear legacy localStorage hours fields
        else User discards
            Config->>Config: Clear legacy localStorage hours fields
        end
    end
```

## Deployment View

### Single Application Deployment

- **Environment**: Web server (PHP 8.x, Apache/Nginx)
- **Configuration**: No new environment variables required
- **Dependencies**: MySQL, Redis (existing infrastructure)
- **Performance**:
  - Config page load: <2s
  - API response: <500ms
  - Cache hit rate target: >95% for store object reads

### Database Migration

```yaml
Migration File: 20251216_018_001_store_hours.json
Database: kiosk_buykiosk (central)
Order:
  1. ALTER stores table (add new columns)
  2. CREATE storeOperatingHours table
  3. CREATE storeHolidayHours table
Rollback:
  1. DROP storeHolidayHours
  2. DROP storeOperatingHours
  3. ALTER stores DROP new columns
```

### Feature Rollout

- **Immediate availability**: All stores see new configuration page
- **No feature flag**: Feature is additive, no existing behavior broken
- **Migration period**:
  - Calendar reads hours from database (store config) as the default path
  - Legacy localStorage is supported only for an explicit import/discard prompt when `meta.lastUpdated` is null
  - After the migration window, legacy localStorage reads/writes should be removed from calendar.html

## Cross-Cutting Concepts

### Pattern Documentation

```yaml
# Existing patterns used
- pattern: Controller pattern (PageController + Controller)
  relevance: CRITICAL
  why: "Must follow established separation of page rendering vs API endpoints"

- pattern: Repository pattern (direct PDO)
  relevance: HIGH
  why: "Data access follows existing repository patterns in scheduling module"

- pattern: JSON migration format
  relevance: HIGH
  why: "Database migrations must use existing JSON migration format"

# New patterns created
- pattern: Store configuration API pattern
  relevance: MEDIUM
  why: "Establishes pattern for future store-level settings (not just scheduling)"
```

### System-Wide Patterns

**Security**:
- All endpoints require session authentication
- `uri_store_settings` permission checked via `$app->user->checkAccess()`
- Store access checked via `$app->user->checkStoreGroup($typeNum)`
- No sensitive data in hours configuration

**Error Handling**:
- Global: Exceptions logged, generic message returned
- Local: Validation errors returned with specific field messages
- API: JSON responses with `success` boolean; failures include `error` + `code` (+ `validationErrors` when applicable)

**Logging/Auditing**:
- `hoursLastUpdated` and `hoursUpdatedByUserId` track modifications in `stores` table
- Detailed change logging deferred to "Could Have" feature (separate audit table)

**Caching**:
- Store objects cached in Redis (key: `{typeNum}_store`, TTL: 1 hour)
- Cache invalidated on any hours update via `StoreController->clearCache()`
- No additional caching needed for hours (low volume config data)

### Implementation Patterns

#### Code Patterns and Conventions

- **Naming**: camelCase for columns and properties (e.g., `defaultOpenTime`, `dayOfWeek`)
- **Time format**: HH:MM string (24-hour), stored as VARCHAR(5)
- **Date format**: YYYY-MM-DD for holiday dates
- **JSON responses**: `success: true/false` + payload on success; `error`/`code` (and optional `validationErrors`) on failure

#### Error Handling Pattern

```pseudocode
FUNCTION: handle_api_request(operation)
  TRY:
    VALIDATE: permissions, store_access
    VALIDATE: input_data (format, business_rules)
    EXECUTE: operation
    INVALIDATE: cache_if_modified
    RESPOND: success_json
  CATCH ValidationException:
    RESPOND: 400_json(validation_errors)
  CATCH AuthException:
    RESPOND: 403_json(permission_denied)
  CATCH Exception:
    LOG: error_details
    RESPOND: 500_json(generic_error)
```

#### Component Structure Pattern

```pseudocode
COMPONENT: StoreConfigPage(typeNum)
  ON_LOAD:
    FETCH: GET /api/:typeNum/store/config
    IF error: SHOW error_alert
    ELSE: POPULATE form_fields

  ON_SAVE:
    VALIDATE: client_side_validation
    IF invalid: SHOW validation_errors
    ELSE:
      SUBMIT: PUT /api/:typeNum/store/hours
      IF success: SHOW success_toast, UPDATE form
      ELSE: SHOW error_toast

  ON_RESET:
    RESTORE: form_to_last_saved_state
```

## Architecture Decisions

- [x] **ADR-1: Store hours in central database (`kiosk_buykiosk`)**
  - Rationale: Follows existing scheduling config pattern, single source of truth accessible by any component
  - Trade-offs: Requires migration from localStorage, additional DB queries vs Redis-cached Store object
  - User confirmed: Yes (implicit in PRD requirement for persistent storage)

- [x] **ADR-2: Separate tables for per-day and holiday hours**
  - Rationale: Normalized design, flexible for 7-day week + unlimited holidays, easier to query specific dates
  - Trade-offs: Additional JOINs when loading full config, but config reads are infrequent
  - User confirmed: Yes (PRD includes per-day + holiday hours; implementation can be staged, but schema supports both)

- [x] **ADR-3: New dedicated Store Configuration page**
  - Rationale: Extensible foundation for future store-level settings, clear navigation, not tied to scheduling feature
  - Trade-offs: Additional page to maintain, but provides cleaner UX than overloading existing pages
  - User confirmed: Yes (user chose "New 'Store Configuration' page")

- [x] **ADR-4: Wall-clock time format (HH:MM), not UTC timestamps**
  - Rationale: Store hours represent local opening times, interpreted in store's timezone. Storing as timestamp would require timezone conversion and introduce DST complexity.
  - Trade-offs: Requires combining with store timezone for absolute time calculations
  - User confirmed: Yes (PRD specifies "Hours are wall-clock times")

- [x] **ADR-5: Same-day hours only (no overnight support)**
  - Rationale: Simplifies validation, covers 99%+ of retail use cases, explicit PRD constraint
  - Trade-offs: Stores operating overnight (bars, convenience stores) not supported
  - User confirmed: Yes (PRD decision logged: "Same-day store hours only")

## Quality Requirements

| Requirement | Metric | Target |
|-------------|--------|--------|
| **Performance: Page Load** | Time to interactive | <2 seconds |
| **Performance: API Response** | GET/PUT config endpoints | <500ms p95 |
| **Reliability: Save Success** | Error rate on save operations | <1% |
| **Usability: Task Completion** | Time to configure hours first time | <2 minutes |
| **Usability: Form Clarity** | Users understand all fields without documentation | >90% |
| **Security: Authorization** | Unauthorized access attempts blocked | 100% |
| **Data Integrity: Validation** | Invalid data rejected before persistence | 100% |

## Risks and Technical Debt

### Known Technical Issues

- **localStorage/Database Dual State**: During migration period, users may have localStorage values that differ from database. Mitigated by migration prompt on config page load.
- **Redis Cache Staleness**: If cache invalidation fails, stale hours could be served. Mitigated by 1-hour TTL as backup expiration.

### Technical Debt

- **Calendar localStorage handling**: After database migration is complete, localStorage code in calendar.html should be removed (tech debt cleanup task)
- **Store.php size**: Store class already large (60+ properties). Consider extracting configuration into sub-objects in future refactor.

### Implementation Gotchas

- **StoreController caching**: Must call `$storeController->clearCache()` after modifying store hours, or Redis will serve stale data for up to 1 hour
- **Time input browser quirks**: `<input type="time">` behavior varies by browser. Consider using a consistent time picker library.
- **Timezone display**: Always show store timezone prominently when displaying/editing hours to avoid user confusion
- **Date-only semantics (holidays)**: `holidayDate` is a store-local calendar date (DATE). Frontend must not use `new Date('YYYY-MM-DD')` (UTC parsing drift). Treat date-only strings as store-local dates.
- **Day-of-week calculation**: Effective-hours resolution must derive `dayOfWeek` from the store-local date, not a UTC instant (DST/timezone boundaries can shift the calendar day).
- **Time normalization**: Normalize persisted and returned times to zero-padded `HH:MM` to avoid subtle diffs (`9:00` vs `09:00`) and to make comparisons/idempotency reliable.
- **24-hour stores**: The same-day constraint (`close > open`) cannot represent true 24-hour operation; decide on an explicit representation if needed (e.g., dedicated `isOpen24Hours` flag) before rollout to 24h locations.
- **Migration JSON format**: Migrations must use the existing JSON format in `userfrosting/migrations/input/`, not raw SQL files

## Test Specifications

### Critical Test Scenarios

**Scenario 1: Happy Path - Save Default Hours**
```gherkin
Given: Store manager is on Store Configuration page
And: Store has default hours 09:00-21:00
When: Manager changes close time to 18:00
And: Manager clicks Save
Then: Success toast displays "Hours saved successfully"
And: Form shows new close time 18:00
And: Database stores.defaultCloseTime = '18:00'
And: Redis cache for store is invalidated
```

**Scenario 2: Per-Day Override**
```gherkin
Given: Store has default hours 09:00-21:00
When: Manager sets Sunday to "Closed"
And: Manager sets Saturday hours to 10:00-20:00
And: Manager clicks Save
Then: storeOperatingHours has Sunday with isClosed=1
And: storeOperatingHours has Saturday with openTime='10:00', closeTime='20:00'
And: Other days have no override rows (use default)
```

**Scenario 3: Holiday Override**
```gherkin
Given: Store has default hours configured
When: Manager adds holiday "2025-12-25" named "Christmas" as Closed
Then: storeHolidayHours has row with holidayDate='2025-12-25', isClosed=1
And: Holiday appears in holidays list on config page
```

**Scenario 4: Validation - Close Before Open**
```gherkin
Given: Manager is editing default hours
When: Manager enters openTime='18:00' and closeTime='09:00'
And: Manager clicks Save
Then: Validation error displays "Close time must be after open time"
And: Form remains in edit state
And: No database changes occur
```

**Scenario 5: Authorization - Wrong Store**
```gherkin
Given: Manager has access only to store 'ou00'
When: Manager attempts PUT /api/pa00/store/hours
Then: API returns 403 Forbidden
And: No database changes occur
```

**Scenario 6: Scheduling Calendar Integration**
```gherkin
Given: Store hours are set to 10:00-18:00 in database
When: User opens scheduling calendar
Then: Calendar receives storeOpenTime='10:00' and storeCloseTime='18:00'
And: Shift type classification uses these hours (not localStorage)
```

### Test Coverage Requirements

- **Business Logic**: Hours validation, effective hours resolution, per-day/holiday priority
- **API Endpoints**: All CRUD operations, error responses, auth checks
- **UI Components**: Form submission, validation display, toast notifications
- **Integration**: Cache invalidation, calendar data flow
- **Edge Cases**: Empty hours, all days closed, overlapping holidays, timezone boundary dates

---

## Glossary

### Domain Terms

| Term | Definition | Context |
|------|------------|---------|
| Store Hours | The times when a store is open for business | Used for scheduling, shift classification |
| Default Hours | Store's standard open/close times that apply when no override exists | Base configuration |
| Day Override | Custom hours for a specific day of week (e.g., Sunday closed) | Per-day configuration |
| Holiday Override | Custom hours for a specific calendar date | Takes priority over default and day overrides |
| Effective Hours | The resolved open/close times for a specific date after applying all overrides | Computed value |

### Technical Terms

| Term | Definition | Context |
|------|------------|---------|
| typeNum | Store identifier pattern `[a-z][a-z]\d+` (e.g., `ou00`, `pa00`) | Used in URLs, DB queries, cache keys |
| Wall-clock Time | Time as displayed on a clock in the store's timezone | Hours stored as HH:MM strings |
| Central Database | `kiosk_buykiosk` - shared database for all stores | Where store configuration lives |
| Store Database | Per-store database named by typeNum (e.g., `kiosk_ou00`) | Where operational data lives |

### API Terms

| Term | Definition | Context |
|------|------------|---------|
| Config API | REST endpoints for reading/writing store configuration | `/api/:typeNum/store/*` |
| Effective Hours API | Future endpoint to get resolved hours for a date range | Integration with scheduling |
