# Як налаштувати UniTalk

Підключіть UniTalk до ConnectiveOne через вбудований preset **UniTalk**: завершені дзвінки з'являтимуться в архіві дзвінків із записом, призначеним оператором і полями в картці клієнта.

Ця інструкція — повний гайд для імплементації: preset, webhook, два способи призначення оператора (з готовими JSON), поля клієнта, smoke-тест і типові проблеми.

## Передумови

- Доступ адміністратора до кабінету UniTalk
- Доступ до налаштувань бота в ConnectiveOne (**Налаштування → Боти → Телефонія**)
- Email операторів у ConnectiveOne збігаються з email у UniTalk (для автопризначення)
- Для **шляху A** (рекомендовано): таблиця SIP-лінія (**line** у UniTalk) ↔ email оператора
- Для **шляху B** (опційно): API-ключ UniTalk (**Інтеграція → API**)

## Коли знадобиться

- UniTalk — провайдер телефонії
- Потрібно швидко підключити інтеграцію без ручного маппінгу полів webhook
- Завершені дзвінки мають з'являтися в Операторській панелі з записом і оператором

## Два способи призначити оператора

| | Шлях A — `operator_mapping` | Шлях B — `action_telephony_staff_enrich` |
|---|---|---|
| Суть | Статична таблиця SIP → email у Advanced Config | Запит до UniTalk API `/users/list`, матч по полю **`line`** |
| Зовнішні API | Ні | Так (кеш обовʼязковий через ліміти UniTalk) |
| Коли обирати | Відомий стабільний список ліній (типовий кейс First / більшості клієнтів) | Лінії часто змінюються, таблицю не хочете вести вручну |
| Що вказати в каналі | Блок `operator_mapping` у JSON | `API Key` + **Action збагачення webhook** = `action_telephony_staff_enrich` |

> Якщо спрацював `operator_mapping`, ConnectiveOne **не викликає** enrich — зайвих запитів до API немає.

`action_telephony_staff_enrich` — **універсальний** internal action (будь-який HTTP-список співробітників через json_config). Нижче — готова конфігурація саме під UniTalk.

---

## Крок 1: Відкрити налаштування каналу

1. **Налаштування** → **Боти**
2. Оберіть бот
3. Вкладка **Телефонія**
4. Створіть новий канал або відкрийте існуючий

Заповніть мінімум:

| Поле каналу | Значення |
|-------------|----------|
| Назва | Наприклад `UniTalk` |
| Активний | Увімкнено |
| Код країни за замовчуванням | Наприклад `UA` / `380` (як у вашому інстансі) |

Після першого збереження зʼявляться **Webhook URL** і токен.

## Крок 2: Застосувати preset UniTalk

1. У блоці **Розширена конфігурація (JSON)** оберіть у списку провайдера **UniTalk**
2. Натисніть **Застосувати пресет**
3. Натисніть **Зберегти** і скопіюйте **Webhook URL** (з `?token=...`)

Preset додає:

- `event_detection` — лише подія `CALL_END`
- `field_mapping` / `status_mapping` / `direction_mapping`
- `client_field_mapping` — UTM, джерело, коментар → картка клієнта
- записи з `call.link`

Повний JSON preset (без `operator_mapping`) також є в репозиторії: `apps/engine/modules/extra/telephony/presets/unitalk.json`.

## Крок 3: Webhook у кабінеті UniTalk

1. Кабінет UniTalk → **Обробка подій**
2. Додайте обробник **«Надіслати вебхук»**
3. Привʼяжіть **лише** до події **`CALL_END`** (не надсилайте `CALL_NEW` тощо)

| Поле в UniTalk | Значення |
|----------------|----------|
| **Метод** | `POST` |
| **URL** | Повний **Webhook URL** з ConnectiveOne, наприклад: `https://<ваш-домен>/kw/telephony/webhook/<id_каналу>?token=<webhook_token>` |
| **Тіло** | **Стандартний JSON** |

> Токен тільки в URL (`?token=...`). Шлях `/kw/telephony/webhook/...` не змінюйте.

