Перейти к содержимому

Разрешить сайту считать ваши посещения с помощью Яндекс Метрики? Для этого она сохранит в браузере файлы cookie. Как сайт использует cookie

VC
Кейс 2 из 25 · Интернет-торговля · поддержка

На пропущенный звонок покупатель получает SMS с настоящим сроком отгрузки

Телефония сообщает о пропущенном звонке. Система находит клиента и его заказы в двух магазинах Shopify и отправляет SMS с настоящим статусом отгрузки. А в CRM появилась страница «Пропущенные»: кто звонил, что у него в заказах и готовый текст ответа.

Отрасль
Интернет-торговля, покупатели в США
Технологии
Python · уведомления телефонии · Shopify API
Сроки
≈ 2 недели
Итог
≈180 пропущенных звонков в месяц получают автоответ
01 · Проблема

Покупатели звонят днём, когда у поддержки уже ночь

Бренд товаров для активного отдыха из США, два магазина на Shopify, команда поддержки в другом часовом поясе. Покупатель звонит в середине своего рабочего дня, а у команды в это время вечер или ночь. За 90 дней в журнале звонков 739 входящих. Из них 496 пришли в объявленные часы работы, и 310 из них остались без ответа.

Пропущенные звонки нигде не собирались. Утро начиналось с уведомлений в приложении телефонии: обычно около 5 за ночь, в пиковые дни до 25. Разбирать их вручную — отдельная работа, и регулярно этим никто не занимался.

Чаще всего звонили с вопросом «где мой заказ». Люди звонили именно потому, что не видели статуса: товар дорогой, срок производства длинный, а в письмах и в личном кабинете ничего нового.

Встроенный автоответ телефонии был настроен неудачно: ночью людям приходило дневное «мы скоро перезвоним». На одной из линий переключатель рабочих часов вообще был выключен, и этот шаблон уходил всегда и всем.

02 · Решение

Телефония сообщает о звонке, система находит заказ и отправляет SMS с настоящим сроком

После каждого звонка телефония присылает в CRM вебхук — автоматическое уведомление call.completed. Приёмник проверяет подпись, отбрасывает ненужные типы событий, не даёт обработать один звонок дважды (иначе клиент получил бы два сообщения) и записывает всё в журнал. Дальше по номеру звонящего находятся клиент и его заказы в обоих магазинах, и собирается ответ.

01
Уведомление о звонке

Событие call.completed от облачной телефонии: проверка подписи, отбор нужных событий, защита от повторной обработки одного звонка

02
Поиск клиента

По номеру звонящего находятся покупатель и его заказы в двух магазинах Shopify

03
Ступени ответа

От настоящего срока отгрузки до нейтрального запасного ответа

04
Отправка SMS

Тихие часы, пауза между сообщениями на один номер, ограничение частоты, режим черновика

05
Очередь в CRM

Страница «Пропущенные»: кто звонил, его заказы, готовый текст, отметка «разобрала»

Главная находка: даты отгрузки в заказе нет

Всё упиралось в один вопрос: откуда взять срок, который не стыдно отправить покупателю. Оказалось, что календарной даты отгрузки в заказе Shopify нет нигде. Я проверил ещё четыре источника, и ни в одном не нашлось срока, обещанного клиенту.

Обещанный срок есть только в одном месте — в названии тарифа доставки, который покупатель выбрал при оформлении заказа. Именно там он увидел срок и согласился на него. Из этой строки и вычисляется дата. На этом решении держится вся двухнедельная работа, и найти его нужно было до того, как писать код.

Ответ строится ступенями

Ответ выбирается по ступеням, от самого конкретного к нейтральному. Ни на одной ступени система не имеет права обмануть:

  • клиент и заказ найдены, обещанная дата ещё впереди → называем дату
  • обещанная дата уже прошла → дату не повторяем и честно пишем, что уточняем срок со складом
  • заказ есть, но срок определить нельзя → пишем про заказ без выдуманных обещаний
  • клиент по номеру не найден → нейтральный ответ, и звонок попадает в очередь к сотруднику

Правило «просроченную дату не повторяем» проверено на выгрузке всей истории заказов (больше 11 тыс.): ни один старый застрявший заказ не получает дату в будущем.

Защита при отправке

Автоматическое SMS уходит от имени компании, и никто не видит его перед отправкой. Поэтому ограничения здесь важнее самого текста:

  • тихие часы: сообщения уходят только с 11:00 до 21:00 по Нью-Йорку, а это удобное время во всех часовых поясах континентальной части США; Аляска и Гавайи считаются отдельно
  • пауза для каждого номера: если человек перезвонит, он не получит поток сообщений
  • общее ограничение частоты отправки
  • режим черновика: всё считается и записывается в журнал, но наружу не уходит; настоящая отправка включается отдельным переключателем

Страница «Пропущенные» в CRM

