# GLN & Survey Prompt Language

GLN and Survey share one language rule: **the language is decided by the first queue the call entered.**

- Multi-language is a **per-company** setting (`Enable Multi-Language`) and is **off by default**.
- When enabled, the first queue name **ends with `EN`** (case-insensitive) → **English** prompts.
- Anything else, the first queue cannot be identified, or multi-language is disabled → **Arabic** prompts (default).
- When disabled, the queue log and queue name are not queried at all; there is no language-detection database work.
- Redis caches the **first queue number**, not the language. The language is re-derived from the queue name on every enabled run.
- The post-GLN destination is the per-company **GLN Destination Queue** setting; when it is empty the AGI falls back to queue **700**. The General Settings **Default Queue** (`call_default_queue`) is not used by GLN.

## 1. Call flow

```
DID / IVR
  └─ ext-queues,<queue>,1
       Set(QUEUENUM=<queue>)          FreePBX queue dialplan
       QueueLog(<queue>,...,DID,...)  FreePBX queue dialplan
       Queue(...)                     Asterisk writes ENTERQUEUE
  └─ queue destination / Custom Destination
       custom-gln-dest,s,1     →  AGI agi:gln-handler
       custom-survey-dest,s,1  →  AGI agi:survey-handler
  └─ GLN only: Goto ext-queues,<gln_destination_queue|700>,1
```

The queue `dest` may point directly at `custom-gln-dest` / `custom-survey-dest`, or at a FreePBX Custom Destination that Gotos those contexts on the same channel.

## 2. How the first queue becomes the language

When the company setting `Enable Multi-Language` is enabled, `App\Services\PBX\CallLanguageResolver::resolve()` runs the same steps for both AGIs:

1. Reads `UNIQUEID` and `CHANNEL(LINKEDID)` from the channel.
2. Asks `FirstQueueRegistry` (Redis, see §3) for the queue already resolved for this `LINKEDID`.
3. If Redis has nothing, queries `asteriskcdrdb.queuelog` for the **first** row of the call:

```sql
SELECT q.queuename
FROM asteriskcdrdb.queuelog q
WHERE q.event IN ('DID','ENTERQUEUE')
  AND q.queuename NOT IN ('NONE','')
  AND (q.callid = :uniqueid
       OR q.callid IN (SELECT uniqueid FROM asteriskcdrdb.cdr WHERE linkedid = :linkedid))
ORDER BY q.id
LIMIT 1
```

4. Reads `asterisk.queues_config.descr` for that queue number.
5. `preg_match('/EN$/i', $descr)` → English, otherwise Arabic.

**Why not `QUEUENUM`?** The queue dialplan sets it before `Queue()`, but every queue the call enters overwrites it, so at AGI time it holds the **most recent** queue. It is logged as `current_queue_number` for diagnostics only and never drives the language.

**Why the `cdr` sub-query?** Earlier legs of the same call (before a transfer) have already ended, so they have CDR rows, but their queue rows are filed under a different `UNIQUEID`. The `linkedid → cdr.uniqueid` sub-query includes them, so the original queue is kept.

| First queue `descr` | Language |
| --- | --- |
| `test en`, `TEST EN`, `queue-en` | English |
| `test ar`, `Marketing Group` | Arabic |
| `EN-GLN queue` (contains but does not end with `EN`) | Arabic |
| No queue log rows / no identifiers | Arabic + warning |
| Queue number missing from `queues_config` | Arabic + warning |

Missing information is never replaced by a later queue. The resolver logs `First queue for the call could not be identified; using the Arabic prompts` and returns `language_source = missing`.

## 3. Redis: one queue per call, reused by GLN and Survey

`App\Services\PBX\FirstQueueRegistry`

| Property | Value |
| --- | --- |
| Key | `call-first-queue:<linkedid>` |
| Value | queue number (e.g. `702`) |
| TTL | `86400` s (24 h) |
| Write | `SET key value EX 86400 NX` — the first AGI wins, later queues cannot overwrite |

**Why it is needed:** Survey can run on a **different channel** after an agent transfer to the Survey destination. That channel shares only the `LINKEDID`; its `UNIQUEID` has no queue log rows and its CDR row does not exist yet. Without the cache, Survey would fall back to Arabic while GLN used English.

