From 512aab9ff259ccda69701992dab70838848b4b32 Mon Sep 17 00:00:00 2001 From: Hermes Date: Wed, 16 Sep 2026 17:15:43 +0200 Subject: [PATCH] =?UTF-8?q?fix(sms):=20=D0=B7=D0=B0=D0=BA=D0=B0=D0=B7?= =?UTF-8?q?=D1=8B=20=D0=B1=D0=BE=D0=BB=D1=8C=D1=88=D0=B5=20=D0=BD=D0=B5=20?= =?UTF-8?q?=D0=B2=D0=B8=D1=81=D1=8F=D1=82=20=D0=B2=20=C2=AB1-=D0=B5=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=D0=B3=D0=BB=D0=B0=D1=88=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=BE=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=BE=C2=BB=20=E2=80=94=20=D0=B2=20=D1=80=D1=83=D1=87=D0=BD?= =?UTF-8?q?=D1=83=D1=8E=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=83=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=B2=D0=BE=D0=B4=D0=B8=D0=BC=20=D0=BE=D1=82?= =?UTF-8?q?=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=D0=B4=D0=BD=D0=B5=D0=B3=D0=BE?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BD=D1=82=D0=B0=D0=BA=D1=82=D0=B0=20=D0=B8=20?= =?UTF-8?q?=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=BF=D0=BE=20=D0=B2?= =?UTF-8?q?=D0=BA=D0=BB=D1=8E=D1=87=D1=91=D0=BD=D0=BD=D1=8B=D0=BC=20=D0=BA?= =?UTF-8?q?=D0=B0=D0=BD=D0=B0=D0=BB=D0=B0=D0=BC=20(SMS/=D0=B0=D0=B2=D1=82?= =?UTF-8?q?=D0=BE=D0=B4=D0=BE=D0=B7=D0=B2=D0=BE=D0=BD);=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=BA=D0=B0=D0=B7,=20=D0=B3=D0=B4=D0=B5=20=D0=B4=D0=B0=D1=82?= =?UTF-8?q?=D0=B0=20=D1=83=D0=B6=D0=B5=20=D0=B2=D1=8B=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D0=B0,=20=D1=80=D0=BE=D0=B1=D0=BE=D1=82=D1=83=20=D0=BD?= =?UTF-8?q?=D0=B5=20=D0=B7=D0=B2=D0=BE=D0=BD=D0=B8=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/autocall/autocall-how-it-works.html | 2 +- docs/autocall/autocall-how-it-works.pdf | Bin 92290 -> 94635 bytes docs/autocall/deck_content.py | 20 ++ scripts/autocall_scheduler.py | 18 +- scripts/sms_manual_campaign.py | 196 ++++++++++++++----- src/components/admin/SmsCampaignPanel.jsx | 5 +- src/components/orders/AutocallStatusCard.jsx | 35 +++- src/services/autocallProcess.js | 12 ++ src/services/autocallProcess.test.js | 22 +++ 9 files changed, 251 insertions(+), 59 deletions(-) diff --git a/docs/autocall/autocall-how-it-works.html b/docs/autocall/autocall-how-it-works.html index 70e2f39..350002d 100644 --- a/docs/autocall/autocall-how-it-works.html +++ b/docs/autocall/autocall-how-it-works.html @@ -83,6 +83,6 @@ -
СУПЕР САМ · ДОСТАВКА И САМОВЫВОЗ

Автодозвон: как это работает

Робот звонит клиенту, согласует день и половину дня — без участия оператора

Что описано: правила звонка, паузы, поведение в нештатных случаях (не дозвонились,

сбросили трубку, не выбрали день/время), где что настраивается и что видит менеджер.

 

Основано на текущем коде и настройках боевой системы, без обещаний и продажи.

Автодозвон SuperSam · как это работает1

Что делает робот

Коротко: он не продаёт и не консультирует — только согласует день и половину дня

Три задачи

  • Дозвониться, когда заказ готов и клиент не подтвердил день по ссылке из SMS
  • Предложить конкретные дни голосом и получить выбор цифрой
  • Записать выбор в заказ — дату, половину дня и признак «согласовано роботом»

Чего он не делает

  • Не соединяет с живым менеджером — такой цифры в диалоге нет
  • Не спорит и не уговаривает: нет ответа — повторит по расписанию
  • Не предлагает нерабочие дни и не звонит вне рабочего времени

Как это выглядит для клиента

  • Звонок с одного номера, голос один и тот же: «Ваш заказ готов. Выберите день и время»
  • Клиент нажимает цифру дня и цифру половины дня — робот подтверждает выбор вслух
  • После подтверждения даёт возможность изменить выбор (нажатием цифры 2)
Автодозвон SuperSam · как это работает2

Путь заказа до звонка

Когда именно звучит звонок

1. Заказ из 1Сприходит готовымпо обмену
›
2. Ссылка и SMSссылка создаётсясама, SMS уходит
›
3. Пауза 90 минутпосле SMS илипосле попытки, еслиSMS не ушло
›
4. Звонок роботаесли клиент неподтвердил по ссылке
›
5. Результат в заказдата и половина дняв самом заказе

Важно

  • Готовность заказа приходит из 1С по обмену: заказ приходит уже готовым, вручную в системе её никто не отмечает
  • Если клиент подтвердил день по ссылке сам — звонка не будет: заказ уже согласован
  • Звонок идёт только в рабочее время и только в рабочие дни (настройки кампании)
Автодозвон SuperSam · как это работает3

Откуда берутся дни и время

Админка → Расписание → Рабочие дни: один источник правды

Что настраивается

  • Рабочие дни склада (самовывоз) и рабочие дни доставки
  • Часы: открытие и закрытие склада, часы доставки
  • «Первая половина — до» — граница между половинами дня
  • «Сегодня предлагаем до» — до какого времени предлагать сегодняшний день

Как это работает в коде

  • Окно звонка считается рабочими днями: выходные не сокращают окно, а пропускаются
  • Робот называет реальные дни голосом: «сегодня», «в понедельник», «во вторник»
  • Клиентская ссылка использует те же дни — расхождений между звонком и ссылкой нет

Текущие значения в системе

  • Склад: пн–пт, 09:00–17:30, половина дня до 13:00, «сегодня» предлагаем до 15:30
  • Доставка: пн–пт (суббота — выходной) 09:00–21:00, половина дня — граница 13:00
  • Граница половины дня — 13:00 для обоих каналов: звоним после 13:00 — говорим про вторую половину дня, первую уже не предлагаем
Автодозвон SuperSam · как это работает4

Самовывоз: какие дни предлагает

Окно — «день готовности + два рабочих дня», выходные пропускаются

