# Time Conditions

VitalPBX-style time-based call routing. Routes calls to different destinations based on schedule, holidays, and live overrides.

## Quick Start

### 1. Set up holiday calendar (one-time)

```bash
php artisan db:seed --class=HolidayZaSeeder
```

Or create one manually via **Applications → Holiday Calendars**.

### 2. Create a Time Group (schedule)

Go to **Applications → Business Hours & Holidays → Time Groups → New Group**

Example for "Business Hours":
- Rule 1: weekly, Mon-Thu, 08:00-16:30
- Rule 2: weekly, Fri, 08:00-16:00

### 3. Create a Time Condition

Go to **Applications → Business Hours & Holidays → Time Conditions → New**

Example "Main Office Hours":
- **Entry Code**: `*BH1` (used by inbound routes/IVRs to enter this TC)
- **Toggle Code**: `*888` (dialed to flip OPEN/CLOSED/AUTO)
- **Schedule**: Business Hours
- **Holiday Calendar**: South African Public Holidays
- **Match destination**: IVR "Welcome" or Extension 1003
- **No-Match destination**: Voicemail 1003 or Speed Dial *51
- **Fallback**: Extension 1000 (reception, in case of misconfiguration)

### 4. Point an Inbound Route at it

**Trunks → Inbound Routes → New** → Destination Type: Time Condition → select "Main Office Hours".

## Architecture

### Three Layers

1. **Time Groups** (`time_groups` + `time_rules`) — reusable schedules
2. **Holiday Calendars** (`holiday_calendars` + `holiday_dates`) — reusable holiday lists
3. **Time Conditions** (`time_conditions`) — combine schedule + calendar + destinations

### Decision Logic (in order)

```
IF live override (ASTDB) == OPEN  → MATCH destination
IF live override (ASTDB) == CLOSED → NO-MATCH destination
IF today is in holiday calendar    → NO-MATCH destination
IF current time matches schedule   → MATCH destination
ELSE                                → NO-MATCH destination
```

### Live Overrides via ASTDB

Override state lives in Asterisk's internal database (`ASTDB`):
- `/TC/{id}/state` = `OPEN` | `CLOSED` | (absent = AUTO)
- `/TC/{id}/expires` = unix timestamp (optional)

The dialplan reads `${DB(TC/{id}/state)}` at every call. **No reload needed** to change override.

### Holiday Lookup via ASTDB

Holiday dates are pre-populated daily into ASTDB:
- `/HOLIDAY/{calendar_id}/{YYYY-MM-DD}` = `1`

The dialplan does `Set(TODAY=${STRFTIME(...)})` then `Set(IS_HOLIDAY=${DB(HOLIDAY/{cal}/${TODAY})})` then `GotoIf`.

### Dedicated Context Per TC

Each TC gets its own context: `time-condition-{id}`. Contains:
- `s` — entry, runs decision logic, jumps to `match` or `nomatch`
- `match` — routes to match destination
- `nomatch` — routes to no-match destination
- `i`, `t` — invalid/timeout failsafes route to fallback destination
- `h` — hangup

The contexts are declared in `/etc/asterisk/extensions_timeconditions.conf` (auto-generated, included from `extensions.conf`).

The actual extensions are loaded from the `extensions` table via realtime ODBC.

## Feature Code Behavior

Dialing the toggle code (e.g. `*888`) cycles through three states:

```
AUTO → OPEN → CLOSED → AUTO
```

Each state plays an audio confirmation:
- OPEN: `auth-thankyou`
- CLOSED: `vm-goodbye`
- AUTO: `beep`

If a PIN is configured for the TC, the user must enter it before the toggle takes effect.

## Artisan Commands

```bash
# Regenerate dialplan for all TCs (use after major schema changes)
php artisan timeconditions:sync

# Refresh holiday ASTDB (auto-runs daily at 00:01)
php artisan timeconditions:populate-holidays
php artisan timeconditions:populate-holidays --days=180

# Clear expired temporary overrides (auto-runs every 5 min)
php artisan timeconditions:expire-overrides
```

## Override via API/GUI

The GUI sends a POST to `/applications/timeconditions/{id}/override` with `state=OPEN|CLOSED|AUTO` and optional `expires_at`. This:
1. Writes to `time_conditions.override_mode` (DB record)
2. Writes to ASTDB `/TC/{id}/state` (live in Asterisk)
3. Optionally sets `/TC/{id}/expires` for auto-clearing

## Failsafes

Every TC has multiple safety nets:
1. **Fallback destination** — if `match` or `nomatch` are misconfigured, calls go here
2. **`i` extension** — invalid input handler, routes to fallback
3. **`t` extension** — timeout handler, routes to fallback
4. **`h` extension** — graceful hangup
5. **Validation** prevents:
   - Saving without match/nomatch destinations
   - Code/feature_code collisions
   - Circular routing chains
   - Deleting a TC referenced by inbound routes

A misconfigured TC will never drop a call silently. Worst case: caller reaches the fallback destination (typically reception).

## Files

| File | Purpose |
|------|---------|
| `app/Services/TimeConditionDialplan.php` | Single source of truth for dialplan generation |
| `app/Http/Controllers/Applications/TimeConditionController.php` | TC CRUD + override + test endpoints |
| `app/Http/Controllers/Applications/TimeGroupController.php` | Schedule CRUD |
| `app/Http/Controllers/Applications/HolidayCalendarController.php` | Holiday calendar CRUD |
| `app/Console/Commands/SyncTimeConditions.php` | Manual sync command |
| `app/Console/Commands/PopulateHolidayDb.php` | Daily holiday ASTDB refresh |
| `app/Console/Commands/ExpireTimeConditionOverrides.php` | 5-min expiry cleanup |
| `database/seeders/HolidayZaSeeder.php` | South African public holidays seeder |
| `/etc/asterisk/extensions_timeconditions.conf` | Auto-generated TC context declarations |

## Troubleshooting

### A TC isn't routing correctly
1. Check the dialplan: `mysql -e "SELECT * FROM extensions WHERE context = 'time-condition-{id}'"`
2. Check Asterisk recognizes it: `asterisk -rx "dialplan show time-condition-{id}"`
3. Run a test: in the TC edit page, use the "Test at a specific time" form
4. Check the include file exists: `ls /etc/asterisk/extensions_timeconditions.conf`

### Override doesn't take effect
1. Check ASTDB: `asterisk -rx "database show TC"`
2. The state value should be `OPEN` or `CLOSED` (case-sensitive)
3. Re-trigger from GUI: visit the edit page, click Force OPEN/CLOSED/AUTO

### Holidays aren't being detected
1. Verify holidays exist: `mysql -e "SELECT * FROM holiday_dates WHERE holiday_date >= CURDATE() LIMIT 10"`
2. Refresh ASTDB: `php artisan timeconditions:populate-holidays`
3. Verify ASTDB: `asterisk -rx "database show HOLIDAY"`
4. The TC must have `use_holidays=1` and a linked `holiday_calendar_id`

### Schedule rules not matching
1. Check timezone: `mysql -e "SELECT name, timezone FROM time_groups"`
2. Verify the `dow_mask` format: `Mon-Fri`, `Mon,Wed,Fri`, etc. (capitalized)
3. Test via the controller's evaluate method (Test At Time button)

### Feature code dial does nothing
1. Verify the code exists: `mysql -e "SELECT * FROM extensions WHERE context='from-internal' AND exten='*888'"`
2. Check from-internal context is reachable from your phone (look at your dialplan)
3. Watch the call: `asterisk -rvvv` then dial `*888`
