Инструкция по интеграции Виртуальной АТС МегаФон и Elma365
Настройка интеграции ВАТС и Elma365 не является простой, но при достаточной концентрации вы сможете реализовать её всего за 10 минут. Не стоит пугаться кода, который вы увидите ниже – его нужно лишь скопировать и вставить в нужные поля и всё заработает. Вам не нужны даже минимальные знаний разработки для настройки интеграции.
1. В интерфейсе Elma365 перейдите в раздел Администрирование → Модули.
1. Справа вверху нажмите +Модуль
2. Вбейте любое имя виджета, например Виртуальная АТС МегаФон. Описание – любое.
3. Перейдите в управление модулем, вкладка Методы API.
4. Справа нажмите на вкладку Скрипты.
5. Вставьте данный код под скриншотом.
Код для вставки
// Загрузить значение поля "Ключ для авторизации в облачной АТС" из настроек модуля. function getApiKey() { const apiKey = Namespace.params.data.apiKey; if (!apiKey) { throw new Error('invalid API key'); } return apiKey; } // Загрузить значение поля "Адрес облачной АТС" из настроек модуля. function getEndpoint() { const endpoint = Namespace.params.data.endpoint; if (!endpoint) { throw new Error('invalid endpoint URL'); } return endpoint; } // Выполнить запрос к АТС async function fetchEndpoint(command: string, body: Object = {}): Promise<FetchResponse> { return fetch(getEndpoint(), { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ cmd: command, token: getApiKey(), ...body, }), }); } // Распарсить контент с типом "application/x-www-form-urlencoded" function parseUrlEncoded(body: string): Record<string, string> { const result: Record<string, string> = {}; for (const pair of body.split('&')) { const keyValue = pair.split('='); if (keyValue.length === 2) { const key = decodeURIComponent(keyValue[0]); const value = decodeURIComponent(keyValue[1]); result[key] = value; } } return result; } // Сообщение от АТС interface MegafonWebhookRequest { // Тип операции cmd: string; // Тип события, связанного со звонком type: string; // Номер телефона клиента (в формате E.164) phone: string; // Внутренний номер пользователя облачной АТС, если есть ext?: string; } // Данные, передающиеся с командой "history". interface MegafonWebhookHistoryCommand extends MegafonWebhookRequest { // Общая длительность звонка в секундах duration: string; // Уникальный id звонка callid: string; // Ссылка на запись звонка, если она включена в Облачной АТС link?: string; // Статус входящего или исходящего звонка status: string; } // Данные, которые сохраняются с каждым звонком. interface MegafonCallData { id: string; link: string; } // Сконвертировать статуса звонка АТС в статус звонка ELMA365 function toCallDisposition(status: string): VoipCallDisposition { switch (status) { case 'Success': return VoipCallDisposition.Answered; case 'Busy': return VoipCallDisposition.Busy; case 'Missed': return VoipCallDisposition.Cancel; case 'NotAvailable': return VoipCallDisposition.NoAnswer; case 'NotAllowed': default: return VoipCallDisposition.Unknown; } } // Проверить соединение к телефонии (вызывается по нажатию на кнопку "Проверить соединение" на странице модуля). async function VoipTestConnection(): Promise<VoipTestConnectionResult> { try { // Выполняем команду на получение списка пользователей телефонии (можно выбрать любую другую команду для тестирования). const response = await fetchEndpoint('accounts'); const message = await response.json(); if (response.status !== 200) { throw new Error(`${response.status} ${response.statusText}: ${message.error}`); } return { success: true }; } catch (e) { return { success: false, failReason: e.message, }; } } // Обработать запрос от провайдера IP-телефонии. async function VoipParseWebhookRequest(request: FetchRequest): Promise<VoipWebhookParseResult> { try { const headers = request.headers; const contentType = headers ? headers['Content-Type'] : undefined; if (contentType !== 'application/x-www-form-urlencoded') { throw new Error(`Expected Content-Type to be "application/x-www-form-urlencoded" (got: "${contentType}"`); } if (typeof request.body !== 'string') { throw new Error('Expected request body to be string') } let event: VoipWebhookRequest | undefined; let callRecord: VoipCallRecord | undefined; let response: HttpResponse | undefined; const data = <MegafonWebhookRequest><unknown> parseUrlEncoded(request.body); switch (data.cmd) { case 'event': { // Облачная АТС отправляет в вашу CRM уведомления о событиях входящих звонков пользователям: появлении, принятии или завершении звонка. Команда может быть использована для // отображения всплывающей карточки клиента в интерфейсе CRM. const dstPhone = data.ext ?? ''; switch (data.type) { case 'INCOMING': { // Пришел входящий звонок (в это время у менеджера должен начать звонить телефон). event = { event: VoipWebhookEvent.NotifyStart, direction: VoipCallDirection.In, dstPhone: dstPhone, srcPhone: data.phone, disposition: VoipCallDisposition.Unknown, }; } break; case 'ACCEPTED': { // Звонок успешно принят (менеджер снял трубку). В этот момент можно убрать всплывающую карточку контакта в CRM. event = { event: VoipWebhookEvent.NotifyAnswer, direction: VoipCallDirection.In, dstPhone: dstPhone, srcPhone: data.phone, disposition: VoipCallDisposition.Unknown, } } break; case 'COMPLETED': { // Звонок успешно завершен (менеджер или клиент положили трубку после разговора). event = { event: VoipWebhookEvent.NotifyEnd, direction: VoipCallDirection.In, dstPhone: dstPhone, srcPhone: data.phone, disposition: VoipCallDisposition.Unknown, } } break; case 'CANCELLED': { // Звонок сброшен (клиент не дождался пока менеджер снимет трубку. Либо, если это был звонок сразу на группу менеджеров, на звонок мог ответить кто-то еще). event = { event: VoipWebhookEvent.NotifyEnd, direction: VoipCallDirection.In, dstPhone: dstPhone, srcPhone: data.phone, disposition: VoipCallDisposition.Cancel, } } break; case 'OUTGOING': { // Менеджер совершает исходящий звонок (в это время облачная АТС пытается дозвониться до клиента). event = { event: VoipWebhookEvent.NotifyStart, direction: VoipCallDirection.Out, dstPhone: dstPhone, srcPhone: data.phone, disposition: VoipCallDisposition.Unknown, } } break; default: throw new Error(`Unknown event type "${data.type}"`) } } break; case 'history': { // После успешного звонка в CRM отправляется запрос с данными о звонке и ссылкой на запись разговора. // Команда может быть использована для сохранения в данных ваших клиентов истории и записей входящих и исходящих звонков. const cmd = <MegafonWebhookHistoryCommand> data; callRecord = { srcPhone: cmd.phone, dstPhone: cmd.ext ?? '', direction: cmd.type === 'out' ? VoipCallDirection.Out : VoipCallDirection.In, duration: parseInt(cmd.duration), // Данные из этого поля будут доступны в функции VoipGetCallLink call: <MegafonCallData>{ link: cmd.link, id: cmd.callid, }, disposition: toCallDisposition(cmd.status), } } break; } return { event: event, callRecord: callRecord, response: response, }; } catch (e) { return { response: new HttpResponse() .status(400) .content(JSON.stringify({ error: e.message ?? 'internal error', })) }; } } // Получить список пользователей от провайдера IP телефонии. // Это внутренние пользователи - внутренние номера/SIPID, нужно для сопоставления между пользователями IP-провайдера и пользователями ELMA365 async function VoipGetMembers(): Promise<VoipMember[]> { const response = await fetchEndpoint('accounts'); if (response.status !== 200) { throw new Error(`received error response ${response.status}: ${response.statusText}`); } interface MegafonVoipUser { name: string; ext: string; } const voipUsers = <MegafonVoipUser[]> (await response.json()); return voipUsers.map(user => ({ id: user.ext, label: user.name, })); } // Сгенерировать звонок async function VoipGenerateCall(srcPhone: string, dstPhone: string): Promise<void> { const response = await fetchEndpoint('makeCall', { phone: dstPhone, user: srcPhone, }); if (response.status !== 200) { throw new Error(`received error response ${response.status}: ${response.statusText}`); } } // Получить ссылку на запись разговора async function VoipGetCallLink(callData: MegafonCallData): Promise<string> { return callData.link; }
После этого слева вверху нажмите Сохранить, Опубликовать и вернитесь обратно.
6. Перейдите в Настройки и через кнопку +Добавить создайте два поля. Внутри создания используйте только поля Отображаемое имя и Имя свойства. Друг за другом создайте:
- Первый параметр. Отображаемое имя: Ключ для авторизации в Виртуальной АТС. Имя свойства: apiKey.
- Второй параметр. Отображаемое имя: Адрес Виртуальной АТС. В качестве названия свойства укажите endpoint.
7. Перейдите в раздел в карточку модуля, кликнув на его название слева наверху. Теперь нужно настроить связь между Elma365 и МегаФон.
Перейдите в вашу Виртуальную АТС МегаФон, раздел Настройки → Интеграция с CRM. В конце списка выберите карточку «Ваша CRM».
Теперь нужно вставить 2 параметра АТС в CRM и наоборот.
В интерфейсе Elma365 скопируйте адрес и ключ и вставьте их в ВАТС МегаФон. В ВАТС МегаФон скопируйте ключ АТС и вставьте в Elma365. В качестве адреса облачной АТС нужно вписать адрес вашей АТС (вида https://vats123456.megapbx.ru) и добавить /sys/crm_api.wcgp. Например, https://vats123456.megapbx.ru/sys/crm_api.wcgp.
Нажмите «Сохранить» в ВАТС МегаФон и в интерфейсе Elma365 (но именно верхнюю кнопку «Сохранить»).
8. Нажмите «Проверить подключение» и убедитесь, что всё работает.
9. Нажмите «Настроить» и сопоставьте пользователей CRM и АТС. Это необходимо, чтобы правильно вести статистику и показывать информацию правильным сотрудникам.
Поздравляем! Интеграция настроена.