### Приклад тіла `CALL_END` (вхідний)

```json
{
  "event": "CALL_END",
  "call": {
    "id": "sample-call-id",
    "from": "380501234567",
    "to": "9578",
    "direction": "in",
    "state": "ANSWER",
    "secondsFullTime": 120,
    "secondsTalk": 90,
    "date": "2026-09-02T12:00:00.000Z",
    "link": "https://example.test/recording.mp3",
    "utmSource": "google",
    "utmCampaign": "brand",
    "source": "ads",
    "comment": "Коментар до дзвінка"
  }
}
```

| Поле UniTalk | Після preset у ConnectiveOne |
|--------------|------------------------------|
| `event` | Має бути `CALL_END`, інакше відповідь 200 ignored |
| `call.id` | ID дзвінка (ідемпотентність — повтор не створить дубль) |
| `call.from` / `call.to` | Номер клієнта або SIP оператора (залежить від `direction`) |
| `call.direction` | `in` / `out` |
| `call.state` | `ANSWER` → прийнятий; `NOANSWER`, `BUSY`, … → пропущений |
| `call.link` | Запис розмови |
| `call.utmSource`, `utmCampaign`, `source`, `comment` | Картка клієнта (`client_field_mapping`) |

**Напрямок і SIP оператора:**

| Напрямок | Номер клієнта | SIP-лінія оператора (ключ у `operator_mapping` / lookup enrich) |
|----------|---------------|------------------------------------------------------------------|
| `in` (вхідний) | `call.from` | `call.to` |
| `out` (вихідний) | `call.to` | `call.from` |

У формі каналу ConnectiveOne: формат webhook = **JSON**.

---

## Крок 4A: Призначення оператора через `operator_mapping` (рекомендовано)

Після **Застосувати пресет** додайте в той самий JSON блок `operator_mapping` (на одному рівні з `event_detection`, `field_mapping`, …) і натисніть **Валідувати JSON** / **Зберегти**.

### Приклад фрагмента

```json
"operator_mapping": {
  "source": {
    "in": "callee_number",
    "out": "caller_number"
  },
  "map": {
    "9578": "operator@example.com",
    "201": "operator2@example.com"
  }
}
```

### Повний приклад Advanced Config (preset + mapping)

Скопіюйте, підставте свої SIP → email, збережіть:

```json
{
  "event_detection": {
    "mode": "event_field",
    "event_field": "event",
    "call_ended_values": ["CALL_END"]
  },
  "field_mapping": {
    "call_id": "call.id",
    "caller_number": "call.from",
    "callee_number": "call.to",
    "status": "call.state",
    "duration": "call.secondsFullTime",
    "talk_duration": "call.secondsTalk",
    "recording_url": "call.link",
    "direction": "call.direction",
    "date": "call.date",
    "utm_source": "call.utmSource",
    "utm_campaign": "call.utmCampaign",
    "call_source": "call.source"
  },
  "status_mapping": {
    "ANSWER": "answered",
    "NOANSWER": "missed",
    "BUSY": "missed",
    "CANCEL": "missed",
    "AUTOCANCEL": "missed",
    "UNREACHABLE": "missed",
    "VOICE_MAIL": "missed",
    "FAIL": "failed",
    "BLOCKED": "failed",
    "AUDIO_ERR": "failed"
  },
  "direction_mapping": {
    "in": "in",
    "out": "out",
    "inbound": "in",
    "outbound": "out"
  },
  "recordings": {
    "enabled": true,
    "download": false
  },
  "transcription": {
    "enabled": false,
    "source": "provider"
  },
  "client_field_mapping": {
    "utm_source": "call.utmSource",
    "utm_campaign": "call.utmCampaign",
    "call_source": "call.source",
    "last_call_comment": "call.comment"
  },
  "operator_mapping": {
    "source": {
      "in": "callee_number",
      "out": "caller_number"
    },
    "map": {
      "9578": "operator@example.com",
      "201": "operator2@example.com"
    }
  }
}
```

