Привет, Хабр. На связи Екатерина Саяпина, Product Owner платформы МТС Exolve. В этом материале я расскажу, как автоматически добавить в CRM оптимальное время для общения с клиентами. В рамках этого кейса я покажу настройку нашей платформы и получение информации из нее в Битрикс24.

Подготовка МТС Exolve

Подключение

Этот процесс описан в одном из наших предыдущих материалов: нужно завести аккаунт, подписать договор, создать приложение и получить API-ключ. В списке приложений выберите нужное:

Затем перейдите в «Настройки» на панели слева:

Пролистайте до конца вниз и нажмите на кнопку «Подключить» в блоке «Умная проверка номеров»:

Так как это платная услуга, то нужно будет ознакомиться с условиями и тарифами и подтвердить ваше согласие.

Теперь нам доступна работа с Number Lookup API — набором методов, с помощью которых можно получить оптимальное время для звонка. Мы используем GetBestCallTime. Он покажет, когда лучше всего позвонить абоненту. Надо учитывать, что этот метод работает только с клиентами МТС.

Применение

Используем POST-запрос https://api.exolve.ru/hlr/v1/GetBestCallTime. Для авторизации берем Bearer-токен из нашего приложения МТС Exolve, а в тело запроса в JSON-формате добавляем проверяемый телефон. Отправку запроса и преобразование ответа к нужному виду делаем так:

def get_time(time_string: str):
   hstr = time_string.split(':')[0]
   return time(hour=int(hstr))


def get_time_range(recepient: str):
   payload = {'number': recipient}
   r = requests.post(r'https://api.exolve.ru/hlr/v1/GetBestCallTime', headers={'Authorization': 'Bearer '+sms_api_key}, data=json.dumps(payload))
   print(r.text)
   if r.status_code == 200:
       ans = json.loads(r.text)
       text = ans['result'].split(',')
       since, till = [get_time(t) for t in text]
       ans.update({'since': since, 'till': till})
       return ans
   return None

Функция get_time_range отправляет запрос и проверяет успешность передачи информации. В последнем случае при помощи get_time получаем время в часах, соответствующее началу и концу периода, наиболее благоприятного для звонка этому абоненту.

Готовим приложение

Импортируем все необходимые библиотеки, в том числе для работы с CRM Битрикс24, потому что именно в ней будет все храниться:

from flask import Flask
import requests
import json
from dataclasses import dataclass
import pandas as pd
from fast_bitrix24 import Bitrix
from datetime import time
from datetime import datetime as dt

База данных

Для синхронизации используем функции получения записей о клиентах и номерах телефонов по их ID. Работу с ними мы описывали в одном из прошлых материалов. Номера, а вместе с ними начало и конец подходящих для звонка интервалов храним в pandas DataFrame. Эта библиотека используется для автоматизации обработки данных, а объекты класса DataFrame можно представить как таблицу с индексацией по строкам и столбцам.

Создадим класс для управления базой данных и инициализируем ее поля:

@dataclass
class DB:
   UTC_plus = 3
   phones_and_clients_table = pd.DataFrame(columns=['phone', 'client_id', 'call_since', 'call_till']).set_index(['phone'])

Они содержат сдвиг в часах вашей временной зоны относительно UTC+0, а также pandas DataFrame с информацией о номерах телефонов.

Синхронизация с CRM

Сначала функция add_client_phones получает номера клиента из Битрикс24 и передает их в локальную базу. Затем push_phone нормализует номер телефона, проверяет его наличие в базе и при необходимости добавляет. У каждого номера должен быть свой уникальный client_id. Если его нет, то он присваивается автоматически.

   def add_client_phones(self, client_full_record):
       contact_phones = [ph.get('VALUE', '') for ph in client_full_record.get('PHONE', [{}])]
       if len(contact_phones) == 0:
           return 0
       for c in contact_phones:
           self.push_phone(c, client_ID=[client_full_record.get('ID')])
       return 1

def push_phone(self, phone, client_ID=None, since=None, till=None, replace=False):
   phone_str = str(phone).replace(' ', '').replace('(', '').replace(')', '').replace('-', '').replace('+', '')
   if not replace and phone_str in self.phones_and_clients_table.index:
       return
   if client_ID is None:
       client_ID = self.phones_and_clients_table.shape[0]
   self.phones_and_clients_table.loc[phone_str, 'client_id'] = client_ID
   self.phones_and_clients_table.loc[phone_str, 'call_since'] = since + self.UTC_plus
   self.phones_and_clients_table.loc[phone_str, 'call_till'] = till + self.UTC_plus

Получение списка телефонов

