# Digital Signage Business Rules

> **Last Updated**: December 2025
> **Related**: [Overview](./digital-signage-overview.md)

## Slide Hierarchy

### Three-Tier Slide Model

The system supports slides from three sources, each with different scope and access rules:

| Source | slideUploader | Database | Scope |
|--------|---------------|----------|-------|
| Store Slides | 0 | Store-specific DB | Single store only |
| Corporate Slides | 1 | Global DB (corpSlides) | Filtered by storeType |
| Hipbone Slides | 2 | Global DB (hbSlides) | Filtered by storeType |

**Code Reference**: `userfrosting/src/BuyerKiosk/DigitalSign/StoreLoop.php:93-96`

### Corporate Slide Availability

Corporate slides are available to stores based on the `storeType` field:

```php
// From AvailableSlides.php:23-51
// Corporate slides filtered by store's storeType
SELECT * FROM corpSlides
WHERE enabled = 1 AND storeType = :storeType
```

Store types include: `ou` (Once Upon A Child), `pa` (Plato's Closet), etc.

---

## Slide Lifecycle

### State Machine

```
┌─────────────────┐
│   UNSCHEDULED   │ ← startDate = NULL, scheduled = 0
│  (Immediately   │   Slide is visible immediately
│    Active)      │
└────────┬────────┘
         │
         │ OR (with startDate)
         ▼
┌─────────────────┐
│    SCHEDULED    │ ← startDate = future, scheduled = 1
│  (Hidden until  │   Slide hidden from active loop
│   startDate)    │
└────────┬────────┘
         │ (cron triggers when startDate <= now)
         ▼
┌─────────────────┐
│    ACTIVATED    │ ← started = 1, scheduled = 0
│   (Visible in   │   startDate cleared
│     loop)       │   Ably refresh sent
└────────┬────────┘
         │ (when expireDate <= now)
         ▼
┌─────────────────┐
│     EXPIRED     │ ← finished = 1
│   (Deleted)     │   Removed from dsLoop
└─────────────────┘
```

### Slide Creation Rules

**Store Slides** (`Slide.php:40-49`)
- Required fields: `slideName`, `fileName`, `type`
- Default enabled: Always `1` (enabled) on creation
- Stored in store-specific database

**Corporate Slides** (`CorpSlide.php:28-37`)
- Required fields: `slideName`, `fileName`, `type`, `storeType`
- Default enabled: Always `1` (enabled) on creation
- Stored in global database (`kiosk_buykiosk`)

### Scheduling Rules

**Adding Scheduled Slides** (`LoopController.php:38-46`)
```php
// If startDate provided, mark as scheduled
if ($startDate != null) {
    $scheduled = 1;
} else {
    $scheduled = 0;
}

// Add to global schedule if scheduled OR has expireDate
if ($scheduled || $expireDate != null) {
    $this->addToGlobalSchedule($loopItem);
}
```

**Schedule Processing** (`SlideScheduleController.php:20-48`)
- Runs via cron/background task
- Queries: `(startDate <= :date AND started IS NULL) OR (expireDate <= :date AND finished = 0)`
- Processes each matching item for activation or deactivation

### Activation Logic

**When**: `startDate <= currentDate AND started IS NULL`

**Actions** (`SlideScheduleController.php:68-87`):
1. Update `dsLoop`: Set `scheduled = 0`, clear `startDate`
2. Update `digitalSignSchedule`: Set `started = 1`
3. Publish Ably message: `{action: "refresh"}`

### Deactivation/Expiration Logic

**When**: `expireDate <= currentDate AND (started = 1 OR started IS NULL)`

**Actions** (`SlideScheduleController.php:89-109`):
1. DELETE from `dsLoop`
2. Update `digitalSignSchedule`: Set `finished = 1`
3. Publish Ably message: `{action: "refresh"}`

---

## Loop Composition Rules

### Position Management

**Adding Slides** (`LoopController.php:27`)
```php
// Auto-assign position as last in loop
$position = MAX(position) + 1
```

**Reordering** (`StoreLoop.php:129-146`)
- After any deletion, positions are recalculated sequentially (0, 1, 2...)
- Single query per position update

### Loop Entry Structure

Each loop entry contains (`LoopItem.php`):

| Field | Type | Description |
|-------|------|-------------|
| id | int | Loop entry ID |
| slideID | int | Reference to slide |
| position | int | Order in loop (0-based) |
| duration | int | Display time in seconds |
| animation | string | Transition effect (default: "fadeIn") |
| slideUploader | int | Source (0=store, 1=corp, 2=hipbone) |
| scheduled | bool | 0=active now, 1=scheduled |
| startDate | datetime | Activation date (null = immediate) |
| expireDate | datetime | Expiration date (null = never) |

### Active vs Full Loop

| Method | Returns | Use Case |
|--------|---------|----------|
| `getCurrentSignLoop()` | ALL slides including scheduled | Admin view |
| `getCurrentSignLoopForSign()` | Only `scheduled = 0` | Display rendering |

**Code Reference**: `StoreLoop.php:56` - `if($loopItem['scheduled'] == 0)`

---

## Media Validation Rules

### Accepted File Types

**Images** (`UploadController.php:26`):
- MIME: `image/jpeg`, `image/png`, `image/gif`
- Extensions: `.gif`, `.jpg`, `.jpeg`, `.png`

**Videos**:
- MIME: `video/mp4`, `video/webm`
- Extensions: `.mp4`, `.webm`

### Type Detection

```php
// From UploadController.php
if (MIME in ['image/jpeg', 'image/png', 'image/gif']) {
    $slide->type = 0;  // Image
} else if (MIME in ['video/mp4', 'video/webm']) {
    $slide->type = 1;  // Video
    $slide->videoDuration = $handler->response['files'][0]->duration;
}
```

### Filename Generation

**Security Pattern** (`UploadHandler.php:53-57`):
```php
// Random 32-character hex string
$random_string = bin2hex(random_bytes(16));
$filename = $random_string . '.' . $extension;
```

This prevents:
- Directory traversal attacks
- Duplicate filename conflicts
- Predictable file URLs

### File Size Limits

- Max file size: Configurable (default: PHP upload_max_filesize)
- Min file size: 1 byte
- Validated against `post_max_size` and `upload_max_filesize`

**Code Reference**: `UploadHandler.php:416-427`

---

## Thumbnail Generation Rules

### Image Thumbnails

- Max dimensions: 300x300 pixels
- Format: Same as source
- Location: `upload_path/thumbs/[filename]`

### Video Thumbnails

- Extracted frame: 5 seconds into video
- Format: JPEG
- Naming: `[original_filename].jpg`
- Location: `upload_path/thumbs/[filename].jpg`

**Code Reference**: `UploadHandler.php:1061-1097`

### Thumbnail Retrieval

```php
// From StoreLoop.php:158-167
if ($type == 0) {  // Image
    return $fileName;  // Use original
} else {  // Video
    return $fileName . '.jpg';  // Append .jpg
}
```

---

## Animation Rules

### Default Animation

- Value: `"fadeIn"`
- Set in: `LoopItem.php:26`
- Applied to all slides unless overridden

### Supported Animations

Uses Animate.css library. Common options:
- `fadeIn` (default)
- `slideInLeft`
- `slideInRight`
- `zoomIn`
- `random` (selects randomly)

---

## Queue Display Rules (Type 3)

### Activation

- Slide type `3` triggers queue display
- Only the FIRST type 3 slide renders (`isFirstQueue` flag)
- Embedded in loop but refreshes independently

### Data Sources

| Endpoint | Data |
|----------|------|
| `/{typeNum}/json` | Current queue items |
| `/{typeNum}/jsonWait` | Wait time estimation |
| `/{typeNum}/jsonCompleted` | Today's completed customers |

### Refresh Interval

- Queue data refreshes every 5000ms (5 seconds)
- Independent of slide rotation

**Code Reference**: `templates/themes/default/ds/snips/queue.html`

---

## Permission & Access Rules

### Store Access

All operations require valid store context:
```php
$storeController = new StoreController($typeNum);
$store = $storeController->getStore();
$db = dbConnectByName($store->getDbName());
```

### Database Isolation

- Store slides: Only accessible from that store's database
- Corporate slides: Filtered by storeType
- No cross-store data leakage at database level

### Sync App Authentication

**API Key Validation** (`DownloadDigitalSignSyncApp.php`):
- 60-character alphanumeric key required
- HTTPS enforced
- Key validated against store configuration