Заказ готовЧто предложит роботПочему так
в понедельникпн, вт, сртри рабочих дня подряд
в пятницупт, пн, втсуббота и воскресенье — выходные склада
в пятницу после 15:30пн, вт«сегодня» больше не предлагаем (настройка)
в понедельник после 13:00пн, вт, срполовину дня не спрашиваем — сразу вторая
заказ готов давнозвонка нетокно самовывоза прошло — звонить не о чем

Две настройки, которые это двигают

  • «Сегодня предлагаем до» (сейчас 15:30) — позже этого времени сегодняшний день исчезает из звонка и из ссылки
  • Граница половины дня (13:00): звоним после 13:00 — робот говорит про вторую половину дня, первую не предлагает
Автодозвон SuperSam · как это работает5

Доставка: не в день готовности

Нужен запас времени, чтобы собрать маршрут — поэтому начинаем со следующего дня

Заказ готовЧто предложит робот
в понедельниквторник и среда
в пятницупонедельник и вторник (в субботу не возим)
в четверг, звонок в пятницупятница и понедельник
в пятницу, звонок в понедельникпонедельник и вторник

Ключевое отличие от самовывоза

  • В день готовности доставку не предлагаем никогда — только следующие рабочие дни
  • Суббота и воскресенье в графике доставки — выходные, их робот не назовёт
  • Половина дня — граница 13:00: звоним после 13:00, значит говорим про вторую половину дня
Автодозвон SuperSam · как это работает6

Как проходит разговор

Робот задаёт вопрос и ждёт цифру; молчание не считается согласием

ШагЧто говорит роботЧто делает клиентСколько ждёт
Приветствие«Ваш заказ готов. Выберите день и время доставки»слушает—
День«Нажмите один, чтобы доставить. В понедельник. Нажмите два…»нажимает 1 или 29 секунд
Половина дня«Первая половина дня или вторая?»нажимает 1 или 29 секунд
Подтверждение«Первая половина дня» — озвучивает выборслушает—
Изменить?подсказка про кнопку 2 — один разнажимает 2, чтобы переделать6 секунд

Что делает при ошибке ввода

  • Не понял цифру — повторяет тот же вопрос один раз
  • Снова не понял или молчит — завершает: «Вы не выбрали время. С вами свяжется менеджер»
  • Клиент нажимает 2 — диалог начинается заново («день и время»), прежний слот при этом не теряется
Автодозвон SuperSam · как это работает7

Что можно нажать

Всего несколько цифр — и одна из них «больше не звоните»

ЦифраЗначениеЧто происходит
1первый предложенный деньробот переходит к выбору половины дня
2второй предложенный деньробот переходит к выбору половины дня
3третий день (только самовывоз)робот переходит к выбору половины дня
1 / 2половина дняпервая половина / вторая (после 13:00 — только вторая)
2 после подтвержденияизменить выборробот спрашивает день и время заново — один раз
любая другая цифране принимаетсявопрос повторяется один раз; после двух таких звонков — менеджеру

Что происходит после подтверждения

  • Робот называет выбранный день и половину дня вслух — клиент слышит, что именно записано
  • Дальше один раз предлагает изменить выбор: нажатие 2 — и день со временем спрашивают заново
  • Если клиент ничего не меняет, разговор завершается, а выбор уходит в заказ
Автодозвон SuperSam · как это работает8

Если не дозвонились

Робот различает «не взял трубку» и «вместо человека ответил автоответчик/оператор»

Как понимает, что трубку не взяли

  • Никто не ответил за 45 секунд — попытка считается неудачной
  • Трубку подняли, но гудки шли больше 20 секунд и ни одной цифры не нажали — это объявление оператора, а не человек: пишем «не взял трубку»
  • Живой человек почти всегда берёт трубку раньше, поэтому правило надёжно отделяет автоответчик

Что происходит дальше

  • Повторный звонок — не раньше чем через час (настройка)
  • Всего не больше 5 попыток на заказ (настройка)
  • Звонит только в рабочие часы кампании: 09:00–20:00, дни — по настройке
  • Исчерпали попытки — заказ уходит менеджеру, автозвонок по нему останавливается

Для менеджера это видно в карточке

  • Каждая попытка — отдельная строка истории: время, результат, длительность разговора
  • Статус заказа меняется на «нужен менеджер» — с комментарием «не дозвонились / не выбрал»
  • Отдельно красным предупреждаем, если заявка «Позвонить сейчас» висит больше 15 минут
Автодозвон SuperSam · как это работает9

Если сбросили трубку в середине разговора

Робот запоминает то, что успел услышать, и не портит уже согласованный слот

Когда положили трубкуЧто запишет роботЧто увидит менеджер
до выбора дняответил, день не выбрал«взял, не выбрал день» — попытка зачтена
день выбрал, время не успелдень выбран, время нет«взял, не выбрал время»
выбор подтвердил и сбросилслот согласовандата и половина дня в заказе, статус «согласовано»
начал менять выбор и сбросилостаётся прежний слот«правка не доведена» — прежнее согласование в силе

Почему так

  • Слот становится согласованным только после двух ответов: день и половина дня — это защита от «услышал половину фразы»
  • Обрыв не считается согласием: если клиент не подтвердил, робот перезвонит по расписанию или отдаст заказ менеджеру
  • Если клиент уже согласовал слот, а потом начал менять и бросил трубку — прежний выбор сохраняется, ничего не теряется
Автодозвон SuperSam · как это работает10

Когда подключается менеджер

Робот доводит дело до конца сам, но в трёх случаях передаёт человеку

Не выбрал день/время 2 раза

  • Робот дважды фиксировал «взял, но не выбрал»
  • Заказ помечается «нужен менеджер», комментарий «не дозвонились / не выбрал»
  • Порог «не выбрал × N» настраивается в админке автодозвона

Исчерпаны 5 попыток

  • Каждый звонок — отдельная попытка
  • Когда попытки закончились, автозвонок прекращается
  • Заказ остаётся в списке у менеджера

Неверный номер

  • Если номер не существует или недоступен — задача менеджеру
  • Менеджер может поставить «Не звонить» — тогда робот этого клиента не беспокоит
  • Такая отметка видна в карточке заказа и учитывается при обзвоне

Что видит менеджер в карточке заказа

  • Единый статус автодозвона и история всех попыток с результатами и слотами
  • Кнопка «Позвонить сейчас» — ручной звонок клиенту (не тестовый номер) и живое состояние: «в очереди», «звоним», «звонок сделан» с результатом
  • Кнопка «Не звонить» — если робот не должен беспокоить этого клиента
Автодозвон SuperSam · как это работает11

Когда робот не звонит вообще

Список условий, при которых звонка не будет — проверяются до набора номера