При синхронизации с Битрикс24 для новых клиентов устанавливаются значения границ интервалов доступности — когда стоит звонить. Для уже внесенных в базу номеров данные не обновляются повторно — это позволяет избежать лишних запросов к API, если не установлен флаг update=True.

def add_times(self, phone, update=False):
   if phone not in self.phones_and_clients_table.index: return
   if self.phones_and_clients_table.loc[phone, 'call_since'] and self.phones_and_clients_table.loc[phone, 'call_till'] and not update:
       return
   ans = get_time_range(phone)
   if ans is None: return
   self.phones_and_clients_table.loc[phone, 'call_since'] = ans['since']
   self.phones_and_clients_table.loc[phone, 'call_till'] = ans['till']

Функция add_times_for_db запускает обновление интервалов для всех телефонов в базе:

def add_times_for_db(self, update=False):
   for ph in self.phones_and_clients_table.index:
       self.add_times(ph, update)

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

def select_for_call(self, hour=None):
   if hour is None: hour = dt.now().hour
   since_indexes = self.phones_and_clients_table['call_since'] <= hour + 1
   till_indexes = self.phones_and_clients_table['call_till'] >= hour
   indexes = since_indexes * till_indexes
   temp_table = self.phones_and_clients_table.loc[indexes].sort_values('call_till')
   return temp_table.index.to_list()

По умолчанию используется текущий час. Выбираются номера:

  • чей интервал еще не истек (call_till ≥ hour);

  • интервал уже идет или начнется в течение следующего часа (call_since ≤ hour + 1).

Затем список сортируется по времени окончания интервала (call_till) — в приоритете те, у кого он скоро закончится. Номера, которые пока недоступны, в выборку не попадают.

Для экспорта базы телефонов в Excel используем эту функцию:

def dump(self):
   self.phones_and_clients_table.to_excel('Dump.xlsx')

Как работает серверная часть

Создаем сервер на фреймворке Flask. Нам достаточно одной функции, возвращающей JSON-объект. Flask автоматически преобразует словарь в объект, который можно передать по сети.

@app.route('/get_phones', methods=['GET'])
def get_phones():
   ans = crm_pipeline()
   return json.dumps(ans)


def main():
   app.run(host='0.0.0.0', port=5000)

В результате приложение возвращает JSON, содержащий только номера, упорядоченные в рекомендуемом порядке:

[
 "79101????24",
 "79101????09",
 "79101????32",
]

Можно получить сведения о доступности клиентов, выгрузив JSON для последующей программной обработки. Для этого отправляем запрос по адресу https://<your_server>/get_phones_list и вызываем функцию:

@app.route('/get_phones_list', methods=['GET'])
def get_phones_list():
   return db.phone_table.to_json(None, 'index')

Она возвращает JSON вида:

{
 "79101????00": {
   "client_id": 0,
   "call_since": 16,
   "call_till": 17
 },
 "79101????01": {
   "client_id": 1,
   "call_since": 8,
   "call_till": 9
 },
 "79101????03": {
   "client_id": 2,
   "call_since": 11,
   "call_till": 12
 }
}

В таком виде результат подходит для обработки другими приложениями. Его можно сделать удобным и для чтения человеком. Используем функции экспорта базы данных в Excel и передачи файла по HTTP-запросу:

@app.route('/get_phones_table', methods=['GET'])
def get_phones_table():
   db.dump()
   try:
       return send_file('Dump.xlsx', mimetype='application/octet-stream', as_attachment=True)
   except Exception as e:
       return str(e), 405

Так будет выглядеть полученная таблица:

Отметим, что читать и записывать Excel-файлы умеет много кто: pandas и polars на Python, MatLab, readxl на R
Отметим, что читать и записывать Excel-файлы умеет много кто: pandas и polars на Python, MatLab, readxl на R

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

def crm_pipeline():
   clients = get_contacts()
   for c in clients:
       db.add_client_phones(c)
   db.add_times_for_db()
   ans = db.select_for_call()
   return ans

Это решение можно развернуть в докер-контейнере, в том числе на бесплатном хостинге, как мы описывали в другом нашем материале. Информация от МТС Exolve будет обновляться только для номеров, которые добавляются в базу при синхронизации с CRM, выполняемой с каждым запросом.


Мы собрали рабочее решение для автоматического определения лучшего времени звонка и интегрировали его с CRM Битрикс24. В основе — метод одиночной проверки GetBestCallTime, который показывает, когда абонент чаще всего на связи. Для работы с большими списками номеров есть отдельный метод GenerateBestCallTimeReport.

Комментарии (1)


  1. 0xC0CAC01A
    14.07.2025 17:14

    Дорогой наш МТС, как достучаться до вашего саппорта, причём до тех, которые реально способны решать проблемы, а не повторять бесполезности вслед за роботом?