diff --git a/docs/autocall/autocall-how-it-works.html b/docs/autocall/autocall-how-it-works.html index f70b688..70e2f39 100644 --- a/docs/autocall/autocall-how-it-works.html +++ b/docs/autocall/autocall-how-it-works.html @@ -83,6 +83,6 @@ -
СУПЕР САМ · ДОСТАВКА И САМОВЫВОЗ

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

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

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

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

 

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

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

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

Три задачи

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

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

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

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

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

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

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

Важно

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Почему так

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

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

Не нажал цифру 2 раза

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

График

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

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

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

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

Коротко

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

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

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

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

+
СУПЕР САМ · ДОСТАВКА И САМОВЫВОЗ

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

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

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

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

 

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

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

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

Три задачи

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

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

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

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

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

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

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

Важно

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Почему так

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

График

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

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

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

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

Коротко

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

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

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

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

diff --git a/docs/autocall/autocall-how-it-works.pdf b/docs/autocall/autocall-how-it-works.pdf index 5407097..5fbe784 100644 Binary files a/docs/autocall/autocall-how-it-works.pdf and b/docs/autocall/autocall-how-it-works.pdf differ diff --git a/docs/autocall/deck_content.py b/docs/autocall/deck_content.py index 0c8734f..71b73ba 100644 --- a/docs/autocall/deck_content.py +++ b/docs/autocall/deck_content.py @@ -16,7 +16,7 @@ TITLE_SLIDE = { "sub": "Робот звонит клиенту, согласует день и половину дня — без участия оператора", "lines": [ "Что описано: правила звонка, паузы, поведение в нештатных случаях (не дозвонились,", - "сбросили трубку, не нажали цифру), где что настраивается и что видит менеджер.", + "сбросили трубку, не выбрали день/время), где что настраивается и что видит менеджер.", "", "Основано на текущем коде и настройках боевой системы, без обещаний и продажи.", ], @@ -52,7 +52,7 @@ SLIDES = [ "steps": [ ("1. Заказ из 1С", "приходит готовым\nпо обмену"), ("2. Ссылка и SMS", "ссылка создаётся\nсама, SMS уходит"), - ("3. Пауза 90 минут", "настройка админки:\nстолько ждём ответа"), + ("3. Пауза 90 минут", "после SMS или\nпосле попытки, если\nSMS не ушло"), ("4. Звонок робота", "если клиент не\nподтвердил по ссылке"), ("5. Результат в заказ", "дата и половина дня\nв самом заказе"), ], @@ -187,7 +187,7 @@ SLIDES = [ ], "full": {"title": "Для менеджера это видно в карточке", "accent": ACCENT2, "items": [ "Каждая попытка — отдельная строка истории: время, результат, длительность разговора", - "Статус заказа меняется на «нужен менеджер» — с комментарием «не дозвонились / не нажал»", + "Статус заказа меняется на «нужен менеджер» — с комментарием «не дозвонились / не выбрал»", "Отдельно красным предупреждаем, если заявка «Позвонить сейчас» висит больше 15 минут", ]}, }, @@ -198,8 +198,8 @@ SLIDES = [ "table": { "rows": [ ["Когда положили трубку", "Что запишет робот", "Что увидит менеджер"], - ["до выбора дня", "ответил, ничего не нажал", "«взял, не нажал (день)» — попытка зачтена"], - ["день выбрал, время не успел", "день выбран, время нет", "«взял, не нажал (время)»"], + ["до выбора дня", "ответил, день не выбрал", "«взял, не выбрал день» — попытка зачтена"], + ["день выбрал, время не успел", "день выбран, время нет", "«взял, не выбрал время»"], ["выбор подтвердил и сбросил", "слот согласован", "дата и половина дня в заказе, статус «согласовано»"], ["начал менять выбор и сбросил", "остаётся прежний слот", "«правка не доведена» — прежнее согласование в силе"], ], @@ -216,10 +216,10 @@ SLIDES = [ "title": "Когда подключается менеджер", "sub": "Робот доводит дело до конца сам, но в трёх случаях передаёт человеку", "cards": [ - {"title": "Не нажал цифру 2 раза", "accent": WARN, "items": [ - "Робот писал «взял, но не нажал» дважды", - "Заказ помечается «нужен менеджер», комментарий «не дозвонились / не нажал»", - "Порог «не нажал × N» настраивается в админке автодозвона", + {"title": "Не выбрал день/время 2 раза", "accent": WARN, "items": [ + "Робот дважды фиксировал «взял, но не выбрал»", + "Заказ помечается «нужен менеджер», комментарий «не дозвонились / не выбрал»", + "Порог «не выбрал × N» настраивается в админке автодозвона", ]}, {"title": "Исчерпаны 5 попыток", "accent": WARN, "items": [ "Каждый звонок — отдельная попытка", @@ -253,11 +253,11 @@ SLIDES = [ "Вне рабочих часов кампании (09:00–20:00) и вне рабочих дней", "Город исключён целиком или для города выключен канал", "В админке автодозвона выключены звонки по городу — например, Ялта: SMS идут, звонков нет", - "Меньше 90 минут прошло после SMS со ссылкой", + "Меньше 90 минут прошло после последней попытки отправки SMS: если приглашение ушло — считаем от отправки, если не ушло — от попытки", ]}, ], "full": {"title": "Админка автодозвона: где это переключается", "accent": ACCENT2, "items": [ - "Панель «Автодозвон»: тестовый режим, часы и дни звонков, паузы, лимит попыток, порог «не нажал × N»", + "Панель «Автодозвон»: тестовый режим, часы и дни звонков, паузы, лимит попыток, порог «не выбрал × N»", "Панель «Локации»: для каждого города — звонки включены или выключены (SMS не затрагивается)", "Панель исключений: номера и префиксы, на которые не звоним, с причиной", ]}, @@ -330,12 +330,12 @@ SLIDES = [ "sub": "Одна страница вместо всей презентации", "cards": [ {"title": "Что делает система", "accent": ACCENT, "full": True, "dense": True, "items": [ - "Ждёт 90 минут после SMS и звонит, если клиент не подтвердил день сам", + "Ждёт 90 минут после SMS (или после последней попытки отправки, если SMS не ушло) и звонит, если клиент не подтвердил день сам", "Предлагает только рабочие дни по графику: самовывоз — включая сегодня, доставка — со следующего дня", "Спрашивает день и половину дня, подтверждает вслух, даёт один раз переделать выбор", "Граница половины дня — 13:00: звоним после 13:00, значит говорим про вторую половину", "Повторяет попытки не чаще чем раз в час, максимум 5 раз, только 09:00–20:00 в рабочие дни", - "После двух «взял, но не нажал» или исчерпания попыток передаёт заказ менеджеру", + "После двух «взял, но не выбрал» или исчерпания попыток передаёт заказ менеджеру", "Каждая попытка и результат видны в карточке заказа; ручной звонок идёт клиенту и показывает своё состояние", ]}, ], diff --git a/scripts/autocall_scheduler.py b/scripts/autocall_scheduler.py index 56615b4..961fdb6 100644 --- a/scripts/autocall_scheduler.py +++ b/scripts/autocall_scheduler.py @@ -85,16 +85,7 @@ def resolve_scenario(created_at, now=None): return "A" if created.date() == current.date() else "B" -def get_groups_to_call(conn, settings): - """Группы, которым пора звонить.""" - delay_min = settings.get("delay_after_sms_minutes", 90) - retry_min = settings.get("retry_interval_minutes", 120) - max_attempts = settings.get("max_attempts", 5) - no_input_escalate = settings.get("no_input_escalate_after", 2) - pickup_enabled = bool(settings.get("pickup_calls_enabled", False)) - - with conn.cursor(cursor_factory=RealDictCursor) as cur: - cur.execute(""" +GROUPS_TO_CALL_SQL = """ SELECT og.id, og.customer_phone, og.customer_phone_normalized, og.notification_status, og.delivery_status, og.sms_sent_at, og.autocall_opt_out, og.created_at, og.delivery_type, og.town, @@ -104,14 +95,43 @@ def get_groups_to_call(conn, settings): WHERE l.order_group_id = og.id AND l.outcome IN ('no_input_day','no_input_half')) as no_input_count, (SELECT max(l.created_at) FROM autocall_log l - WHERE l.order_group_id = og.id) as last_call_at + WHERE l.order_group_id = og.id) as last_call_at, + -- От чего считаем паузу перед звонком: + -- * SMS ушло — от времени отправки; + -- * SMS не ушло/не доставлено — от последней попытки отправки + -- (владелец: «если SMS не отправлено — звоним через 90 минут + -- после последней попытки»). + COALESCE( + CASE WHEN t.try_at IS NOT NULL + AND t.try_status NOT IN ('sent', 'delivered') + THEN t.try_at END, + og.sms_sent_at + ) as call_after FROM order_groups og + LEFT JOIN LATERAL ( + SELECT COALESCE(l.updated_at, l.created_at) AS try_at, l.status AS try_status + FROM sms_campaign_log l + WHERE l.order_group_id = og.id + AND l.campaign_type IN ('first_sms', 'second_sms') + ORDER BY COALESCE(l.updated_at, l.created_at) DESC + LIMIT 1 + ) t ON true WHERE og.delivery_status = 'pending_confirmation' AND og.autocall_opt_out = false AND og.notification_status NOT IN ('confirmed', 'manual_required', 'opt_out') - AND og.sms_sent_at IS NOT NULL - -- Пауза после SMS прошла - AND og.sms_sent_at < NOW() - make_interval(mins => %s) + -- Пауза после SMS (или после неудачной попытки) прошла + AND COALESCE( + CASE WHEN t.try_at IS NOT NULL + AND t.try_status NOT IN ('sent', 'delivered') + THEN t.try_at END, + og.sms_sent_at + ) IS NOT NULL + AND COALESCE( + CASE WHEN t.try_at IS NOT NULL + AND t.try_status NOT IN ('sent', 'delivered') + THEN t.try_at END, + og.sms_sent_at + ) < NOW() - make_interval(mins => %s) -- Не звонили или прошёл интервал повтора AND ( NOT EXISTS (SELECT 1 FROM autocall_log l WHERE l.order_group_id = og.id) @@ -142,8 +162,20 @@ def get_groups_to_call(conn, settings): ) -- Самовывоз звоним только если разрешено настройкой AND (COALESCE(og.delivery_type, 'delivery') <> 'pickup' OR %s) - ORDER BY og.sms_sent_at ASC - """, (delay_min, retry_min, max_attempts, pickup_enabled)) + ORDER BY call_after ASC +""" + + +def get_groups_to_call(conn, settings): + """Группы, которым пора звонить.""" + delay_min = settings.get("delay_after_sms_minutes", 90) + retry_min = settings.get("retry_interval_minutes", 120) + max_attempts = settings.get("max_attempts", 5) + pickup_enabled = bool(settings.get("pickup_calls_enabled", False)) + + with conn.cursor(cursor_factory=RealDictCursor) as cur: + cur.execute(GROUPS_TO_CALL_SQL, + (delay_min, retry_min, max_attempts, pickup_enabled)) return [dict(r) for r in cur.fetchall()] def is_work_time(settings, now=None): diff --git a/src/components/UI/CallStatusBadge.jsx b/src/components/UI/CallStatusBadge.jsx index 9ba1185..929c2ab 100644 --- a/src/components/UI/CallStatusBadge.jsx +++ b/src/components/UI/CallStatusBadge.jsx @@ -3,7 +3,7 @@ * @description Бейдж автодозвона в строке реестра доставок. * * Раньше наружу выходили две безликие плашки: «Автодозвон ✅» и «Автодозвон ❌» — - * по ним нельзя было понять, не дозвонились, не нажали цифру или заказ ушёл + * по ним нельзя было понять, не дозвонились, не выбрал клиент день/время или заказ ушёл * менеджеру. Теперь у каждого состояния своя подпись, взятая из * AUTOCALL_STATUS_META (тот же список, что в фильтрах, — не разъезжается). * @@ -31,7 +31,7 @@ const STYLES = { const TITLES = { confirmed: "Клиент согласовал день и половину дня по звонку робота", no_answer: "Робот звонил, трубку не взяли", - no_input: "Трубку взяли, но цифру в меню не нажали", + no_input: "Трубку взяли, но день/время клиент не выбрал", manual: "Нужен менеджер: неверный номер или ошибка звонка", opt_out: "Звонки по этому клиенту запрещены вручную", excluded: "Исключение: по этому номеру или городу не звоним", diff --git a/src/components/UI/CallStatusBadge.test.jsx b/src/components/UI/CallStatusBadge.test.jsx index 18cc365..18e3da1 100644 --- a/src/components/UI/CallStatusBadge.test.jsx +++ b/src/components/UI/CallStatusBadge.test.jsx @@ -1,7 +1,7 @@ /** * @file CallStatusBadge.test.jsx * @description Бейдж автодозвона в реестре доставок: по плашке должно быть понятно, - * что с заказом — не звонили, согласовано, не дозвонились, не нажал цифру, + * что с заказом — не звонили, согласовано, не дозвонились, не выбрал день/время, * нужен менеджер, запрет или исключение. Без «Автодозвон ❌» на всё подряд. */ import { describe, it, expect } from "vitest"; @@ -32,7 +32,7 @@ describe("resolveCallBadge — что видно в строке", () => { expect(new Set(labels).size).toBe(labels.length); expect(labels[0]).toContain("согласовано"); expect(labels[1]).toContain("не дозвонились"); - expect(labels[2]).toContain("не нажал цифру"); + expect(labels[2]).toContain("не выбрал день/время"); expect(labels[3]).toContain("нужен менеджер"); }); diff --git a/src/components/admin/analytics/CommsAnalyticsTab.jsx b/src/components/admin/analytics/CommsAnalyticsTab.jsx index 717ff82..d76eb9f 100644 --- a/src/components/admin/analytics/CommsAnalyticsTab.jsx +++ b/src/components/admin/analytics/CommsAnalyticsTab.jsx @@ -29,8 +29,8 @@ const OUTCOME_LABELS = { confirmed_kept_previous: "Правка не доведена", confirmed_silent: "Молчание", no_answer: "Не взял", - no_input_day: "Не нажал (день)", - no_input_half: "Не нажал (время)", + no_input_day: "Не выбрал день", + no_input_half: "Не выбрал время", operator: "Менеджер", opt_out: "Отказ", invalid_number: "Неверный номер", diff --git a/src/components/admin/autocall/AutocallChannelTab.jsx b/src/components/admin/autocall/AutocallChannelTab.jsx index 3c2e132..4571925 100644 --- a/src/components/admin/autocall/AutocallChannelTab.jsx +++ b/src/components/admin/autocall/AutocallChannelTab.jsx @@ -27,8 +27,8 @@ const OUTCOME_META = { confirmed_kept_previous:{ label: "Правка не доведена", tone: "warning", icon: "🔁" }, confirmed_silent: { label: "Подтвердил молчанием", tone: "accent", icon: "🤫" }, no_answer: { label: "Не взял трубку", tone: "neutral", icon: "📵" }, - no_input_day: { label: "Взял, не нажал (день)", tone: "warning", icon: "🤐" }, - no_input_half: { label: "Взял, не нажал (время)", tone: "warning", icon: "🤐" }, + no_input_day: { label: "Взял, не выбрал день", tone: "warning", icon: "🤐" }, + no_input_half: { label: "Взял, не выбрал время", tone: "warning", icon: "🤐" }, invalid_number: { label: "Неверный номер", tone: "danger", icon: "❌" }, error: { label: "Ошибка", tone: "danger", icon: "⚠️" }, }; @@ -64,8 +64,8 @@ const slotText = (l) => { return null; }; -const SettingField = ({ label, value, onChange }) => ( -