Первый ответ система отправляет сама, а разбираться со звонками всё равно человеку. Для этого есть отдельная страница: кто звонил и когда, номер, по которому можно сразу позвонить (ссылка tel:), найденный клиент, его заказы с состоянием («не отгружен · 82 дня»), готовый текст ответа и отметка «разобрала».

Страница выдерживает частичный сбой: одна испорченная строка очереди её не ломает. Если очередь не читается, на странице так и написано: «очередь не прочиталась». Если бы там стояло «звонков нет», сотрудник спокойно закрыл бы вкладку.

03 · Технологии

Только необходимое: система должна работать ночью без дежурного

Стандартная библиотека Python

Приёмник уведомлений и проверка подписи (HMAC) без веб-фреймворка

Облачная телефония

Уведомления о звонках, отправка SMS, расшифровки голосовой почты

Shopify Admin API

Поиск покупателя по номеру и его заказов в двух магазинах

Языковая модель

Пишет текст ответа под конкретный заказ

systemd

Служба приёмника, автоматический перезапуск, журнал

pytest

Общий набор из 2471 теста CRM и аналитики, в том числе на ступени ответа, тихие часы, защиту от повторов и разбор события

PythonWebhooksHMACSMS APIShopify Admin APILLMsystemdpytest
04 · Результат

Сначала разбор данных, потом код

Настоящий масштаб
108 ~180

пропущенных в месяц: задача оказалась примерно на две трети больше первоначальной оценки

Расшифровки уже есть
29%

пропущенных звонков — с голосовым сообщением и готовой расшифровкой: по ней видно, о чём звонили

Стоимость отправки
62%

сообщений стали помещаться в один сегмент SMS после замены одного символа

Работа началась с выгрузки из телефонии. Разбор показал три вещи, которых не было ни в одном отчёте. Пропущенных звонков оказалось примерно на две трети больше, чем считали. У 29% пропущенных звонков уже есть расшифровка голосового сообщения, и по ней видно, о чём звонили. А 36% SMS от клиентов оставались без ответа сотрудника.

Попутно исправлены пять ошибок, и из-за трёх из них система вообще не работала

  • поиск клиента падал из-за несовпадения параметров функций, и каждый звонок считался «клиент не найден»
  • разбор события искал данные не на том уровне вложенности и молча отбрасывал все пропущенные
  • номер звонящего искался в поле, которого в уведомлении нет
  • карточка автоответов сломала всю страницу звонков, с которой два сотрудника начинают утро. Код писали по угаданным именам полей, тестовые данные придумали так же, и тест лишь подтверждал догадку автора
  • подсчёт недозвонов был написан и покрыт тестами, но его никто не вызывал. Руководитель спрашивал, сколько теряется на недозвонах, и не получал ответа, хотя код для него уже был готов

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

Как проверяется, что уведомления доходят

Тишина в системе может означать две вещи: звонков действительно нет или уведомления перестали доходить. Пока эти два случая выглядят одинаково, отчёт о спокойной ночи ничего не стоит. Поэтому доставку уведомлений проверяют отдельно:

  • Приёмник 18 суток работал на коде почти трёхнедельной давности. Счётчик перезапусков показывал ноль, контрольная сумма файла на диске совпадала с репозиторием, но это ничего не доказывало: сверять нужно время запуска процесса со временем изменения файлов. Последствие — именно то, о чём весь этот кейс. Поиск клиента по номеру уходил запросом на другой сервер, при пустом ответе функция молча возвращала пустой результат, и звонок превращался в «клиент не найден». Перед перезапуском код сверен с репозиторием до байта и прогнаны 732 теста этой части системы; все прошли
  • Проверка «а были ли звонки» опиралась на служебный метод API телефонии. Этот метод отвечает ошибкой HTTP 400 без данных, простой разбор выводит «0 звонков», и ошибку не отличить от доказательства, что звонков не было. Это выяснилось на контрольном отрезке времени, где звонки точно были, а метод всё равно показал «0». Теперь для проверки используется другой метод, а доставка уведомлений проверяется четырьмя независимыми способами, и ни один из них не зависит от очереди
  • Контрольное правило записано заранее: «если к 18:00 понедельника не придёт ни одного события, проверяем настройки уведомлений». Проверять не пришлось: тишина объяснилась выходными. В пятницу пришло 4 события, в субботу 0, в воскресенье 3, в понедельник 6, и 41 час тишины пришёлся на выходные
  • Сторожевой скрипт, который ждал письма от поставщика телефонии, прочитал нужное письмо, записал его к себе и никого не предупредил. Фильтр отписок искал фразу во всём тексте письма, а система обращений поставщика добавляет ниже всю прежнюю переписку. Теперь разбирается только новая часть письма. Там же нашлась вторая проблема: в журнале скрипта было 50 строк, и все до одной — ошибки. Успешный запуск ничего не записывал, поэтому «сработал без происшествий» и «не работает вовсе» выглядели одинаково