По клиенту и заказу

  • Клиент сам подтвердил день по ссылке — звонок не нужен
  • Менеджер поставил «Не звонить» — робот этого клиента не беспокоит
  • Номер в списке исключений — например, на МТС-диапазон звонки не идут
  • Самовывоз: автообзвон по самовывозу включается отдельной галочкой (сейчас выключен)

По времени и географии

  • Вне рабочих часов кампании (09:00–20:00) и вне рабочих дней
  • Город исключён целиком или для города выключен канал
  • В админке автодозвона выключены звонки по городу — например, Ялта: SMS идут, звонков нет
  • Меньше 90 минут прошло после последней попытки отправки SMS: если приглашение ушло — считаем от отправки, если не ушло — от попытки

Админка автодозвона: где это переключается

  • Панель «Автодозвон»: тестовый режим, часы и дни звонков, паузы, лимит попыток, порог «не выбрал × N»
  • Панель «Локации»: для каждого города — звонки включены или выключены (SMS не затрагивается)
  • Панель исключений: номера и префиксы, на которые не звоним, с причиной
Автодозвон SuperSam · как это работает12

Что записывается в заказ

Результат звонка виден там же, где всё остальное

ПолеЗначение
Датаконкретная дата, которую назвал робот (например, понедельник 21.09)
Время«первая половина дня» или «вторая половина дня»
Источник«согласовано роботом» — видно, что выбор сделал клиент в звонке
Статус«согласовано» / «нужен менеджер» / «не звонить» (запрет ставит менеджер)
Комментарий«Согласовано роботом: завтра, первая половина дня»

Зачем это нужно

  • Менеджер видит причину и источник решения, а не просто «дата стоит»
  • История звонков хранится отдельно: видно, сколько было попыток и чем они закончились
  • Дата, названная роботом, используется дальше в работе склада и доставки
Автодозвон SuperSam · как это работает13

Тестовый режим и текущее состояние

Сейчас обзвон клиентов выключен — система в тестовом режиме

ПараметрЗначение сейчасЧто будет при запуске
Обзвон клиентоввыключенвключается отдельной командой
Тестовый номерзвонит только на негомассовый обзвон идёт клиентам
Кнопка «Позвонить сейчас»звонит клиенту сразутак же — звонок клиенту
Звонки по самовывозувыключены галочкойвключаются отдельно от доставки

Как проверяется перед запуском

  • Тестовый звонок на свой номер: диалог, дни, половина дня, запись результата
  • Прогон диалога без звонка: проверка всех веток (согласие, обрыв, молчание, отказ)
  • Проверка окон: робот предлагает именно те дни, которые заданы в админке
Автодозвон SuperSam · как это работает14

Что пока не учитывается

Честный список: это важно знать до запуска

График

  • Праздники: календаря нерабочих дат нет — если праздник попадёт в окно, робот его предложит
  • Часы доставки используются частично: границу половины дня робот проговаривает словами, без конкретного времени

Настройки, которые пока не влияют

  • «Попыток в день» — поле есть, но расписание повторов задаётся интервалом между попытками
  • «Попытки следующего дня (10:00, 14:00)» — пока не используются
  • «Таймаут звонка, 30 секунд» — звонок ждёт ответа 45 секунд (значение в самом звонке)

Что предлагаем уточнить до боевого запуска

  • Список праздников (или решение не учитывать их)
  • Нужен ли жёсткий график повторов по времени, кроме паузы в час
  • Кто и когда разбирает заказы, которые робот не согласовал за 5 попыток
Автодозвон SuperSam · как это работает15

Коротко

Одна страница вместо всей презентации

Что делает система

  • Ждёт 90 минут после SMS (или после последней попытки отправки, если SMS не ушло) и звонит, если клиент не подтвердил день сам
  • Предлагает только рабочие дни по графику: самовывоз — включая сегодня, доставка — со следующего дня
  • Спрашивает день и половину дня, подтверждает вслух, даёт один раз переделать выбор
  • Граница половины дня — 13:00: звоним после 13:00, значит говорим про вторую половину
  • Повторяет попытки не чаще чем раз в час, максимум 5 раз, только 09:00–20:00 в рабочие дни
  • После двух «взял, но не выбрал» или исчерпания попыток передаёт заказ менеджеру
  • Каждая попытка и результат видны в карточке заказа; ручной звонок идёт клиенту и показывает своё состояние

Что осталось решить

  • Праздники и точный график повторов — по вашим правилам
  • Момент включения боевого обзвона клиентов и по самовывозу
Автодозвон SuperSam · как это работает16
+
СУПЕР САМ · ДОСТАВКА И САМОВЫВОЗ

Автодозвон: как это работает

Робот звонит клиенту, согласует день и половину дня — без участия оператора

Что описано: правила звонка, паузы, поведение в нештатных случаях (не дозвонились,

сбросили трубку, не выбрали день/время), где что настраивается и что видит менеджер.

 

Основано на текущем коде и настройках боевой системы, без обещаний и продажи.

Автодозвон SuperSam · как это работает1

Что делает робот

Коротко: он не продаёт и не консультирует — только согласует день и половину дня

Три задачи

  • Дозвониться, когда заказ готов и клиент не подтвердил день по ссылке из SMS
  • Предложить конкретные дни голосом и получить выбор цифрой
  • Записать выбор в заказ — дату, половину дня и признак «согласовано роботом»

Чего он не делает

  • Не соединяет с живым менеджером — такой цифры в диалоге нет
  • Не спорит и не уговаривает: нет ответа — повторит по расписанию
  • Не предлагает нерабочие дни и не звонит вне рабочего времени

Как это выглядит для клиента

  • Звонок с одного номера, голос один и тот же: «Ваш заказ готов. Выберите день и время»
  • Клиент нажимает цифру дня и цифру половины дня — робот подтверждает выбор вслух
  • После подтверждения даёт возможность изменить выбор (нажатием цифры 2)
Автодозвон SuperSam · как это работает2

Путь заказа до звонка

Когда именно звучит звонок

1. Заказ из 1Сприходит готовымпо обмену
›
2. Ссылка и SMSссылка создаётсясама, SMS уходит
›
3. Пауза 90 минутпосле SMS илипосле попытки, еслиSMS не ушло
›
4. Звонок роботаесли клиент неподтвердил по ссылке
›
5. Результат в заказдата и половина дняв самом заказе

Важно

  • Готовность заказа приходит из 1С по обмену: заказ приходит уже готовым, вручную в системе её никто не отмечает
  • Если клиент подтвердил день по ссылке сам — звонка не будет: заказ уже согласован
  • Звонок идёт только в рабочее время и только в рабочие дни (настройки кампании)
Автодозвон SuperSam · как это работает3

