Разработка виртуальной АТС, интегрированной с системой управления интернет-магазином
Шушлин В.В.., ООО «Инсейлс-Рус» V. 8ИшЫт@§таП. сот Рогозин О.В., МГТУ им. Н.Э. Баумана [email protected]
Аннотация
Работа посвящена разработке облачной АТС, интегрированной с платформой Интернет-торговли InSales и поддерживающей некоторый CRM-функционал. АТС разработана на базе продукта VoxImplant, с использованием HTTP-api и javascript-api. Сама АТС является Ruby on Rails-приложением.
Рассматриваются используемые паттерны проектирования, технические решения и подводные камни: внутреннее представление данных, генерация сценариев обработки звонков, обработка голосовой почты, проблемы работы с другими АТС, технические ограничения платформы VoxImplant. В открытом доступе находится реализация обертки HTTP-api VoxImplant для языка Ruby.
Ключевые слова: Виртуальная АТС(РВХ system), CRM-система, VoxImplant, SaaS.
1 Введение
Современная платформа для интернет магазинов уже не может быть просто CMS-системой, она должна быть интегрирована с множеством других продуктов: складские и бухгалтерские системы, сервисы рассылок СМС и email. Невозможно представить себе интернет магазин без номера телефона, на который могут обратиться клиенты.
И конечно, клиенту очень удобно, когда, только подняв трубку, оператор уже находит заказ клиента или создает новый для старого клиента с учетом персональных скидок и баллов. Кроме удобства интеграции АТС с системой управления интернет магазином стоит отметить, что она может быть хорошим источником дохода для своего владельца. Это две основные причины, по которым было решено создать собственную АТС.
Обработка вызовов была реализована на базе платформы VoxImplant, взаимодействие с которой производится по HTTP-api.
VoxImplant так же предоставляет возможность аренды номеров телефонов.
Обработка вызовов осуществляется при помощи javasoript-сценариев, которые генерируется в зависимости от заданного пользователем сценария, который может включать в себя проигрывание аудиозаписей и параллельный или последовательный обзвон операторов. Внутри этих сценариев существует возможность делать любые HTTP-запросы к внешним сервисам, которые позволяют например отслеживать звонки или даже принимать решения по дальнейшей их обработке. HTTP-api
При разработке первой задачей стало создание подаккаунтов в платформе VoxImplant. Создание аккаунтов происходит при помощи HTTP-api, документацию на которое можно найти в [HTTP-api VoxImplant]. На момент создания АТС не существовало оберток этого api для языка Ruby на котором писалась система, поэтому было принято решение такую обертку создать и выложить в публичный доступ [Документация на реализацию HTTP-api VoxImplant для языка Ruby].
1.1 Формат данных и обработка ошибок
Параметры запросов к api могут кодироваться почти в любом виде, который поддерживает протокол HTTP, а возврат всегда имеет формат JSON. HTTP-api VoxImplant достаточно удобно по формату возврата ошибок: при возникновении любых логических ошибок в ответе будет присутствовать result['error'] ['msg'] с текстом ошибки и result['error']['code'] с ее кодом. Для обертки этих ошибок был создан специальный класс исключений VoxImplantApi::Error<StandardErro r.
1.2 Авторизация
Доступ к данным конкретного аккаунта осуществляется при помощи параметров account_id и api_key. Исключение со-
ставляет метод AddAccount, в котором эти параметры имеют префикс parent_* .
С другими исключениями сталкиваться не приходилось.
1.3 Отправка запросов
Для отправки HTTP-запросов была выбрана библиотека RestClient.
Изначально это решение было продиктовано исключительно знакомством разработчика с этой библиотекой, однако позже это решение было оправдано функционалом библиотеки, а именно возможностью и удобством загрузки файлов в теле POST-запросов: для этого достаточно передать в хеш параметров запроса объект типа File.
Все запросы было решено реализовывать через метод POST, хотя сама платформа VoxImplant не делает различия по методу которым осуществляется запрос. Это решение так же показало себя хорошо, когда появилась необходимость загрузки файлов.
Обертка методов. Описанная выше отправка запросов была реализована методом perform_request, принимающим в качестве параметров название метода api и хеш параметров. Однако, достаточно неудобно пользоваться этим методом и передавать имя метода при каждом запросе.
Желательно, чтобы класс
VoxImplantApi: :Client, обладал следующими свойствами.
• Для каждого метода VoxImplant HTTP-api был бы удобен метод класса с соответствующим названием.
• Многие методы api возвращают коллекции объектов разбитые на страницы. Однако чаще всего нужна работа просто со всей коллекцией, и возникает желание абстрагироваться от этого разбиения и получить метод, который бы просто возвращал итератор для этой коллекции. Реализовать эти условия можно при помощи реализации метода method_missing(name, *args) [Ruby method_missing.], и использования метода enum_for [Ruby enum_for], который оборачивает в стандартный итератор метод, поддерживающий передачу блока.
Для обертки методов было принято соглашение, что слова в методах приводятся к нижнему регистру и разделяются символом _, например метод GetTransactionHistory оборачивается методом
get_transaction_history. К названиям методов-итераторов добавляется префикс each_*.
Ознакомиться с реализацией, примерами использования и документацией к полученной библиотеке можно на странице [Документация на реализацию HTTP-api VoxImplant для языка Ruby].
2 Процесс регистрации аккаунта
Взаимодействие между сущностями в VoxImplant хорошо объяснено в [Аиларов А. IP АТС в облаке своими руками за 10 минут].Перед дальнейшим чтением рекомендуем ознакомиться с данной публикацией.
При создании аккаунта задаются логин и пароль, которые надежнее всего генерировать случайным образом, однако в логин для удобства можно добавить информацию позволяющую быстро идентифицировать акка-унт.
Сразу после создания аккаунта имеет смысл создание «приложения». «Приложением» в данном контексте называется сущность, внутри которой затем будет происходить «маршрутизация» вызовов - их распределение по исполняющим их скриптам.
3 Пополнение баланса
Следующим шагом после регистрации для пользователя будет пополнение баланса, в данном случае оно производится стандартным методом оплаты InSales [InSales API -Счета приложения].
После получения подтверждения оплаты средства переводятся со счета родительского аккаунта на счет дочернего соответствующим методом HTTP-api -
TransferMoneyToChildAccount. У этого метода есть ряд необязательных но очень полезных параметров, которые помогают избежать проблем в будущем. Первый из них -parent_transact ion_descript ion, он будет выводиться в описании транзакции в административной панели родительского ак-каунта, весьма уместно дополнить стандартное описание идентификаторами аккаунтов в вашей системе и любой информацией, которая позволит в будущем идентифицировать платеж. Следующие два важных параметра: payment_reference и
check_duplicate_reference_from. Первый из которых - произвольный идентификатор транзакции в вашей системе, а вто-
рой - дата с который уникальность этого идентификатора проверяется. В случае совпадения payment_reference при создании транзакции ответ api будет таким же, как если бы транзакция прошла успешно, но дубликат транзакции не создастся. Эти параметры почти никак не описываются в документации, и в разрабатываемом проекте были применены далеко не сразу, по этой причине мы приводим их описание здесь.
4 Представление данных
После того, как пользователь пополнил баланс, ему необходимо арендовать номер телефона. Это достаточно простая операция, осуществляемая так же через HTTP-api, поэтому мы не будем на ней подробно останавливаться. Стоит однако упомянуть о необходимости «верификации» аккаунта после покупки первого номера или до нее. Под верификацией в данном случае понимается предоставление документов подтверждающих личность или заполнение реквизитов юридического лица в специальном документе. Документы необходимо направить в VoxImplant при помощи HTTP-api, а именно метода SetAccountDocument.
После покупки номера, пользователю предоставляется возможность настройки обработки вызовов на этот номер. Можно предоставлять разные по уровню гибкости интерфейсы настройки, однако было принято решение о предоставлении следующих функции:
• проигрывание аудиозаписей;
• проброс звонка на конкретный номер телефона;
• параллельный и последовательный обзвон группы номеров, и соединение с первым ответившим.
Обработка правил прекращается после успешного завершения разговора, что позволяет строить сценарии следующего вида.
• Проиграть аудиозапись "Здравствуйте, вы позвонили в интернет магазин, дождитесь ответа оператора".
• Звонок группе менеджеров No 1.
• Звонок управляющему магазина.
• Проиграть аудиозапись «Извините, сейчас все менеджеры заняты».
Кроме того, необходима возможность принимать решения на основе временных диапазонов и запускать различные сценарии обработки вызовов в различное время.
Для реализации этих задач были созданы следующие сущности. Связи между ними так же показаны на рис. 1.
• Schema - схема привязанная к телефону, объединяющая несколько TimeRule.
• TimeRule - правило срабатывания сценария по времени, принадлежащее схеме и указывающее конкретный сценарии" обработки звонка. Данные правила сортируются, первое подходящее по текущему времени запускает сценарии, остальные игнорируются.
• Scenario - сценарии" обработки входящего звонка, включает в себя несколько отсортированных ActionRule.
• ActionRule - действие осуществляемое в определенный момент обработки скрипта. Возможные варианты: звонок пользователю, звонок на произвольный номер, звонок группе, проигрывание аудиозаписи.
• User - пользователь системы, с привязанным личным номером и сотрудником InSales, последняя связка будет необходима в дальнейшем для определения с каким именно клиентом сейчас соединен текущий пользователь системы.
• Group - группа пользователей определяющая порядок обзвона.
• AudioRecord - аудиозапись.
5 Генерация сценариев обработки вызовов
На итоговый скрипт обработки вызовов влияют сразу несколько сущностей, а именно: Schema, TimeRule, Scenario, ActionRule, AudioRecord, User, Group. В итоговой сценарии" обработки вызова VoxImplant отображается Schema. Однако все остальные сущности могут менять сразу несколько схем, поэтому может понадобиться перегенерация сразу нескольких сценариев. Для реализации такой перегенерации был выбран паттерн "Обзер-вер", а именно его реализация «rails-observers» [Rails::Observers.].
Изначальный вариант интерфейса настройки предполагал использование вложенных атрибутов «nested-attributes», что осложняло использование обзерверов, т.к. перегенерация скриптов обработки запускалась несколько раз на одно действие пользователя при сохранении. Эта проблема была решена следующим образом.
а) Обзервер делает фиктивное обновление модели Schema, обновляя атрибут updated_at.
б) Модель Schema, устанавливает коллбек after_commit, который вызывается единожды за одну транзакцию.
Так как сам обзервер вызывает обновление Schema в уже идущей транзакции, обновление происходит единственный раз.
Следующей важной проблемой стало время генерации сценариев обработки, которое может достигать нескольких секунд, и тем самым создавая у пользователей негативный опыт использования. Для решения этой проблемы было решено производить перегенерацию сценариев отложено. Реализация отложенной обработки была реализована при помощи библиотеки DelayedJob [Delayed::Job].
Как можно увидеть из диаграммы (рис. 2), модель ActionRule имеет ссылки сразу на несколько сущностей: группу, пользователя и аудиозапись, а так же поле phone_number, используемое для представления переадреса-
ции вызова на конкретный номер. Так как сами действия могут быть разными, было решено использовать паттерн Single-Table-Inheritance [Single Table Inheritance with Rails 4], суть которого заключается в следующем. В таблице создается поле type, которое соответствует имени класса, экземпляр которого необходимо создать при инициализации объекта. Паттерн поддерживается Rails по умолчанию, и не требует никаких дополнительных усилии программиста, за исключением создания текстового поля type.
Сама генерация сценария обработки начинается с модели Schema, которая затем вызывает генерацию прикрепленных к ней TimeRule, те в свою очередь вызывают Scenario. Scenario вызывает последовательную генерацию скриптов обработки для каждого ActionRule.
Phones
id
number
schema. .id
id
schemajd
position
time_from
time_to
days_mask
scenariojd
Schemas
id
scenariojd
User type
id —> audiorecordjd
name
........H userjd
insalesjd groupjd
phone_number phone number
GroupUsers Audio Record
id id
groupjd я— file
userjd
Рис. 9. Сущности отвечающие за обработку вызовов
6 Создание и обновление правил VoxImplant
Важным действием пользователя является привязка номера к определенной схеме обработки вызова. По этому событию необходимо обновлять список правил в VoxImplant. Эти правила определяют по но- меру, на который происходит вызов, на какой сценарии" направляется обработка вызова при помощи регулярных выражении". В данном случае регулярные выражения являются просто номерами, которые подключил пользователь. Эти правила ссылаются на сценарии, про генерацию которых было рассказано предыдущем параграфе. Важным моментом является соглашение об именовании этих правил. Достаточно удобно оказалось именовать их тоже по номеру, для которого они созданы. Самым удобным способом обновления оказался вариант, когда перед обновлением, проверяется существование правила, если правило существует - оно удаляется. Далее в зависимости от того, подключен номер к какой-то схеме или нет, это правило создается или нет соответственно. Такой способ обновления теоретически может порождать ошибки синхронизации, т.к. состоит из нескольких этапов, однако на практике таких ситуации" не возникало.
7 Синхронизация телефонов
В целях ускорения работы приложения было решено хранить полный список арендованных телефонов в базе данных приложения со всеми данными, даже если они дублировали данные, хранящиеся в VoxImplant. Еще одним преимуществом данного подхода является возможность использовать методы стандартной библиотеки ORM. Реализовано это было следующим образом, в поля данных ак-каунта было добавлено поле, содержащее время последнего обновления, а метод возвращающий список номеров был переопределен таким образом, что проверял это время, и при необходимости выполнял синхронизацию списка номеров, после чего вызывал исходный метод. В некоторых случаях время последнего обновления сбрасывается, например в момент покупки номера или обновления его данных. Данная синхронизация односторонняя - она лишь обеспечивает соответствия данных в локальной базе данных данным в
VoxImplant. Синхронизация в другом направлении реализована с помощью колбеков
8 Обработки входящих вызовов.
Хороший и развернутый пример организации обработки вызовов приведен в [Аиларов А. IP АТС в облаке своими руками за 10 минут], куда мы ссылались ранее. Однако этот пример включает в себя только параллельный обзвон операторов.
Для реализации описанной ваше системы правил была выбрана описанная далее структура, в некотором роде повторяющая диаграмму на рис.1, но уже хранящаяся не в базе данных, а непосредственно записанная в сценарии" обработки вызова.
8.1 Schema
Объект класса Schema отображается в простои" массив объектов TimeRule, каждый их которых содержит два метода:
• condition() - метод определяющий, подпадает ли текущее время под указанный пользователем интервал, в котором срабатывает текущий сценарии";
• run() - метод запускающий этот сценарии".
При обработке вызова, каждый объект массива проверяется вы- зовом метода condition(), и для первого вернувшего true вызывается метод run().
В реализацию метода run() вставляется код сгенерированный мо-делью Scénario.
8.2 CallProcessor
CallProcessor - главный объект, который руководит обработкой вызовов.
Этот объект содержит следующие поля и методы(неполный спи-сок):
• action_rules - массив действий, выполняемых этим сценарием;
• current_rule - индекс текущего действия;
• startNextRule() - метод, запускающий выполнение следующего действия;
• call - входящий звонок;
• endCall() - метод завершающий обработку вызова;
• answer() - метод отвечающий за поднятие трубки;
• record() - метод, включающий запись звонка, если нужно;
• notifyAboutCall() - метод уведомляющий приложение посредством HTTP-запроса о
соединении входящего звонка с каким-то пользователем; • reportCallEnd() - метод уведомляющий о завершении звонка.
8.3 Начало обработки вызова
Еще перед обработкой вызова необходимо произвести несколько действий, которые являются очень неочевидными.
Во время дозвона принято транслировать звонящему "гудки". Для этого необходимо послать событие startEarlyMedia, а затем включить проигрывание гудков или аудиозаписи. Изначально приветственные аудиозаписи было решено проигрывать без поднятия трубки, то есть через указанное выше событие. Однако после некоторых тестов была выявлена следующая ошибка: часть аудиозаписи "проглатывалась" при звонках. Как выяснилось, это было связанно с тем, что событие startEarlyMedia запаздывало и приходило позже уже начавшей проигрываться аудиозаписи. Проблема была решена вставлением небольшой задержки(1 секунда) перед проигрыванием аудиозаписи, если так стоит первой в списке правил. По рекомендации VoxImplant событие startEarlyMedia отправляется дважды, так оно иногда может не доходить, хотя на практике таких случаев замечено не было.
Неочевидным сразу моментом оказалось и ограничение на время, которое звонок может находиться в состоянии дозвона, равное 30 секундам. Для правильной работы необходимо через 29 секунд поднять трубку и можно продолжить дозвон в нормальном режиме.
Некоторые внешние АТС могут так же не пропускать early media и заменять его гудками, что выяснилось уже во время работы, поэтому при проигрывании аудиозаписи было решено сразу поднимать трубку.
Все описанные проблемы были связаны с желанием избавить клиентов магазина от необходимости "платить за дозвон", и всех их можно избежать сразу поднимая трубку, что при изначальной разработке может являться правильным решением, но в данном случае был выбран другой путь.
8.4 Голосовая почта
Достаточно неприятной проблемой оказалась работа с «автоответчиком» или голосовой почтой. Как обнаруживать голосовую почту достаточно подробно описано в [Аиларов А., Определение голосовой почты при звон-
ке]. Как правило о том, что звонок попал на голосовую почту соответствующие событие сообщают нам уже после того, как мы соединили пользователя а оператором. Из-за этого приходится выполнять аккуратный откат произведенных действий и продолжать об-звон других операторов.
8.5 Функция «Горячий звонок»
До сих пор мы описывали базовый функционал любой виртуальной АТС. Теперь опишем возможности интеграции с интернет-магазином, о которых мы говорили еще во введении.
Идея состоит в следующем: когда опера-тор(менеджер магазина) принимает входящий звонок, он может нажать на кнопку «горячий звонок» прямо в бек-офисе магазина, и в зависимости от того звонит человек в первый раз, или он уже делал заказы он попадает в различные разделы бек-офиса магазина.
• Если у клиента 1 заказ, то по клику откроется этот заказ.
• Если у клиента несколько заказов, то откроется список.
• Если звонит новый клиент, то откроется создание заказа для нового клиента.
• Если клиент есть, но он без заказов, то откроется создание заказа для этого клиента. Для реализации данного функционала была
использована возможность делать исходящие НТТР-запросы прямо из скрипта обработки вызова.
Последовательность событии" при этом следующая, 8едиепее-диаграмму можно найти на рис. 2.
• Клиент набирает номер магазина и начинает слушать «гудки».
• Сценарии" в Уох1шр1ай обзванивает операторов в соответствии с заданным сценарием.
• Один из операторов поднимает трубку.
• Сценарии" уведомляет бек-офис АТС о том, с каким оператором был соединен звонок.
• Оператор нажимает на кнопку «Горячий звонок» в бек-офисе магазина.
• Оператор перенаправляется в бек-офис АТС.
• АТС запускает поиск заказов и клиентов по номеру, с которого осуществляется звонок.
• Оператора перенаправляется в раздел бек-офиса интернет магазина, соответствующий найденным сущностям.
Для защиты от подделки запросов о звон- ключом, который известен сценарию испол-ках, эти запросы подписываются случайным нения.
Рис. 10 Диаграмма последовательности деиствий при работе с функцией «горячий звонок»
9 Заключение
В результате работы была создана виртуальная АТС, поддерживающая некоторый CRM-функционал. Мы описали некоторые подходы и сложности, которые могут помочь другим компаниям разрабатывать подобные АТС и интегрировать их со своими основными продуктами.
Разработанная АТС сейчас используется в нескольких десятках магазинов. Можно выделить следующие направления развития продукта:
• поддержка sip-телефонов и «софтфонов»;
• поддержка исходящих вызовов;
• реализация функционала "обратного вызова" на сайте магазина;
более тесная интеграция с бек-офисом магазина. Благодарности
Список литературы
Документация на HTTP-api VoxImplant. Режим доступа
https://voximplant.com/docs/references/httpapi/ (дата обращения 20.09.2015).
Шушлин В.В. Документация на реализацию HTTP-api VoxImplant для языка Ruby. Режим доступа https://github.com/ in-
sales/voximplant_api (дата обращения 20.09.2015).
Ruby method_missing. Режим доступа http://ruby-doc. org/core-2. 2. 0/B asicObj ect.html#method-i-method_missing (дата обращения 20.09.2015).
Ruby enum_for. Режим доступа http://ruby-doc. org/core-2. 2.3/Obj ect.html#method-i-enum_for (дата обращения 20.09.2015).
Аиларов А. IP АТС в облаке своими руками за 10 минут, Режим доступа
http://habrahabr.ru/post/212713/ (дата обращения 20.09.2015).
InSales API - Счета приложения. Режим доступа https://wiki.insales.ru/wiki/InSales_API_Счета_ приложения (дата обращения 20.09.2015).
Аиларов А., Определение голосовой почты при звонке, Режим доступа
http://habrahabr.ru/company/Voximplant/blog/2 48735/ (дата обращения 20.09.2015).
Delayed::Job. Режим доступа
https://github.com/collectiveidea/delayed_job (дата обращения 20.09.2015).
Rails::Observers. Режим доступа
https://github.com/rails/rails-observers (дата обращения 20.09.2015).
Single Table Inheritance with Rails 4. Режим доступа http://samurails.com/tutorial/single-table-inheritance-with-rails-4-part-1/ (дата обращения 20.09.2015).