Lookup order in every AGI: **Redis → queuelog**. A Redis read or write failure is logged (`First queue cache read failed` / `First queue cache write failed`) and never breaks the call; the queue-log lookup remains the fallback.

> This is **not** the GLN digits key. Captured GLN digits are stored separately under `call-gln:<linkedid>` (TTL 7 days) for the softphone. Language detection only uses `call-first-queue:*`.

## 4. Prompt paths (`AgiPromptPaths`)

| Flow | Arabic | English |
| --- | --- | --- |
| GLN | `en/custom/gln` | `en/custom/gln-en` |
| Survey Q1 | `en/custom/survey` | `en/custom/survey-en` |
| Survey Q2 | `en/custom/survey-q2` | `en/custom/survey-q2-en` |
| Survey Q3 | `en/custom/survey-q3` | `en/custom/survey-q3-en` |
| Survey Q4 | `en/custom/survey-q4` | `en/custom/survey-q4-en` |
| Survey Q5 | `en/custom/survey-q5` | `en/custom/survey-q5-en` |
| Survey thank you | `en/custom/survey-thanks` | `en/custom/survey-thanks-en` |

Arabic paths are unchanged. English paths are the same name with `-en` inserted before the extension. The AGIs **never** check, create, copy, rename or move audio files — the prompts must already exist. Asterisk resolves paths under `/var/lib/asterisk/sounds`, so `en/custom/gln-en` means `/var/lib/asterisk/sounds/en/custom/gln-en.wav`.

## 5. How the components connect

```
dialplan/extensions_custom.conf  [custom-gln-dest]
  └─ AgiGlnHandler            (php artisan agi:gln-handler)
      └─ AGIGlnController::run()
          ├─ GlnPromptResolver::resolve()
          │    └─ CallLanguageResolver ─┬─ FirstQueueRegistry (Redis call-first-queue:*)
          │                              ├─ asteriskcdrdb.queuelog  (first DID/ENTERQUEUE)
          │                              └─ asterisk.queues_config.descr
          ├─ AgiPromptPaths::glnPromptForLanguage()
          ├─ getData(prompt) → digits → Redis call-gln:<linkedid>
          ├─ CallLogSyncService::saveGln()
          └─ Goto ext-queues,<gln_destination_queue|700>,1

dialplan/extensions_custom.conf  [custom-survey-dest]
  └─ AgiSurveyHandler         (php artisan agi:survey-handler)
      └─ AGISurveyController::run()
          ├─ resolveCallLanguage() → CallLanguageResolver   (same rule, same Redis key)
          ├─ AgiPromptPaths::surveyPromptsForLanguage()
          └─ AgiPromptPaths::surveyThanksPromptForLanguage() (when SURVEY_PLAY_THANKS_PROMPT is unset/true)
```

Both AGIs instantiate `CallLanguageResolver` themselves, so the rule and the Redis key are shared by construction.

## 6. Changed files

| File | Change | Why |
| --- | --- | --- |
| `app/Services/PBX/CallLanguageResolver.php` | New | Resolves the first queue and maps its name to `ar`/`en`; single source of truth for both AGIs |
| `app/Services/PBX/FirstQueueRegistry.php` | New | Persists the first queue per `LINKEDID` in Redis (24 h, `SET NX`) so Survey reuses GLN's decision |
| `app/Services/PBX/AgiPromptPaths.php` | New | Maps language → prompt paths; adds the `-en` suffix before the extension |
| `app/Services/PBX/GlnPromptResolver.php` | New | Combines the language resolver with the GLN prompt selection |
| `app/Http/Controllers/AGIGlnController.php` | New (replaces `AGIGdnController`) | GLN AGI: logs the selection, captures digits, saves them, returns the caller to the selected GLN destination queue |
| `app/Console/Commands/AgiGlnHandler.php` | New (replaces `AgiGdnHandler`) | `agi:gln-handler` entry point called from the dialplan |
| `app/Console/Kernel.php` | Modified | Registers `AgiGlnHandler` |
| `app/Http/Controllers/AGISurveyController.php` | Modified | Uses the shared resolver, localized question/thank-you prompts, and logs `Survey language resolved` |
| `dialplan/extensions_custom.conf` | Modified | `[custom-gln-dest]` now calls `agi:gln-handler` |
| `app/Console/Commands/EgyInstall.php` | Modified | Deploys `egy:dialplan` during install |
| `tests/Unit/CallLanguageResolverTest.php`, `tests/Unit/AgiPromptPathsTest.php`, `tests/Unit/GlnPromptResolverTest.php`, `tests/Feature/AGIGlnControllerTest.php`, `tests/Feature/AGISurveyControllerTest.php` | New | Cover the language rule, prompt paths, Redis reuse across channels, and both AGI flows |
> The `GDN → GLN` identifier rename (`call_logs.gln`, migration `2026_10_28_000001_add_gln_to_call_logs_table`, softphone `glnEnabled`, etc.) is a separate change and is not part of the language logic described here.