Откуда берутся дни и время

Админка → Расписание → Рабочие дни: один источник правды

Что настраивается

  • Рабочие дни склада (самовывоз) и рабочие дни доставки
  • Часы: открытие и закрытие склада, часы доставки
  • «Первая половина — до» — граница между половинами дня
  • «Сегодня предлагаем до» — до какого времени предлагать сегодняшний день

Как это работает в коде

  • Окно звонка считается рабочими днями: выходные не сокращают окно, а пропускаются
  • Робот называет реальные дни голосом: «сегодня», «в понедельник», «во вторник»
  • Клиентская ссылка использует те же дни — расхождений между звонком и ссылкой нет

Текущие значения в системе

  • Склад: пн–пт, 09:00–17:30, половина дня до 13:00, «сегодня» предлагаем до 15:30
  • Доставка: пн–пт (суббота — выходной) 09:00–21:00, половина дня — граница 13:00
  • Граница половины дня — 13:00 для обоих каналов: звоним после 13:00 — говорим про вторую половину дня, первую уже не предлагаем
Автодозвон SuperSam · как это работает4

Самовывоз: какие дни предлагает

Окно — «день готовности + два рабочих дня», выходные пропускаются

Заказ готовЧто предложит роботПочему так
в понедельникпн, вт, сртри рабочих дня подряд
в пятницупт, пн, втсуббота и воскресенье — выходные склада
в пятницу после 15:30пн, вт«сегодня» больше не предлагаем (настройка)
в понедельник после 13:00пн, вт, срполовину дня не спрашиваем — сразу вторая
заказ готов давнозвонка нетокно самовывоза прошло — звонить не о чем

Две настройки, которые это двигают

  • «Сегодня предлагаем до» (сейчас 15:30) — позже этого времени сегодняшний день исчезает из звонка и из ссылки
  • Граница половины дня (13:00): звоним после 13:00 — робот говорит про вторую половину дня, первую не предлагает
Автодозвон SuperSam · как это работает5

Доставка: не в день готовности

Нужен запас времени, чтобы собрать маршрут — поэтому начинаем со следующего дня

Заказ готовЧто предложит робот
в понедельниквторник и среда
в пятницупонедельник и вторник (в субботу не возим)
в четверг, звонок в пятницупятница и понедельник
в пятницу, звонок в понедельникпонедельник и вторник

Ключевое отличие от самовывоза

  • В день готовности доставку не предлагаем никогда — только следующие рабочие дни
  • Суббота и воскресенье в графике доставки — выходные, их робот не назовёт
  • Половина дня — граница 13:00: звоним после 13:00, значит говорим про вторую половину дня
Автодозвон SuperSam · как это работает6

Как проходит разговор

Робот задаёт вопрос и ждёт цифру; молчание не считается согласием

ШагЧто говорит роботЧто делает клиентСколько ждёт
Приветствие«Ваш заказ готов. Выберите день и время доставки»слушает—
День«Нажмите один, чтобы доставить. В понедельник. Нажмите два…»нажимает 1 или 29 секунд
Половина дня«Первая половина дня или вторая?»нажимает 1 или 29 секунд
Подтверждение«Первая половина дня» — озвучивает выборслушает—
Изменить?подсказка про кнопку 2 — один разнажимает 2, чтобы переделать6 секунд

Что делает при ошибке ввода

  • Не понял цифру — повторяет тот же вопрос один раз
  • Снова не понял или молчит — завершает: «Вы не выбрали время. С вами свяжется менеджер»
  • Клиент нажимает 2 — диалог начинается заново («день и время»), прежний слот при этом не теряется
Автодозвон SuperSam · как это работает7

Что можно нажать

Всего несколько цифр — и одна из них «больше не звоните»

ЦифраЗначениеЧто происходит
1первый предложенный деньробот переходит к выбору половины дня
2второй предложенный деньробот переходит к выбору половины дня
3третий день (только самовывоз)робот переходит к выбору половины дня
1 / 2половина дняпервая половина / вторая (после 13:00 — только вторая)
2 после подтвержденияизменить выборробот спрашивает день и время заново — один раз
любая другая цифране принимаетсявопрос повторяется один раз; после двух таких звонков — менеджеру

Что происходит после подтверждения

  • Робот называет выбранный день и половину дня вслух — клиент слышит, что именно записано
  • Дальше один раз предлагает изменить выбор: нажатие 2 — и день со временем спрашивают заново
  • Если клиент ничего не меняет, разговор завершается, а выбор уходит в заказ
Автодозвон SuperSam · как это работает8

Если не дозвонились

Робот различает «не взял трубку» и «вместо человека ответил автоответчик/оператор»

Как понимает, что трубку не взяли

  • Никто не ответил за 45 секунд — попытка считается неудачной
  • Трубку подняли, но гудки шли больше 20 секунд и ни одной цифры не нажали — это объявление оператора, а не человек: пишем «не взял трубку»
  • Живой человек почти всегда берёт трубку раньше, поэтому правило надёжно отделяет автоответчик

Что происходит дальше

  • Повторный звонок — не раньше чем через час (настройка)
  • Всего не больше 5 попыток на заказ (настройка)
  • Звонит только в рабочие часы кампании: 09:00–20:00, дни — по настройке
  • Исчерпали попытки — заказ уходит менеджеру, автозвонок по нему останавливается

Для менеджера это видно в карточке

  • Каждая попытка — отдельная строка истории: время, результат, длительность разговора
  • Статус заказа меняется на «нужен менеджер» — с комментарием «не дозвонились / не выбрал»
  • Отдельно красным предупреждаем, если заявка «Позвонить сейчас» висит больше 15 минут
Автодозвон SuperSam · как это работает9

Если сбросили трубку в середине разговора

Робот запоминает то, что успел услышать, и не портит уже согласованный слот

Когда положили трубкуЧто запишет роботЧто увидит менеджер
до выбора дняответил, день не выбрал«взял, не выбрал день» — попытка зачтена
день выбрал, время не успелдень выбран, время нет«взял, не выбрал время»
выбор подтвердил и сбросилслот согласовандата и половина дня в заказе, статус «согласовано»
начал менять выбор и сбросилостаётся прежний слот«правка не доведена» — прежнее согласование в силе

Почему так

  • Слот становится согласованным только после двух ответов: день и половина дня — это защита от «услышал половину фразы»
  • Обрыв не считается согласием: если клиент не подтвердил, робот перезвонит по расписанию или отдаст заказ менеджеру
  • Если клиент уже согласовал слот, а потом начал менять и бросил трубку — прежний выбор сохраняется, ничего не теряется
Автодозвон SuperSam · как это работает10

Клиент не выбрал дату после приглашения