**Правила:**

1. Ключі в `map` — **сирі** SIP-рядки як у UniTalk (`"9578"`), **до** нормалізації E.164 (не `+3809578`).
2. Email у ConnectiveOne = email оператора (активний користувач Операторської панелі).
3. У UniTalk у кожного оператора заповнена **внутрішня лінія** (`line`).
4. Поле **Action збагачення webhook** можна залишити порожнім.

---

## Крок 4B: Динамічне призначення через `action_telephony_staff_enrich` (опційно)

Використовуйте, якщо **не** ведете `operator_mapping` (або як запасний шлях, коли лінії в таблиці немає).

### 4B.1 Поля каналу

| Поле | Значення |
|------|----------|
| **API Key** | API-ключ UniTalk (**Інтеграція → API**) |
| **API Base URL** (якщо є в формі) | `https://api.unitalk.cloud/api` |
| **Action збагачення webhook** | `action_telephony_staff_enrich` |

Ключ — секрет. Не публікуйте в чатах / тікетах.

### 4B.2 Параметри action (json_config)

Action уже є в продукті (internal). Якщо на інстансі використовуєте Action Jail-обгортку — ті самі параметри в **JSON config** action.

Готовий приклад під UniTalk:

```json
{
  "api_url": "https://api.unitalk.cloud/api/users/list",
  "http_method": "POST",
  "auth_header_name": "Authorization",
  "auth_scheme": "raw",
  "request_body": {},
  "response_items_path": "",
  "match_field": "line",
  "lookup_source": {
    "in": "callee_number",
    "out": "caller_number"
  },
  "email_field": "email",
  "name_fields": ["firstName", "lastName"],
  "cache_ttl_seconds": 300,
  "timeout_ms": 8000
}
```

| Параметр | Навіщо |
|----------|--------|
| `match_field`: **`line`** | Внутрішня SIP-лінія АТС у відповіді `/users/list` (не плутати з `phone` — мобільний, часто `null`) |
| `lookup_source` | Звідки брати SIP з нормалізованого дзвінка: in → `callee_number`, out → `caller_number` |
| `cache_ttl_seconds` | Кеш списку співробітників (ліміти UniTalk: ~15 req/s) |
| `api_key` | Можна в json_config **або** лише в полі каналу **API Key** (канал має пріоритет як джерело секрету, якщо в params немає ключа) |

Якщо відповідь API — масив у корені, `response_items_path` залиште порожнім `""`. Якщо обгортка на кшталт `{ "data": [ … ] }` — вкажіть `"data"`.

### 4B.3 Що має повернути UniTalk `/users/list` (фрагмент)

```json
[
  {
    "id": 1,
    "email": "operator@example.com",
    "firstName": "Іван",
    "lastName": "Петренко",
    "line": "9578",
    "phone": null,
    "role": "OPERATOR"
  }
]
```

ConnectiveOne шукає запис з `line` = SIP з дзвінка і пише `manager_email` / `manager_name` у збагачення → призначення за email.

Деталі загального механізму enrich: [Налаштувати збагачення webhook](/uk/telephony/how-to/configure-webhook-enrich.md).  
Технічний шаблон для впровадників: `docs/developer/engine/modules/telephony/action_telephony_staff_enrich.template.md`.

---

## Крок 5: Поля картки клієнта

Preset уже містить `client_field_mapping`:

| Ключ у `extra_data` | Шлях у webhook |
|---------------------|----------------|
| `utm_source` | `call.utmSource` |
| `utm_campaign` | `call.utmCampaign` |
| `call_source` | `call.source` |
| `last_call_comment` | `call.comment` |

Щоб поля **відображались** у картці клієнта, додайте відповідні кастомні поля в **Налаштування інстансу** (схема `extra_data`). Сам merge у клієнта робить Telephony без Action Jail.

---

## Крок 6: Збереження та перевірка (smoke)

