# Автодозвон SuperSam — детальный план реализации (БД → API → планировщик → UI) > **Дата:** 01.09.2026 · **Статус АТС:** диалоговая машина и голос Vivienne проверены живьём. > **Контракт реализации:** каждая фаза — отдельный PR, сначала dev (dev.mkn8n.ru), prod — только после проверки Михаилом. Mobile-first: реестр и журнал — подписанные строки на viewport 390px, таблица только desktop. Термины/строки в UI и SQL — ровно те, что уже используются в приложении («Первая половина дня» и т.п.). --- ## Фаза 0 — Заморозка контрактов (нулевой код, только согласование) **Цель:** зафиксировать значения, вокруг которых всё строится, чтобы не разъехались UI, БД и AGI. **Файл:** `docs/autocall-contract.md` (новый, в репо). Содержимое: 1. **Кампания-ключ:** `campaign_type='autocall_slot'` (первый сценарий). Резерв ключей: `autocall_storage`, `autocall_reminder`, `autocall_confirm_morning`. 2. **Сценарии звонка:** `A` (день+время), `B` (время, день=завтра), `C` (подтверждение выбранного, молчание=да). 3. **Outcome-значения `autocall_log.outcome` (единственный enum, на него смотрит UI):** `confirmed`, `confirmed_kept_previous`, `confirmed_silent`, `no_answer`, `no_input_day`, `no_input_half`, `operator`, `opt_out`, `invalid_number`, `error`. 4. **DTMF-семантика:** `dtmf_day`: 1=завтра, 2=послезавтра; `dtmf_half`: 1=первая половина дня, 2=вторая. Больше ничего. 0/9/* — не сохраняются как ответ, а формируют outcome. 5. **Записи в `order_groups` при `confirmed*`:** `delivery_date/pickup_date`, `delivery_time/pickup_time_slot`, `delivery_date_source='autocall'`, `delivery_status='agreed'`, `notification_status='confirmed'`, `called=true`, `called_at`, `called_comment='Согласовано роботом: …'`. 6. **Эскалация:** `operator`, `opt_out`, `invalid_number`, `no_input×no_input_escalate_after`, `attempts≥max_attempts` → `notification_status='manual_required'` (плюс `autocall_opt_out=true` только для 9). 7. **Тайминг:** отсчёт от `sms_sent_at`, пауза `delay_after_sms_minutes` (90), `retry_interval_minutes` (120), попытки в день `attempts_per_day` (3), следующий день `next_day_attempt_times` (`10:00,14:00`), окно `work_hours_start/end` 9–20, `work_days` пн–сб. Все — из БД, не хардкод. 8. **Тест:** `test_mode=true` → набор `test_phone`; `was_test_mode=true` пишем в лог. **Проверка:** файл открыт и подтверждён Михаилом в PR-описании. --- ## Фаза 1 — База данных (миграция dev, потом prod) ### Файлы - Создать `supabase/migrations/20260901_autocall_base.sql` (модель: один файл, применяем как остальные DDL — см. правило: DDL только `psql -U supabase_admin`). ### Что делает ```sql -- 1) лог звонков create table if not exists public.autocall_log ( id bigint generated always as identity primary key, order_group_id uuid references public.order_groups(id) on delete set null, campaign_type text not null default 'autocall_slot', customer_phone text not null, attempt_no smallint not null, scenario text not null check (scenario in ('A','B','C')), call_started_at timestamptz not null, answered_at timestamptz, duration_sec smallint, sip_code smallint, channel_state text, dtmf_day smallint check (dtmf_day in (1,2) or dtmf_day is null), dtmf_half smallint check (dtmf_half in (1,2) or dtmf_half is null), changed boolean not null default false, outcome text not null check (outcome in ( 'confirmed','confirmed_kept_previous','confirmed_silent', 'no_answer','no_input_day','no_input_half', 'operator','opt_out','invalid_number','error')), was_test_mode boolean not null default false, asterisk_uniqueid text, created_at timestamptz not null default now() ); create index if not exists idx_autocall_log_group on public.autocall_log(order_group_id, created_at desc); create index if not exists idx_autocall_log_phone on public.autocall_log(customer_phone, created_at desc); create index if not exists idx_autocall_log_outcome on public.autocall_log(outcome, created_at desc); create index if not exists idx_autocall_log_uniq on public.autocall_log(asterisk_uniqueid); comment on table public.autocall_log is 'Лог робо-звонков (автодозвон через АТС клиента). UI читает outcome.'; grant select on public.autocall_log to authenticated; -- 2) order_groups alter table public.order_groups add column if not exists autocall_opt_out boolean not null default false; create index if not exists idx_order_groups_autocall_optout on public.order_groups(autocall_opt_out) where autocall_opt_out = true; -- 3) новые колонки настроек (если их ещё нет) alter table public.sms_campaign_settings add column if not exists delay_after_sms_minutes int; alter table public.sms_campaign_settings add column if not exists retry_interval_minutes int; alter table public.sms_campaign_settings add column if not exists attempts_per_day int; alter table public.sms_campaign_settings add column if not exists no_input_escalate_after int; alter table public.sms_campaign_settings add column if not exists next_day_attempt_times text; alter table public.sms_campaign_settings add column if not exists call_timeout_seconds int; -- 4) строка кампании (в боевом режиме — выключена и тестовая) insert into public.sms_campaign_settings (campaign_type, enabled, test_mode, test_phone, work_hours_start, work_hours_end, work_days, max_attempts, delay_after_sms_minutes, retry_interval_minutes, attempts_per_day, no_input_escalate_after, next_day_attempt_times, call_timeout_seconds) values ('autocall_slot', false, true, '9067029953', 9, 20, '1,2,3,4,5,6', 5, 90, 120, 3, 2, '10:00,14:00', 30) on conflict (campaign_type) do nothing; -- 5) представление флагов для реестра (без N+1) create or replace view public.order_group_comms_flags as select og.id, og.autocall_opt_out, og.delivery_date_source, og.notification_status, og.delivery_status, (select l.outcome from public.autocall_log l where l.order_group_id = og.id order by l.created_at desc limit 1) as last_call_outcome, (select l.changed from public.autocall_log l where l.order_group_id = og.id order by l.created_at desc limit 1) as last_call_changed, (select count(*) from public.autocall_log l where l.order_group_id = og.id) as call_attempts, (select count(*) from public.autocall_log l where l.order_group_id = og.id and l.outcome in ('no_input_day','no_input_half')) as no_input_count from public.order_groups og; grant select on public.order_group_comms_flags to authenticated; ``` ### Применение и проверка - Dev: `docker exec supabase-db psql -U supabase_admin -d postgres -f /tmp/20260901_autocall_base.sql` - Проверки: `\d autocall_log`; `select campaign_type from sms_campaign_settings where campaign_type='autocall_slot'`; `select * from order_group_comms_flags limit 1`. --- ## Фаза 2 — Edge function `autocall-result` ### Файлы - Создать `supabase/functions/autocall-result/index.ts` - Общие helpers: `supabase/functions/_shared/` (используем существующие cors/service-role паттерны из `confirm-delivery-choice`). ### Контракт endpoint'а `POST /functions/v1/autocall-result` Headers: `x-webhook-secret: ` (тот же стиль, что у `dadata-suggest`). Body (JSON ровно в этом виде; AGI шлёт curl'ом): ```json { "asterisk_uniqueid":"1788283049.23", "order_group_id":"uuid-or-null", "campaign_type":"autocall_slot", "customer_phone":"79067029953", "attempt_no":2, "scenario":"A", "call_started_at":"2026-09-01T20:17:45+03:00", "answered_at":"2026-09-01T20:17:45+03:00", "duration_sec":65, "sip_code":200, "channel_state":"Up", "dtmf_day":1,"dtmf_half":2,"changed":true, "outcome":"confirmed", "was_test_mode":true } ``` ### Логика функции (по шагам) 1. Валидация секрета и полей; `outcome` — только из enum (Фаза 0). 2. Идемпотентность: upsert по `asterisk_uniqueid` (повторный curl не плодит строки). 3. Вставка в `autocall_log`. 4. Если `order_group_id` нет — найти последний активный заказ по `customer_phone` (`status not in ('closed','cancelled')`, `delivery_status='pending_confirmation'`). 5. При `confirmed | confirmed_kept_previous | confirmed_silent`: - вычислить `delivery_date` = (завтра/послезавтра от даты звонка, по Europe/Moscow); - `delivery_time` = `'Первая половина дня'|'Вторая половина дня'` (точно эти строки); - pickup: вместо delivery_* пишем `pickup_date`,`pickup_time_slot` при `delivery_type='pickup'`; - обновить `order_groups`: поля из Фазы 0 п.5 + `next_notification_check_at=null`. 6. При `operator|opt_out|invalid_number`: `notification_status='manual_required'`, `called=true`, `called_comment=...`; для `opt_out` ещё `autocall_opt_out=true`. 7. При `no_answer|no_input_day|no_input_half`: посчитать уже сделанные попытки; при `attempt_no >= max_attempts` или `no-input >= no_input_escalate_after` → `manual_required`, иначе оставить `notification_status='link_ready'` (планировщик назначит следующую). 8. `auth.uid()` пуст → в `order_history` source проставится «Система» (триггер уже работает); добавить `metadata.source='autocall'` через update-поле (если триггер поддерживает; иначе отдельный insert в `order_history`). 9. Ответ: `{ok:true}` / `{ok:false,error}`. ### Тесты (ручные, dev) - curl с тестовым uniqueid → строка в `autocall_log`, заказ обновился, в UI бейдж «🤖 слот от робота». - Повторный curl с тем же uniqueid → дубликата нет. --- ## Фаза 3 — Планировщик (systemd на проде, dev — ручной запуск) ### Файлы - Создать `scripts/autocall_campaign.py` - Создать `scripts/autocall_campaign.service` - Создать `scripts/autocall_campaign.timer` - Конфиг-реюз: импортировать helper'ы из `sms_run_with_db.sh` (резолв IP supabase-db). ### Алгоритм тика (запуск каждые 10 мин) 1. Загрузить настройки `autocall_slot` из `sms_campaign_settings`. Если `enabled=false` → выход. 2. Выбор кандидатов: ```sql select id, customer_phone, delivery_type, first_sms_sent_at from order_groups where notification_status='link_ready' and delivery_status='pending_confirmation' and autocall_opt_out=false and first_sms_sent_at is not null and first_sms_sent_at <= now() - (delay_after_sms_minutes || ' minutes')::interval and coalesce((select max(attempt_no) from autocall_log l where l.order_group_id=order_groups.id),0) < max_attempts and (next_notification_check_at is null or next_notification_check_at <= now()); ``` В test_mode — `customer_phone` заменяем на `test_phone`. 3. Проверка окна: `work_hours_start <= now < work_hours_end`, день недели ∈ `work_days`; иначе — отложить (`next_notification_check_at` = ближайшее начало окна). 4. Сценарий: если дата «готовности» = сегодня → `A`, иначе `B`. (C планировщик пока не выбирает — сценарий C включается отдельной задачей позже.) 5. Расчёт номера попытки: `attempt_no = колв-во строк в autocall_log + 1`; распределение по дню/след.дню: если `attempt_no <= attempts_per_day` → сейчас; иначе по `next_day_attempt_times`. 6. Генерация call-файла в `/tmp/ss--.call`: ``` Channel: PJSIP/0@mmedia Context: SS-Autocall-Slot Extension: s Priority: 1 MaxRetries: 0 WaitTime: 30 Archive: yes Setvar: SCENARIO=A Setvar: CALLTYPE=dlv Setvar: GROUP_ID= Setvar: ATTEMPT= Setvar: API_URL=https:///functions/v1/autocall-result Setvar: API_KEY= Setvar: TEST_MODE=0|1 ``` 7. Передача на АТС: `scp → /tmp` (атомарно mv в `/var/spool/asterisk/outgoing/`), ssh-кред только на сервере (никаких ключей в репо — в `/opt/supersam/.env` локально). 8. Логирование тика в `sms_campaign_log`-стиле своей строкой `campaign_type='autocall_slot'` (техническая метка «тик был») — опционально, для отладки таймера. 9. Обработка ручных запросов: таблица `autocall_queue` (НОВАЯ, минимальная): ```sql create table if not exists public.autocall_queue ( id bigint generated always as identity primary key, order_group_id uuid not null, phone text not null, scenario text not null default 'A', requested_by uuid, requested_at timestamptz not null default now(), processed_at timestamptz ); ``` Кнопка «Позвонить сейчас» пишет сюда; тик забирает `processed_at is null`, создаёт call-файл, ставит `processed_at`. (Так ssh никогда не попадает в edge fn.) ### Юнит-проверки (без настоящих звонков) - Запустить скрипт с `--dry-run`: в консоль печатает список кандидатов и текст call-файла. - `test_mode=true`, `test_phone=<твой>` → один звонок тебе. ### Таймер ``` # autocall_campaign.timer [Timer] OnCalendar=*:0/10 Persistent=true # autocall_campaign.service [Service] Type=oneshot ExecStart=/opt/supersam/scripts/sms_run_with_db.sh /opt/supersam/scripts/autocall_campaign.py ``` --- ## Фаза 4 — Диалплан и AGI-связь с API (изменения на АТС, с показом Паше) 1. `ss-dialog.agi` дополнить: в конце (после определения итогового `OUTCOME`) — curl на `API_URL` с телом из Фазы 2. Переменные приходят из call-файла (`GROUP_ID`, `ATTEMPT`, `TEST_MODE`). Покрыть: потерю сети (curl timeout 5s, не валить звонок — результат всё равно в `/tmp/ss-result-*.txt`). 2. `[SS-Autocall-Slot]` — additive контекст (диф Паше до применения). 3. `pbx_spool.so` — попросить постоянную загрузку (иначе ночной reboot 03:33 сносит). --- ## Фаза 5 — UI: оболочка «Коммуникации» (вкладки) ### Файлы - Создать `src/components/admin/CommunicationsPanel.jsx` — подключает вкладки. - Перенести содержимое `SmsCampaignPanel.jsx` в `src/components/admin/sms/SmsChannelTab.jsx` (почти copy-paste, имя файла сохранить в git как rename). - `src/pages/DashboardPage.jsx`: - импорт `CommunicationsPanel` вместо `SmsCampaignPanel`; - `MEGA_ADMIN_NAV`: `{ key:"sms_campaign", label:"Коммуникации", description:"SMS, автодозвон и аналитика обращений." }` (key не трогаем — совместимость `?tab=sms_campaign`). - роут: `if (activeSection === "sms_campaign") return `. ### Поведение вкладок - Состояние: `searchParams.channel` ∈ `sms | autocall | analytics`, дефолт `sms`. - UI: таблетки (по стилю существующего период-селектора в `SmsCampaignStats`). - На мобильном: вкладки в одну строку с горизонтальным скроллом, подписи короткие. ### Проверка (dev) - Прямой переход `/dashboard?tab=sms_campaign&channel=autocall` открывает нужную вкладку. - Все 4 SMS-карточки, статистика, журнал — без регрессий. - На 390px — ничего не ломается. --- ## Фаза 6 — UI: вкладка «Автодозвон» ### Файлы - Создать `src/components/admin/autocall/AutocallChannelTab.jsx` — карточки, настройки, журнал. - Создать `src/components/admin/autocall/AutocallLogTable.jsx` — таблица/строки + фильтры + поиск. - Создать `src/components/admin/autocall/AutocallStats.jsx` — графики (recharts, как SmsCampaignStats). ### Карточки кампаний звонков `CAMPAIGNS` для канала: ```js [ { key:"autocall_slot", label:"Согласование слота", icon:"🔊", live:true }, { key:"autocall_storage", label:"Платное хранение (голосом)",icon:"📦", live:false }, { key:"autocall_reminder", label:"Напоминание накануне", icon:"⏰", live:false }, { key:"autocall_confirm_morning", label:"Подтверждение утром", icon:"☀️", live:false }, ] ``` Неработающие — карточка disabled с бейджем «скоро». Рабочая — по шаблону SMS-карточек: тоггл вкл/выкл, бейдж (⏸/🧪/🚀), мини-статистика Всего / Дозвон / Согласовали, кнопки ▶/🧪/🔄. ### Настройки `autocall_slot` (форма, все поля из Фазы 0 п.7) Группы: - Режим: тест/боевой + тестовый номер. - Интервалы: `delay_after_sms_minutes`, `retry_interval_minutes`, `attempts_per_day`, `max_attempts`, `next_day_attempt_times`. - Эскалация: `no_input_escalate_after`. - Окно: часы (селекты как у SMS) и дни-таблетки. - Telegram chat ID для алертов недозвона. Кнопка «Сохранить» — тот же паттерн (`update sms_campaign_settings`). По умолчанию `enabled=false`, `test_mode=true`. ### Журнал звонков - Данные: `supabase.from("autocall_log").select("*").order("created_at desc").limit(200)`. - Таблица (desktop): `Заказ | Клиент/телефон | Попытка | Время | Длит. | Исход | Слот`. - Mobile (<640px): подписанные строки: ``` 🔊 Володя · +7 906 702-99-53 Исход: ✅ Согласовал (попытка 2) Слот: завтра, первая половина дня 01.09.26, 20:18 ``` - Бейджи исходов: тон/иконка/текст из маппинга (Фаза 0 п.3); `confirmed*` — зелёные, `no_answer` — нейтраль, `no_input*` — warning, `operator/opt_out/invalid_number/error` — danger. - Фильтры-chips: все + 10 исходов (значение = outcome). Поиск: по номеру заказа/телефону/имени (общий helper с SMS-журналом). - Клик по строке → `/dashboard/group/`. - Пагинация 20/стр, автообновление 30с (тоггл, как у SMS). --- ## Фаза 7 — Бейджи снаружи (реестр и логистика) ### Файлы - `src/components/logistics/LogisticsReadinessBoard.jsx` — колонка/строка бейджа. - `src/hooks/useOrderGroups.js` — подтягивать `order_group_comms_flags` одним запросом join'ом (или вторым запросом по списку id — проще, 1 запрос). ### Бейдж (приоритет сверху вниз — один наружу) | Условие | Бейдж | Тон | |---|---|---| | `autocall_opt_out` | 🚫 отказ от звонков | danger | | `last_call_outcome='invalid_number'` | ❌ неверный номер | danger | | `notification_status='manual_required'` + были звонки | 👤 менеджеру | warning | | `no_input_count ≥ 2` | 🤐 не нажимает | warning | | `call_attempts ≥ 3` + последний `no_answer` | 📵 не дозвонились | warning | | `delivery_date_source='autocall'` + `last_call_changed` | 🤖✏️ слот от робота (правился) | info | | `delivery_date_source='autocall'` | 🤖 слот от робота | info | Реализация: компонент `src/components/UI/CallStatusBadge.jsx` (props: флаги), рендерится: - desktop — отдельная колонка «Дозвон» в таблице групп; - mobile — иконка в подписанной строке (рядом с телефоном). ### Фильтры реестра Chips над таблицей (мультивыбор): `📵 Не дозвонились`, `🤐 Не нажимают`, `👤 Менеджеру (после робота)`, `🤖 Согласовано роботом`, `🚫 Отказ от звонков`. Фильтрация — на клиенте по уже загруженным флагам (скрываем строки), без новых запросов. ### Дашборд логистика и водителя В карточках доставок — мелкая пометка «🤖» возле времени слота при `delivery_date_source='autocall'`. Без фильтров, только визуальная пометка. --- ## Фаза 8 — Карточка заказа: блок «Автодозвон» ### Файл - Создать `src/components/orders/AutocallStatusCard.jsx`; подключить в `DeliverySetDetailPanel.jsx` рядом с `SmsStatusCard`. ### Содержимое - Заголовок: «📞 Автодозвон» + бейдж-итог (успех/в работе/менеджеру/отказ). - Слот: если `confirmed*` — «Завтра, первая половина дня» (плюс пометка «согласовано роботом»). - История: до 5 строк из `autocall_log` этого заказа (иконка исхода, время, попытка). - Если `last_call_changed=true` — подсказка: «Клиент правил выбор. При сомнении — перезвоните». - Кнопки (роли `mega_admin`, `logistician`): - `[📞 Позвонить сейчас]` → insert в `autocall_queue` (см. Фаза 3, п.9), кнопка заменяется на «В очереди ✓». - `[🚫 Не звонить]` / `[🔔 Вернуть в автодозвон]` → toggle `autocall_opt_out`. - В `order_history` уже попадает (триггер); доработка: `metadata.source='autocall'` — вывести в таймлайне как «Автодозвон», если UI таймлайна отображает source. --- ## Фаза 9 — Вкладка «Аналитика» ### Файл - Создать `src/components/admin/analytics/CommsAnalyticsTab.jsx`. ### Блоки 1. **Сводка каналов за период** (7/14/30/всё — селектор как в SmsCampaignStats): - SMS: отправлено/доставлено/конверсия переходов по ссылке (если считаем — иначе «н/д»), - Звонки: попыток/дозвонились/согласовали/эскалации, - Итог: «Согласовано автоматикой» vs `manual_required` всего. 2. **Воронка автодозвона** (bar): SMS→звонок1→…→звонок5→согласовано/менеджеру. 3. **Исходы** (donut из `autocall_log.outcome`). 4. **По дням** (bar: попыток/дозвон/согласовано) + **конверсия согласования** (line). 5. **«Согласовали с N-й попытки»** (bar) — покажет, нужна ли 4–5 попытка. 6. **Минуты**: «время на линии всего / средний звонок» (sum/avg `duration_sec`) — данные для разговора с Пашей о тарифе Miranda. 7. **Экспорт CSV** текущего фильтра журнала звонков и сводки (кнопка «⬇ CSV», генерим на клиенте Blob). Все запросы — только чтение `autocall_log`/`sms_campaign_log` за период; без новых таблиц. --- ## Фаза 10 — Раскатка и приёмка 1. **Dev прогон**: миграция → edge fn → планировщик (dry-run) → UI (вкладки, журнал, бейджи, карточка, аналитика) на 390px и desktop. 2. **Тестовый заказ**: создать заказ, прогнать цикл SMS → (не выбирает слот) → звонок на `test_phone` → согласование → проверить карточку, бейдж в реестре, таймлайн, аналитику. 3. **Боевая проба на твоём номере**: `test_mode=true`, реальный заказ → ты нажимаешь 1/2 → всё сошлось. 4. **Перенос prod**: миграция, edge fn, планировщик (таймер включён, кампания `enabled=false`), UI-деплой (бамп SW cache). 5. **Включение prod** — только по твоей команде: сначала `test_mode=true` (боевой prod, но звонит тебе), потом выключаем test_mode. 6. **Гипигиена**: вернуть `approvals` в manual, секреты — в `/opt/supersam/.env`, доки: `docs/autocall-contract.md` актуален. --- ## Риски и вопросы (держим перед глазами) - Reboot АТС 03:33 → Паша ставит `pbx_spool.so` в автозагрузку (диф показан до). - Тариф Miranda не считается из CDR → своя статистика минут (Фаза 9, п.6). - Отказ от звонков — юр. требование: озвучка «9» уже в диалоге; счётчик opt-out на карточке кампании. - Никаких ssh из edge fn — ручные звонки только через `autocall_queue` + планировщик. - Совместимость URL: `?tab=sms_campaign` сохранён как key; label меняем на «Коммуникации». ## Оценка по объёму - Фазы 1–4 (БД+API+планировщик+АТС-curl): ~0,5–1 день. - Фазы 5–9 (UI): ~1–1,5 дня. - Фаза 10 (прогоны): несколько часов. Итого ≈ 2–2,5 рабочих дня до prod-включения.