Срок ожидания считаем от последнего контакта — SMS или звонка робота

Что настроено в системеОт чего считаем срокКогда заказ уходит менеджеру
Только SMS-приглашениеот последнего SMSчерез 3 ч (настройка в админке)
Плюс автодозвонждём звонок робота, отсчёт — от негочерез 3 ч после последнего звонка
Плюс второе приглашениесначала ждём второе SMS, потом звонокпосле последнего контакта по последнему каналу
Звонить нельзя (гео, исключение, «не звонить»)от последнего SMSчерез 3 ч — звонок не ждём

Что важно

  • Ожидание включается только по каналам, которые реально включены в системе: если автодозвона нет — не ждём звонок
  • Позвонили позже — ожидание начинается заново: менеджер получает заказ, только когда клиент молчит по всем каналам
  • Клиент выбрал дату — из ожидания выходим сразу, даже если срок не истёк
Автодозвон SuperSam · как это работает11

Когда подключается менеджер

Робот доводит дело до конца сам, но в трёх случаях передаёт человеку

Не выбрал день/время 2 раза

  • Робот дважды фиксировал «взял, но не выбрал»
  • Заказ помечается «нужен менеджер», комментарий «не дозвонились / не выбрал»
  • Порог «не выбрал × N» настраивается в админке автодозвона

Исчерпаны 5 попыток

  • Каждый звонок — отдельная попытка
  • Когда попытки закончились, автозвонок прекращается
  • Заказ остаётся в списке у менеджера

Неверный номер

  • Если номер не существует или недоступен — задача менеджеру
  • Менеджер может поставить «Не звонить» — тогда робот этого клиента не беспокоит
  • Такая отметка видна в карточке заказа и учитывается при обзвоне

Что видит менеджер в карточке заказа

  • Единый статус автодозвона и история всех попыток с результатами и слотами
  • Кнопка «Позвонить сейчас» — ручной звонок клиенту (не тестовый номер) и живое состояние: «в очереди», «звоним», «звонок сделан» с результатом
  • Кнопка «Не звонить» — если робот не должен беспокоить этого клиента
Автодозвон SuperSam · как это работает12

Когда робот не звонит вообще

Список условий, при которых звонка не будет — проверяются до набора номера

По клиенту и заказу

  • Клиент сам подтвердил день по ссылке — звонок не нужен
  • Менеджер поставил «Не звонить» — робот этого клиента не беспокоит
  • Номер в списке исключений — например, на МТС-диапазон звонки не идут
  • Самовывоз: автообзвон по самовывозу включается отдельной галочкой (сейчас выключен)

По времени и географии

  • Вне рабочих часов кампании (09:00–20:00) и вне рабочих дней
  • Город исключён целиком или для города выключен канал
  • В админке автодозвона выключены звонки по городу — например, Ялта: SMS идут, звонков нет
  • Меньше 90 минут прошло после последней попытки отправки SMS: если приглашение ушло — считаем от отправки, если не ушло — от попытки

Админка автодозвона: где это переключается

  • Панель «Автодозвон»: тестовый режим, часы и дни звонков, паузы, лимит попыток, порог «не выбрал × N»
  • Панель «Локации»: для каждого города — звонки включены или выключены (SMS не затрагивается)
  • Панель исключений: номера и префиксы, на которые не звоним, с причиной
Автодозвон SuperSam · как это работает13

Что записывается в заказ

Результат звонка виден там же, где всё остальное

ПолеЗначение
Датаконкретная дата, которую назвал робот (например, понедельник 21.09)
Время«первая половина дня» или «вторая половина дня»
Источник«согласовано роботом» — видно, что выбор сделал клиент в звонке
Статус«согласовано» / «нужен менеджер» / «не звонить» (запрет ставит менеджер)
Комментарий«Согласовано роботом: завтра, первая половина дня»

Зачем это нужно

  • Менеджер видит причину и источник решения, а не просто «дата стоит»
  • История звонков хранится отдельно: видно, сколько было попыток и чем они закончились
  • Дата, названная роботом, используется дальше в работе склада и доставки
Автодозвон SuperSam · как это работает14

Тестовый режим и текущее состояние

Сейчас обзвон клиентов выключен — система в тестовом режиме

ПараметрЗначение сейчасЧто будет при запуске
Обзвон клиентоввыключенвключается отдельной командой
Тестовый номерзвонит только на негомассовый обзвон идёт клиентам
Кнопка «Позвонить сейчас»звонит клиенту сразутак же — звонок клиенту
Звонки по самовывозувыключены галочкойвключаются отдельно от доставки

Как проверяется перед запуском

  • Тестовый звонок на свой номер: диалог, дни, половина дня, запись результата
  • Прогон диалога без звонка: проверка всех веток (согласие, обрыв, молчание, отказ)
  • Проверка окон: робот предлагает именно те дни, которые заданы в админке
Автодозвон SuperSam · как это работает15

Что пока не учитывается

Честный список: это важно знать до запуска

График

  • Праздники: календаря нерабочих дат нет — если праздник попадёт в окно, робот его предложит
  • Часы доставки используются частично: границу половины дня робот проговаривает словами, без конкретного времени

Настройки, которые пока не влияют

  • «Попыток в день» — поле есть, но расписание повторов задаётся интервалом между попытками
  • «Попытки следующего дня (10:00, 14:00)» — пока не используются
  • «Таймаут звонка, 30 секунд» — звонок ждёт ответа 45 секунд (значение в самом звонке)

Что предлагаем уточнить до боевого запуска

  • Список праздников (или решение не учитывать их)
  • Нужен ли жёсткий график повторов по времени, кроме паузы в час
  • Кто и когда разбирает заказы, которые робот не согласовал за 5 попыток
Автодозвон SuperSam · как это работает16

Коротко

Одна страница вместо всей презентации

Что делает система

  • Ждёт 90 минут после SMS (или после последней попытки отправки, если SMS не ушло) и звонит, если клиент не подтвердил день сам
  • Предлагает только рабочие дни по графику: самовывоз — включая сегодня, доставка — со следующего дня
  • Спрашивает день и половину дня, подтверждает вслух, даёт один раз переделать выбор
  • Граница половины дня — 13:00: звоним после 13:00, значит говорим про вторую половину
  • Повторяет попытки не чаще чем раз в час, максимум 5 раз, только 09:00–20:00 в рабочие дни
  • После двух «взял, но не выбрал» или исчерпания попыток передаёт заказ менеджеру
  • Каждая попытка и результат видны в карточке заказа; ручной звонок идёт клиенту и показывает своё состояние

Что осталось решить

  • Праздники и точный график повторов — по вашим правилам
  • Момент включения боевого обзвона клиентов и по самовывозу