1. **Зберегти** канал (активний).
2. У формі каналу — **Тест** webhook **або** curl нижче.
3. Зробіть тестовий **вхідний** і **вихідний** дзвінок у UniTalk.
4. **Операторська панель → Закриті звернення**: діалог `telephony`, запис (якщо є `call.link`), оператор.

### Приклад curl (локальний / stage smoke)

Підставте URL каналу і токен. Тіло — як у кроці 3; для шляху A змініть `"to"` / email у `operator_mapping` під реальну лінію.

```bash
curl -X POST 'https://<ваш-домен>/kw/telephony/webhook/<channel_id>?token=<webhook_token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "event": "CALL_END",
    "call": {
      "id": "smoke-unitalk-001",
      "from": "380501234567",
      "to": "9578",
      "direction": "in",
      "state": "ANSWER",
      "secondsFullTime": 60,
      "secondsTalk": 45,
      "date": "2026-09-02T12:00:00.000Z",
      "link": "https://example.test/recording.mp3",
      "utmSource": "google",
      "utmCampaign": "brand",
      "source": "ads",
      "comment": "Smoke test"
    }
  }'
```

Очікувано: HTTP 200, у тілі є `dialogue_id`.  
Повтор з тим самим `call.id` → 200, «already processed», без дубля діалогу.

Подія не `CALL_END` (наприклад `"event": "CALL_NEW"`) → 200 ignored, діалог не створюється.

### Чекліст приймання

| # | Перевірка | Очікування |
|---|-----------|------------|
| 1 | `CALL_END` | Діалог створено, канал telephony, завершений |
| 2 | Не-`CALL_END` | 200 ignored, без діалогу |
| 3 | Є `call.link` | Запис доступний у діалозі |
| 4 | Вхідний / вихідний | Клієнт за правильним номером |
| 5 | SIP in і out | Оператор призначений (mapping або enrich) |
| 6 | UTM / comment | Значення в картці клієнта (якщо поля в Instance Settings) |
| 7 | Той самий `call.id` двічі | Без дубля |
| 8 | Шлях B | Кеш / без rate-limit блокування; або шлях A без enrich |

---

## Типові проблеми

| Симптом | Що перевірити |
|---------|----------------|
| 200 ignored на кожному webhook | У тілі є `"event": "CALL_END"`; застосовано preset UniTalk (`event_detection`) |
| Діалог є, оператора немає | Email у `map` / UniTalk збігається з користувачем C1; SIP як `"9578"`, не E.164; для out — лінія в `call.from` |
| Enrich не викликається | Заповнений `operator_mapping` з матчем — це очікувано (skip enrich) |
| Enrich error / немає email | `match_field` = `line`; у `/users/list` у оператора заповнений `line`; **API Key** на каналі |
| Дубль діалогів | Різні `call.id` від UniTalk; ідемпотентність лише за одним id |
| UTM не видно в UI | Merge в `extra_data` є — додайте відображення кастомних полів у Instance Settings |
| 403 / unauthorized webhook | Токен у URL = токен каналу |

---

## Що включає preset UniTalk

- Детекція лише `CALL_END`
- Маппінг полів, статусів, напрямку
- Записи з `call.link`
- `client_field_mapping` (UTM, джерело, коментар)

`operator_mapping` і webhook enrich **не** входять у кнопку preset — додаються вручну (кроки 4A / 4B).

## Обмеження

- Лише завершені дзвінки (`CALL_END`), без realtime під час розмови
- Click-to-call з ConnectiveOne — поза цим обсягом
- Транскрипт / аналітика UniTalk — окремий scope

## Повʼязана документація

- [Налаштування каналу телефонії](/uk/telephony/how-to/configure-telephony-channel.md)
- [Налаштувати збагачення webhook](/uk/telephony/how-to/configure-webhook-enrich.md)
- [Тестування webhook](/uk/telephony/how-to/test-telephony-webhook.md)
- [Перегляд дзвінків в Операторській панелі](/uk/telephony/how-to/view-calls-in-operator-panel.md)
- [Адмін-хаб телефонії](/uk/telephony/admin-hub.md)
