# Исходящий звонок через Infinity (инстанс Kazakhstan)

**Дата:** 2026-06-04
**Статус:** Утверждён, готов к планированию

## Цель

Дать оператору Polaris в инстансе **Kazakhstan** возможность инициировать исходящий
звонок клиенту прямо из карточки — по аналогии с уже существующим click-to-call для
Peru (Webitel) и Colombia (локальный SIP). Телефония — **Infinity** (`era.smsfinanceit.ru`),
вызов `POST /rest/v1/uc/calls`. Каждый звонок логируется (по образцу `webitel_logs`).

Пример запроса:

```bash
curl --location 'https://era.smsfinanceit.ru/rest/v1/uc/calls' \
  --header 'Authorization: Bearer <token>' \
  --header 'Content-Type: application/json; charset=utf-8' \
  --data '{ "from": "34605433060", "to": "34605433060", "mode": true }'
```

`mode: true` заставляет API дождаться завершения сценария и вернуть итоговый статус
в поле `result` **синхронно** в теле ответа.

## Контекст / что уже есть

- Инстанс `INSTANCE_KAZAKHSTAN` + `isKazakhstan()` уже заведены
  (`constants.php`, `functions.php`).
- Конфиг интеграции `config/integrations.php → integrations.infinity`
  (`base_uri`, `token`, webhook-настройки) уже есть.
- Каркас Infinity: вебхук-подписки, события, `InfinityWebhookService`
  (использует фасад `Http::withToken(...)`), модели, Livewire-компоненты —
  уже реализованы. **Инициатора звонка нет** — это и реализуем.
- Эталон для копирования логики: `WebitelCallService` +
  `MainActionsComponent::initWebitelCall` + кнопка в `phones.blade.php`.

## Решения (зафиксировано с заказчиком)

1. **`from` агента** — переиспользуем существующую колонку `users.webitel_id`
   как extension Infinity (новую колонку не заводим).
2. **`result`** — приходит **синхронно** в ответе на `POST` (`mode: true`).
   Вебхуки `route_fin/dlg_stop` для этой фичи не используются.
3. **`to`** — номер клиента `$phone->telefono` отправляем **как хранится в БД**,
   без префикса кода страны.

## Архитектура (подход B: статус-энам)

`result` — это бизнес-статус, а не HTTP-ошибка, поэтому моделируем его enum-ом,
а не исключениями. Сервис всегда логирует и возвращает DTO; компонент через `match`
решает, что показать оператору.

### Поток данных

```
[Кнопка «Исходящий звонок»] (phones.blade, @if isKazakhstan)
  → JS doCallKazakhstan(phoneId): Swal-подтверждение
  → Livewire.dispatch('initInfinityCall', {phoneId})
  → MainActionsComponent::initInfinityCall(phoneId)
  → InfinityCallService::call(client, user, phone)
        POST {base_uri}/rest/v1/uc/calls
        body: { from: user.webitel_id, to: phone.telefono, mode: true }
        (Bearer integrations.infinity.token; фасад Http, как в InfinityWebhookService)
        → пишет InfinityCallLog (request + response + result) ВСЕГДА (try/finally)
        → парсит result → InfinityCallResult; вытаскивает callId
        → возвращает InfinityCallResultData { result, callId }
  → match(result):
      ok         → externalCommunicationId = callId; setAction(1, phoneId)  // окно коммуникаций
      rejected   → showError «Llamada rechazada. Intente de nuevo»
      timeout    → showError «Tiempo agotado. Intente de nuevo»
      error      → showError «Error técnico. Consulte al administrador.»
      redirected → showError «Llamada redirigida.»
      undefined  → showError «Estado desconocido. Revise el registro»
```

### Таблица интерпретации `result`

| `result`     | Действие в Polaris                                              | Сообщение оператору (ключ перевода)         |
|--------------|----------------------------------------------------------------|---------------------------------------------|
| `ok`         | Звонок инициирован — открыть окно коммуникаций `setAction(1)`   | —                                           |
| `rejected`   | Уведомить о неудаче                                             | «Llamada rechazada. Intente de nuevo»       |
| `timeout`    | Предложить повторить                                            | «Tiempo agotado. Intente de nuevo»          |
| `error`      | Записать в лог-канал, уведомить                                 | «Error técnico. Consulte al administrador.» |
| `undefined`  | Неизвестный статус: лог-канал, уведомить                        | «Estado desconocido. Revise el registro»    |
| `redirected` | Переадресация (3xx): лог-канал, уведомить                       | «Llamada redirigida.»                       |