Автодозвон SuperSam · как это работает17
diff --git a/docs/autocall/autocall-how-it-works.pdf b/docs/autocall/autocall-how-it-works.pdf index 5fbe784b3aab0454152e25e0acaee15f3cad7eb2..b826e2eda382c097caa698cf55e8e8b8db7d3a4b 100644 GIT binary patch delta 5179 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 P7AHvd%HzqDvseEMs0G&6 delta 3104 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 H!m9rOvd(>v diff --git a/docs/autocall/deck_content.py b/docs/autocall/deck_content.py index 71b73ba..fb0b2f8 100644 --- a/docs/autocall/deck_content.py +++ b/docs/autocall/deck_content.py @@ -211,6 +211,26 @@ SLIDES = [ "Если клиент уже согласовал слот, а потом начал менять и бросил трубку — прежний выбор сохраняется, ничего не теряется", ]}, }, + { + "kind": "tablecard", + "title": "Клиент не выбрал дату после приглашения", + "sub": "Срок ожидания считаем от последнего контакта — SMS или звонка робота", + "table": { + "rows": [ + ["Что настроено в системе", "От чего считаем срок", "Когда заказ уходит менеджеру"], + ["Только SMS-приглашение", "от последнего SMS", "через 3 ч (настройка в админке)"], + ["Плюс автодозвон", "ждём звонок робота, отсчёт — от него", "через 3 ч после последнего звонка"], + ["Плюс второе приглашение", "сначала ждём второе SMS, потом звонок", "после последнего контакта по последнему каналу"], + ["Звонить нельзя (гео, исключение, «не звонить»)", "от последнего SMS", "через 3 ч — звонок не ждём"], + ], + "widths": [250, 300, 318], + }, + "full": {"title": "Что важно", "accent": ACCENT2, "items": [ + "Ожидание включается только по каналам, которые реально включены в системе: если автодозвона нет — не ждём звонок", + "Позвонили позже — ожидание начинается заново: менеджер получает заказ, только когда клиент молчит по всем каналам", + "Клиент выбрал дату — из ожидания выходим сразу, даже если срок не истёк", + ]}, + }, { "kind": "cards3full", "title": "Когда подключается менеджер", diff --git a/scripts/autocall_scheduler.py b/scripts/autocall_scheduler.py index 083e797..2353d78 100644 --- a/scripts/autocall_scheduler.py +++ b/scripts/autocall_scheduler.py @@ -537,7 +537,8 @@ def get_queued_calls(conn): if ids: cur.execute(""" SELECT og.id, og.customer_phone, og.customer_phone_normalized, - og.created_at, og.delivery_type, og.town + og.created_at, og.delivery_type, og.town, + og.notification_status, og.delivery_date, og.autocall_opt_out FROM order_groups og WHERE og.id IN %s """, (ids,)) og = {r["id"]: r for r in cur.fetchall()} @@ -549,6 +550,9 @@ def get_queued_calls(conn): r.setdefault("created_at", g.get("created_at")) r.setdefault("delivery_type", g.get("delivery_type")) r.setdefault("town", g.get("town")) + r["notification_status"] = g.get("notification_status") + r["delivery_date"] = g.get("delivery_date") + r["autocall_opt_out"] = g.get("autocall_opt_out") return rows @@ -586,6 +590,18 @@ def process_queue(conn, settings): log.info(f"Queue item {q['queue_id']} skipped by exclusion ({excl['rule_type']}={excl['value']}): {excl.get('reason') or 'no reason'}") mark_queue_processed(conn, q["queue_id"]) continue + # Заказ уже согласован (клиент выбрал дату или менеджер поставил её сам) — + # роботу звонить не о чем, иначе он предложит дни по заказу, где всё решено. + # Заявку НЕ закрываем (правило владельца) — просто не обрабатываем. + agreed = (str(q.get("notification_status") or "") in ("confirmed", "manual_required", "opt_out") + or q.get("delivery_date") is not None) + if agreed and not is_test: + log.info( + f"Queue item {q['queue_id']} (group {group_id}): заказ уже согласован " + f"(статус {q.get('notification_status')}, дата {q.get('delivery_date')}) — " + "звонок не нужен, заявка остаётся в очереди" + ) + continue if group_id not in attempt_map: if group_id: with conn.cursor() as cur: diff --git a/scripts/sms_manual_campaign.py b/scripts/sms_manual_campaign.py index 2ae8fdd..909b254 100644 --- a/scripts/sms_manual_campaign.py +++ b/scripts/sms_manual_campaign.py @@ -4,14 +4,21 @@ SuperSam — Manual Campaign (State Machine) Заменяет n8n workflow "Ручное управление" Логика: - Если после второй SMS (notification_status = 'second_sms_sent') - прошло auto_manual_after_hours и клиент не согласовал + Клиент не выбрал дату/время за auto_manual_after_hours (настройка, по умолчанию 3 ч) + после ПОСЛЕДНЕГО КОНТАКТА — SMS или звонка робота — и (delivery_status = 'pending_confirmation'): → notification_status = 'manual_required' → delivery_status = 'manual_confirmation_required' - → next_notification_check_at = +3 мин → Telegram-уведомление + Ожидание считаем только по тем каналам, что реально включены в системе + (load_channels): + • второе приглашение включено → ждём, пока оно уйдёт; + • автодозвон включён и номеру звонить можно → ждём звонок, отсчёт + начнётся от него (позвонили позже — ожидание начинается заново); + • канал выключен / звонить нельзя (не звонить, гео, исключение) — + не ждём его и переводим по сроку ожидания. + Расписание: 9-21, Пн-Сб (настраивается из админки) Скрипт НЕ отправляет SMS — только перевод в ручное управление. """ @@ -111,61 +118,129 @@ def is_within_work_hours(settings): # ─── State Machine ─────────────────────────────────────────────────────────── -def get_groups_to_manual(conn): - """Группы, где срок ожидания истёк и нужно перевести в ручное управление. +def get_groups_to_manual(conn, channels=None, manual_after_hours=3): + """Группы, где клиент не выбрал дату/время и пора в ручную работу. - Сценарии: - 1. second_sms_sent + next_check в прошлом → клиент не согласовал после 2х SMS - 2. first_sms_sent + next_check в прошлом + >3h + second_sms_sent_at IS NOT NULL → клиент не согласовал после 2й SMS - 3. link_ready + next_check в прошлом + нет активных SMS → зависшая группа - 4. sms_sending/second_sms_sending/checking + >3h без доставки → зависшая группа + Отсчёт — от ПОСЛЕДНЕГО КОНТАКТА (SMS или звонок робота), а не от первого + приглашения: если клиенту позвонили позже, ожидание начинается заново. + + Каналы берём из настроек: ждём только те шаги, которые реально включены. + • второе приглашение включено → ждём, пока оно уйдёт; + • автодозвон включён и этому номеру звонить можно → ждём звонка; + • канал выключен (или звонить нельзя: не звонить/гео/исключение) — + не ждём его и переводим в ручную работу по сроку ожидания. + + Гео-исключения и «не звонить» не дают перевести заказ в ручную: такие + заказы в системе ведёт только менеджер, автоматика их не трогает. """ + channels = channels or {} with conn.cursor(cursor_factory=RealDictCursor) as cur: - cur.execute(""" + cur.execute(r""" + WITH contact AS ( + SELECT og.id, + GREATEST( + COALESCE(og.first_sms_sent_at, TIMESTAMP 'epoch'), + COALESCE(og.second_sms_sent_at, TIMESTAMP 'epoch'), + COALESCE(og.sms_sent_at, TIMESTAMP 'epoch'), + COALESCE(og.called_at, TIMESTAMP 'epoch'), + COALESCE((SELECT max(l.created_at) FROM sms_campaign_log l + WHERE l.order_group_id = og.id + AND COALESCE(l.status, '') IN + ('sent','checking','delivered','send_failed','error','limit_exceeded')), + TIMESTAMP 'epoch'), + COALESCE((SELECT max(a.created_at) FROM autocall_log a + WHERE a.order_group_id = og.id), TIMESTAMP 'epoch') + ) AS last_contact_at, + (SELECT count(*) FROM autocall_log a + WHERE a.order_group_id = og.id) AS call_attempts + FROM order_groups og + ), + callable AS ( + SELECT og.id, + (og.autocall_opt_out = false + AND COALESCE(og.customer_phone, '') <> '' + AND NOT EXISTS ( + SELECT 1 FROM autocall_city_rules cr + WHERE cr.city = og.town AND cr.calls_enabled = false) + AND NOT EXISTS ( + SELECT 1 FROM sms_city_rules cr2 + WHERE cr2.city = og.town AND cr2.enabled = false + AND cr2.campaign_type IN ('*', 'autocall_slot') + AND (cr2.valid_from IS NULL OR cr2.valid_from <= CURRENT_DATE) + AND (cr2.valid_until IS NULL OR cr2.valid_until >= CURRENT_DATE)) + AND NOT EXISTS ( + SELECT 1 FROM autocall_exclusions e + WHERE e.active = true + AND ((e.rule_type = 'phone' + AND e.value = '7' || right(regexp_replace(og.customer_phone, '\D', '', 'g'), 10)) + OR (e.rule_type = 'prefix' + AND ('7' || right(regexp_replace(og.customer_phone, '\D', '', 'g'), 10)) + LIKE e.value || '%%'))) + ) AS can_call + FROM order_groups og + ) SELECT og.id, og.group_key, og.customer_name, og.customer_phone, og.notification_status, og.delivery_link, og.first_sms_sent_at, og.second_sms_sent_at, og.sms_sent_at, og.next_notification_check_at, - og.sms_attempts + og.sms_attempts, + c.last_contact_at, c.call_attempts, cl.can_call FROM order_groups og + JOIN contact c ON c.id = og.id + JOIN callable cl ON cl.id = og.id WHERE og.delivery_status = 'pending_confirmation' - -- Гео-исключения — общие для SMS и звонков + -- Клиент уже выбрал дату/время — в ручную не переводим + AND og.delivery_date IS NULL + -- Контакта не было — отсчитывать не от чего + AND c.last_contact_at > TIMESTAMP 'epoch' + -- Срок ожидания истёк от последнего контакта + AND NOW() - c.last_contact_at >= make_interval(hours => %s) + -- Второе приглашение ждём, только если канал включён + AND (NOT %s OR og.second_sms_sent_at IS NOT NULL) + -- Звонок ждём, только если автодозвон включён и звонить можно + AND (NOT %s OR c.call_attempts > 0 OR NOT cl.can_call) + -- Гео-исключения — общие для SMS и ручного управления AND NOT EXISTS ( SELECT 1 FROM sms_city_rules r WHERE r.city = og.town AND r.enabled = false AND r.campaign_type IN ('*', 'manual') - AND (r.valid_from IS NULL OR r.valid_from <= CURRENT_DATE) + AND (r.valid_from IS NULL OR r.valid_from <= CURRENT_DATE) AND (r.valid_until IS NULL OR r.valid_until >= CURRENT_DATE) ) - AND ( - -- 1. second_sms_sent + срок истёк - (COALESCE(og.notification_status, '') = 'second_sms_sent' - AND (og.next_notification_check_at IS NULL OR og.next_notification_check_at <= NOW())) - -- 2. first_sms_sent + срок истёк (>3h после отправки) - -- ВАЖНО: вторая SMS должна быть уже отправлена (second_sms_sent_at IS NOT NULL) - -- Иначе second_campaign не успеет отправить вторую SMS - OR (COALESCE(og.notification_status, '') = 'first_sms_sent' - AND (og.next_notification_check_at IS NULL OR og.next_notification_check_at <= NOW()) - AND og.first_sms_sent_at < NOW() - INTERVAL '3 hours' - AND og.second_sms_sent_at IS NOT NULL) - -- 3. link_ready + next_check в прошлом + нет активных SMS в логе - OR (COALESCE(og.notification_status, '') = 'link_ready' - AND (og.next_notification_check_at IS NULL OR og.next_notification_check_at <= NOW()) - AND NOT EXISTS ( - SELECT 1 FROM sms_campaign_log scl - WHERE scl.order_group_id = og.id - AND scl.status IN ('sent', 'checking') - AND scl.created_at > NOW() - INTERVAL '2 hours' - )) - -- 4. sms_sending/checking + >3h без доставки (зависло) - OR (COALESCE(og.notification_status, '') IN ('sms_sending', 'second_sms_sending', 'checking') - AND COALESCE(og.sms_sent_at, og.created_at) < NOW() - INTERVAL '3 hours') - ) - ORDER BY og.created_at ASC - """) + ORDER BY c.last_contact_at ASC + """, ( + int(manual_after_hours or 3), + bool(channels.get("second_sms")), + bool(channels.get("autocall")), + )) return [dict(r) for r in cur.fetchall()] + +def load_channels(conn): + """Какие каналы связи сейчас включены в системе. + + `first_sms` — первое приглашение, `second_sms` — повторное SMS, + `autocall` — робот-автодозвон (настройки кампании 'autocall_slot'). + От этого зависит, чьё ожидание отсчитывать: ждём только включённые шаги. + """ + channels = {"first_sms": True, "second_sms": False, "autocall": False} + try: + with conn.cursor() as cur: + cur.execute(""" + SELECT campaign_type, enabled + FROM sms_campaign_settings + WHERE campaign_type IN ('first_sms', 'second_sms', 'autocall_slot') + """) + for campaign_type, enabled in cur.fetchall(): + if campaign_type == "autocall_slot": + channels["autocall"] = bool(enabled) + else: + channels[campaign_type] = bool(enabled) + except Exception: # noqa: BLE001 — не прочитали настройки: считаем, что есть только первое SMS + pass + return channels + def update_order_group(conn, group_id, fields): with conn.cursor() as cur: set_parts = [] @@ -182,35 +257,55 @@ def update_order_group(conn, group_id, fields): # ─── Основная логика ───────────────────────────────────────────────────────── -def step_move_to_manual(conn, settings): +def step_move_to_manual(conn, settings, channels=None): """Перевод групп в ручное управление""" tg_chat = settings.get("telegram_chat_id", TELEGRAM_CHAT_ID) - delay_minutes = settings.get("auto_manual_after_hours", 3) + wait_hours = int(settings.get("auto_manual_after_hours", 3) or 3) + channels = channels or load_channels(conn) - groups = get_groups_to_manual(conn) - log.info(f"Step 1: {len(groups)} groups to move to manual") + groups = get_groups_to_manual(conn, channels=channels, manual_after_hours=wait_hours) + log.info( + f"Step 1: {len(groups)} groups to move to manual " + f"(ожидание {wait_hours} ч от последнего контакта; каналы: " + f"первое SMS={'вкл' if channels.get('first_sms') else 'выкл'}, " + f"второе SMS={'вкл' if channels.get('second_sms') else 'выкл'}, " + f"автодозвон={'вкл' if channels.get('autocall') else 'выкл'})" + ) moved = 0 + dry_run = os.environ.get("DRY_RUN") == "1" for group in groups: group_id = str(group["id"]) name = group.get("customer_name") or group.get("group_key", "—") phone = group.get("customer_phone", "") delivery_link = group.get("delivery_link", "") + attempts = group.get("sms_attempts") or 1 - log.info(f"Group {group_id}: moving to manual_required ({name})") + if dry_run: + log.info(f"[DRY RUN] Group {group_id}: будет переведена в ручное ({name})") + moved += 1 + continue + + log.info( + f"Group {group_id}: moving to manual_required ({name}); " + f"последний контакт {group.get('last_contact_at')}, звонков {group.get('call_attempts')}" + ) update_order_group(conn, group_id, { "notification_status": "manual_required", "delivery_status": "manual_confirmation_required", - "sms_attempts": 2, + # Попытки НЕ подменяем: показываем столько, сколько реально отправлено + "sms_attempts": attempts, "last_sms_error": None, - "next_notification_check_at": f"NOW() + INTERVAL '{int(delay_minutes)} minutes'", + # Дальше ждать нечего — проверки по этому заказу закончились + "next_notification_check_at": None, "status": "manual_required", }) send_telegram( f"🔧 Ручное управление\n{name} ({phone})\n" - f"Клиент не согласовал доставку после двух SMS\n" + f"Клиент не выбрал дату/время за {wait_hours} ч после последнего контакта " + f"({'звонки' if channels.get('autocall') else 'SMS'})\n" f"Ссылка: {delivery_link}", tg_chat, ) @@ -272,8 +367,9 @@ def main(): cur.execute("UPDATE sms_campaign_settings SET run_requested = false WHERE campaign_type = 'manual'") conn.commit() log.info("RESTART: manual campaign — will process all eligible groups now") - else: - moved = step_move_to_manual(conn, settings) + + channels = load_channels(conn) + moved = step_move_to_manual(conn, settings, channels=channels) log.info(f"Run summary: moved_to_manual={moved}") diff --git a/src/components/admin/SmsCampaignPanel.jsx b/src/components/admin/SmsCampaignPanel.jsx index adc03b4..51fbf61 100644 --- a/src/components/admin/SmsCampaignPanel.jsx +++ b/src/components/admin/SmsCampaignPanel.jsx @@ -511,7 +511,7 @@ export const SmsCampaignPanel = () => { )} )} - updateSetting("auto_manual_after_hours", parseInt(v) || 3)} /> + updateSetting("auto_manual_after_hours", parseInt(v) || 3)} hint="Отсчёт от последнего контакта: SMS или звонок робота. Если автодозвон включён — ждём звонок, если включено второе SMS — ждём его." /> updateSetting("telegram_chat_id", v)} /> @@ -804,7 +804,7 @@ export const SmsCampaignPanel = () => { }; // ── Helpers ────────────────────────────────────────────────────────────────── -const SettingField = ({ label, value, onChange }) => ( +const SettingField = ({ label, value, onChange, hint }) => ( ); diff --git a/src/components/orders/AutocallStatusCard.jsx b/src/components/orders/AutocallStatusCard.jsx index 2cc03d8..7d2016d 100644 --- a/src/components/orders/AutocallStatusCard.jsx +++ b/src/components/orders/AutocallStatusCard.jsx @@ -161,9 +161,23 @@ export const AutocallStatusCard = ({ order, userRole }) => { }, [order?.id, order?.delivery_type, order?.created_at]); const autocallBlocked = availability ? !availability.possible : false; + // Заказ уже согласован — дату выбрал клиент (в том числе в звонке) или поставил менеджер + const orderAgreed = Boolean(order?.delivery_date || order?.deliveryDate) + || ["confirmed", "manual_required", "opt_out"].includes( + String(order?.notification_status || order?.notificationStatus || ""), + ); + const process = useMemo( - () => describeCallProcess({ queueItem: recentQueue, callAfter, settings: { ...(settings || {}), call_unreachable: callUnreachable } }), - [recentQueue, callAfter, settings, callUnreachable], + () => describeCallProcess({ + queueItem: recentQueue, + callAfter, + settings: { + ...(settings || {}), + call_unreachable: callUnreachable, + order_agreed: orderAgreed, + }, + }), + [recentQueue, callAfter, settings, callUnreachable, orderAgreed], ); const processing = process?.phase === "pending" || process?.phase === "pending_later" || process?.phase === "dialing"; @@ -338,17 +352,28 @@ export const AutocallStatusCard = ({ order, userRole }) => { {/* Кнопки */} {canManage && (
- {autocallBlocked && ( + {autocallBlocked && !orderAgreed && (
🚫 Автодозвон недоступен: {availability?.reason || "по графику роботу нечего предложить"}. Выберите день вручную или позвоните сами.
)} + {orderAgreed && ( +
+ Заказ уже согласован — автодозвон не нужен. Если надо переспросить клиента, позвоните сами. +
+ )}