## 7. Diagnostics

Each AGI logs the decision before playback (no caller numbers, no GLN digits):

```
GLN prompt selected {"linkedid":"...","queue_number":"702","queue_name":"test en","current_queue_number":"701","language":"en","language_source":"first_queue","playback_path":"en/custom/gln-en"}
Survey language resolved {"linkedid":"...","queue_number":"702","queue_name":"test en","current_queue_number":"702","language":"en","language_source":"first_queue","thank_you_prompt":"en/custom/survey-thanks-en"}
```

`language_source` is `first_queue` when the first queue was found and `missing` otherwise. `queue_number` is the first queue; `current_queue_number` is only the last queue on the channel.

## 8. Applying on a new server

**1. Code** — deploy the repository (includes migrations, `dialplan/`, this document).

**2. Migrations** (required)
```sh
php artisan migrate --force
```
On a completely fresh server (no `storage/installed` marker), `php artisan egy:install` does this for you: it runs `migrate`, `pbx:generate-credentials`, `db:seed`, then `egy:dialplan`.

**3. Redis** (required)
Configure `REDIS_HOST` / `REDIS_PORT` / `REDIS_PASSWORD` in `.env` and confirm the connection works. No keys are created by hand: `call-first-queue:<linkedid>` is written on the first AGI of each call and expires after 24 h.

**4. Audio files** (required, manual)
The GLN/Survey prompts are **not** shipped in `sounds/` and `php artisan egy:sounds` does not copy them. Place these files under `/var/lib/asterisk/sounds/en/custom/`:

- GLN: `gln.wav`, `gln-en.wav`
- Survey: `survey.wav`, `survey-en.wav`, `survey-q2.wav` … `survey-q5.wav`, `survey-q2-en.wav` … `survey-q5-en.wav`, `survey-thanks.wav`, `survey-thanks-en.wav`

Verify:
```sh
ls -1 /var/lib/asterisk/sounds/en/custom/ | grep -E 'gln|survey'
```

**5. Dialplan** (required)
```sh
php artisan egy:dialplan     # copies dialplan/* to /etc/asterisk/ and runs fwconsole reload
```
Then in FreePBX point the queue destination (or a Custom Destination) at:
- `custom-gln-dest,s,1` for GLN capture
- `custom-survey-dest,s,1` for the survey

**6. Queue naming** (required for English)
The first queue's **description** (`asterisk.queues_config.descr`) must end with `EN` for English calls. Rename the queue in FreePBX if needed; no code change is required.

**7. Queue workers** (required)
```sh
php artisan queue:restart          # or: supervisorctl restart laravel-queue
```
Run this after **every** deploy. Long-running workers keep the classes they loaded in memory; code or column renames will otherwise fail silently inside jobs.

**8. Clear caches** (if you cache config)
```sh
php artisan optimize:clear
```

**9. Verify**
1. Place a test call into an English-named queue, then an Arabic one.
2. Check the log: `GLN prompt selected` / `Survey language resolved` — `language_source` should be `first_queue` and `playback_path` should carry `-en` only for English.
3. Check the cache during the call: `redis-cli GET call-first-queue:<linkedid>` → returns the **first** queue number.
4. Confirm the logged prompt exists as `.wav` and plays in the expected language.
5. After an agent transfer into Survey, the `Survey language resolved` line must show the **same** `queue_number` as GLN.