`InfinityCallResult::fromApi(?string)` маппит неизвестное/`null` значение в `Undefined`.

## Новые компоненты

- `app/Integrations/Infinity/Enums/InfinityCallResult.php` — backed enum
  `Ok|Rejected|Timeout|Error|Redirected|Undefined`; `fromApi(?string): self`;
  метод, отдающий ключ перевода сообщения (для `Ok` — null).
- `app/Integrations/Infinity/DTO/InfinityCallResultData.php` — readonly-DTO
  `{ InfinityCallResult $result, ?string $callId }`.
- `app/Integrations/Infinity/Services/InfinityCallService.php` —
  `call(Client $client, User $user, string $phone): InfinityCallResultData`.
  Фасад `Http::withToken(config('integrations.infinity.token'))`. Логирует через
  `InfinityCallLog` (создаёт запись до запроса, дописывает response/result в `finally`).
  Ошибки HTTP логирует в лог-канал (как `InfinityWebhookService` — `stdout`).
- `app/Models/Collection/InfinityCallLog.php` — модель (connection `collection`,
  `$timestamps = false`, cast `call_date => datetime`).
- Миграция `infinity_call_logs` (connection `collection`), колонки по образцу
  `webitel_logs` плюс `result`:
  `id, document (nullable), extension (nullable), agent_id (nullable),
   client_id (nullable), request (text nullable), response (text nullable),
   result (string nullable), call_date (datetime nullable)`.

## Изменения существующих файлов

- `app/Livewire/Client/MainActionsComponent.php`
  - `getListeners()`: добавить `'initInfinityCall' => 'initInfinityCall'`.
  - Новый публичный метод `initInfinityCall(int $phoneId): void` (см. поток данных).
- `resources/views/livewire/client/phones.blade.php`
  - Блок `@if(isKazakhstan())` с кнопкой звонка (иконка `la-phone-volume`),
    по образцу блока Peru.
  - JS-функция `doCallKazakhstan(phoneId)`: Swal-подтверждение →
    `Livewire.dispatch('initInfinityCall', {phoneId})`.
- `resources/views/livewire/main-actions.blade.php`
  - Лоадер (строка ~8): добавить `initInfinityCall` в `wire:target`.
- `resources/lang/{es,ru,en}/views.php`
  - Ключи для 5 сообщений результата + подтверждения звонка
    (под `views.client.phones`).

## Обработка ошибок

- Сетевой сбой / неуспешный HTTP-код (`->throw()` бросает `RequestException`):
  запись `InfinityCallLog` уже зафиксирована в `finally`; компонент ловит
  `\Throwable`, показывает «Error técnico. Consulte al administrador.».
- Любой `result ≠ ok` уже сохранён в `InfinityCallLog.result`; `error/undefined/
  redirected` дополнительно пишутся в лог-канал.

## Тесты (PHPUnit + `Http::fake()`)

- Feature-тест `MainActionsComponent::initInfinityCall`: для каждого `result`
  (`ok/rejected/timeout/error/redirected/undefined`) — корректный dispatch
  (`setAction`/`showError` с нужным сообщением) и наличие записи `InfinityCallLog`
  с правильным `result`.
- Feature-тест: HTTP-сбой → `showError` техническое сообщение + лог записан.
- Unit-тест `InfinityCallResult::fromApi`, включая `null` и мусор → `Undefined`.

## Открытые вопросы

- Точное имя поля id звонка в ответе (`id` / `call_id`) неизвестно — берём `id`
  с фоллбэком на `null`. Полный ответ логируется в `InfinityCallLog.response`,
  поэтому поправить тривиально после первого реального ответа.

## Вне области (YAGNI)

- Обработка результата звонка через вебхуки (выбрана синхронная схема).
- Формат-хелперы телефона для Kazakhstan (`toPhone`/`getCountryPhoneCode`) —
  номер шлём как есть.
- Изменения логики для Peru/Colombia/Mexico.