Здесь действует то же правило, что на странице очереди: «не прочиталось» не показывается как «пусто».

Отдельно нашёлся один символ, который напрямую влиял на счёт за SMS. Длинное тире в шаблонах выходило за пределы базового набора символов SMS, и сообщение переходило в двухбайтную кодировку, где в один сегмент помещается меньше текста. Из-за этого все сообщения уходили минимум двумя сегментами. После замены длинного тире на дефис 62% сообщений помещаются в один сегмент.

Владелец бизнеса включил настоящую отправку, и первые SMS доставлены. Каждый пропущенный звонок теперь попадает в очередь в CRM, а клиенту уходит ответ по описанным выше ступеням — с настоящим статусом отгрузки, если заказ найден.

Когда клиенты звонят на самом деле

Система проработала достаточно долго, чтобы журнал уведомлений ответил на вопрос «в какие часы держать линию». Замер сделан по журналу на диске, без обращений к сторонним сервисам. Повторы убираются по идентификатору звонка, в расчёт идёт последнее событие с итоговым статусом, а часовой пояс клиента определяет библиотека разбора номеров. Всего 739 входящих за 90 дней, и пояс определён у каждого. Считать всё по одному времени было нельзя: восемь утра на восточном побережье — это пять утра на западном, и вывод исказился бы.

  • больше всего звонят в 11:00 по местному времени клиента; на промежуток 09:00–12:00 приходится 38% звонков
  • 82% звонков приходится на рабочее время, 5% — на вечер, 1% — на ночь. Представление «звонят вечером» не подтвердилось
  • в понедельник звонят почти вдвое чаще, чем в пятницу
  • 496 звонков из 739 пришли в объявленные часы работы, и 310 из них пропущены
  • всего за 90 дней пропущено 530 звонков, около 180 в месяц, — столько же показала и первая выгрузка из телефонии
  • в дневном отчёте 7 недозвонов из 9 пришлись на объявленные часы работы

Отсюда цифра, ради которой всё и считалось: если закрываться на два часа позже, в 17:00, линия приняла бы ещё 133 звонка за 90 дней, около 44 в месяц. Праздники в расчёте вычисляются по формуле, потому что таблицу с датами рано или поздно забудут продлить.

Здесь была и моя ошибка, и её пришлось исправлять отдельно. Первая версия определяла часовой пояс по собственной таблице телефонных кодов и сразу отнесла один из городов к чужому поясу. Ошибка в поясе незаметно сдвигает весь пик, и на графике её не видно. Поэтому таблицу я выбросил, а разбор номеров доверил Python-версии библиотеки libphonenumber. В отчёте отдельно помечено, что код города может не совпадать с местом, где человек живёт, и это не проверялось.

Карточка «Вчера» на странице звонков

Рядом с очередью появилась сводка за прошедшие сутки: сколько автоответов ушло, сколько не ушло и почему. Карточка собирает данные из всех источников, и на этом построена её проверка. На полных тестовых данных она должна показать «не ушло 2» и назвать две причины. Если какой-то источник подменить заглушкой, получится «не ушло 0», и тест сразу это заметит.

05 · Применимость

Где ещё работает тот же подход

За кейсом стоит типовая задача: «пришло обращение — находим клиента в учётной системе — отвечаем фактами о его заказе». Та же схема подходит везде, где обращения приходят в нерабочее время:

  • → Интернет-магазин с долгой доставкой — на вопрос «где мой заказ» отвечает статус из учётной системы
  • → Сервис и ремонт — на пропущенный звонок уходит SMS о стадии заказ-наряда и настоящем сроке готовности
  • → Клиники и салоны — на звонок в нерабочее время уходит подтверждение записи или ближайшее свободное время из расписания
  • → Бизнес с покупателями в другом часовом поясе — ночью на звонки отвечает система, которая знает факты о заказе
  • → Любой канал обращений — почта, мессенджер, форма на сайте: меняется только источник события; ступени ответа и защита при отправке остаются прежними
Что переиспользуется на следующих проектах
  • Приёмник уведомлений с проверкой подписи, отбором нужных событий и защитой от повторной обработки
  • Ступени ответа с прямым запретом называть просроченную дату; правило проверяется на выгрузке истории заказов до запуска
  • Защита при отправке: тихие часы с учётом часовых поясов клиентов, пауза между сообщениями на один номер, режим черновика и отдельный переключатель настоящей отправки
  • Страница-очередь с готовым текстом ответа, которая выдерживает частичный сбой: «не прочиталось» не показывается как «пусто»
Похожая задача?

Если у вас копятся пропущенные звонки, сначала посчитаем их, потом автоматизируем ответ

Я начинаю с выгрузки из вашей телефонии: сколько звонков пропущено на самом деле, о чём звонят и какие данные уже накопились, но не используются. Дальше делаю автоответ с фактами из вашей учётной системы. В этом кейсе от разбора данных до первых доставленных SMS прошло около двух недель.