Pull to refresh
35.17
Exolve
Конструктор омниканальных диалогов для бизнеса

Приложение «7-дневный экочеллендж по SMS» на Python

Level of difficultyMedium
Reading time8 min
Views1K

Привет, Хабр! Сегодня немного поиграем. Точнее, рассмотрим, как организовать и автоматизировать двустороннее взаимодействие с базой контактов через SMS API. Суть игры — выполнять список экозаданий и набирать баллы.

Челлендж стартует с задания: проведи Час Земли без электричества и гаджетов 🌍. Если участник принимает вызов, то отправляет ответное SMS с коротким текстом «Час Земли!». Каждый день в течение недели человек получает новое задание — сообщение с предложением выполнить его и отправить обратное SMS с результатами.

Мы придумали такие задания:

  • Откажись от новых пластиковых пакетов на 7 дней 🛍️. Возьми тканевую сумку или свой пакет — из пакета с пакетами 🙂

  • При покупке кофе проси наливать его в свою кружку в течение 6 дней 🥤

  • Выключай свет, когда он не нужен, в течение 5 дней 💡

  • Сортируй мусор 4 дня — разделяй пластик, бумагу и стекло.

  • Сдай батарейки в переработку ♻️

  • Убери мусор в парке или у дома 🚮

  • Отдай старую одежду в переработку через пункты приёма.

По итогу выполнения заданий подсчитываются очки игроков — как за выполнение пунктов, так и за численные показатели. Например, количество сданных батареек, пакетов отсортированного или убранного мусора. Дополнительные баллы можно начислять за приобретение экомерча, экотоваров.

Похожие соревнования легко устроить между клиентами магазина, где продаются тематические товары. Но интересно проводить такие челленджи и для персонала, чтобы повысить экологическую грамотность.

Создаём приложение

Приложение состоит из сервера на Flask, планировщика задач и функций общения с «Битрикс» и МТС Exolve. Сервер получает вебхуки для общения с пользователями. 

Для начала создадим константы и инициализируем приложение:

from flask import Flask, request
import requests
import json
import os
from dataclasses import dataclass
import pandas as pd
import sched, time
from fast_bitrix24 import Bitrix




sms_api_key = os.environ['MTS_API_KEY']
mng_phone = os.environ['MANAGER_PHONE']


STAGE_DICT = {'День Земли!': 1, 'Без пакета': 2, 'Без стаканов': 3, 'Экономим электричество!': 4, 'Отсортировали мусор!': 5,
             'Сдали батарейки!': 6, 'Теперь чисто!': 7, 'Сдали одежду!': 8}
BYE_TEXT = 'Также вы можете приобрести наши эко-товары и поднять свой рейтинг в зависимости от суммы покупок!'
SEND_STRINGS = {1: 'Приветствуем Вас! Вы готовы поддержать Час Земли и отказаться от электричества и телефона всего на час? Если да, то приглашаем вас на игру! Пришлите в ответ "День Земли!" на это сообщение и присоединяйтесь! Пройдите несколько заданий, пришлите нам результаты, зарабатывайте очки в нашей игре!',
               2: 'Откажитесь от покупки пластиковых пакетов - берите с собой сумку или свой пакет!',
               3: 'Попробуйте хотя бы следующих 6 дней отказаться от одноразовых стаканов!',
               4: 'Попробуйте хотя бы следующих 5 дней выключать свет за собой!',
               5: 'Хотя бы следующих 4 дня предлагаем вам сортировать мусор!',
               6: 'Предлагаем вам сдать в переработку батарейки.',
               7: 'Позаботьтесь о себе и природе - проведите дома уборку!',
               8: 'Старую одежду вы можете сдать в специальные пункты приёма.'}
WHAT_TO_ANSWER = 'В качестве подтверждения выполнения задания пришлите нам: '




app = Flask(__name__)
db = DB()
s = sched.scheduler(time.time, time.sleep)

Инициализация заключается в создании объектов веб-сервера, объекта базы данных, объекта оболочки «Битрикс», объекта планировщика. При использовании сервера на фреймворке Flask код получается простым и читаемым.

С той же целью используют и библиотеку fast_bitrix24. Достаточно задать адрес эндпоинта входящих вебхуков вашей платформы «Битрикс». Для запуска задач в определённое время выбираем объект класса scheduler. Он инициализируется двумя функциями, отвечающими за измерение текущего времени и осуществление задержки соответственно.

База данных будет построена на основе фреймов pandas. Это удобная библиотека, часто используемая в Data Science, для работы с табличными данными. У фрейма есть индекс, который можно рассматривать как ключ. Любой столбец можно сделать ключом.

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

@dataclass
class DB:
   task_table = pd.DataFrame(columns=['ID', 'S1', 'S2', 'S3', 'S4', 'S5', 'S6', 'S7', 'S8', 'sale', 'score']).set_index(['ID'])
   phone_table = pd.DataFrame(columns=['phone', 'client_id']).set_index(['phone'])


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


   def add_task_item(self, client_full_record):
       if not self.add_client_phones(client_full_record):
           return
       self.task_table.loc[client_full_record.get('ID')] = 0


   def get_phone(self, client_ID):
       phone_indexes = self.phone_table['client_id'] == client_ID
       return self.phone_table.loc[phone_indexes].index[0].strip('+')

Главная функция запускает сервер и создаёт задачи. Они запускаются с задержкой, задаваемой константами START_DELAY и DELAY. Первая определяет задержку перед началом игры, вторая — между заданиями.

Для тестовых целей задержка выбрана минимальной — старт через 2 секунды, отправка SMS по каждой задаче — через 20 секунд. Согласно документации планировщик нельзя использовать для выполнения параллельных задач.

START_DELAY = 2  # For test
DELAY = 20




def main():
   # Schedule tasks
   s.enter(START_DELAY, 1, start_eco_day, argument=())
   for ai in range(8):
       s.enter(START_DELAY+DELAY*(ai+1), 1, send_notif, argument=(ai,))
   s.enter(DELAY*10, 1, finish_eco_day, argument=())
   app.run(host='0.0.0.0', port=5000)




if __name__ == '__main__':
	main()

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

Функции, которые запускаются планировщиком, нужны для уведомления о начале игры, рассылки заданий и завершения игры. Функция отправки уведомлений высылает текст задания и предложение купить экотовары в первый раз всем клиентам, а далее — только тем, кто откликнулся на приглашение.

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

def send_notif(stage: int):
   index_to_send = db.task_table.index
   if stage>1:
       idxs = db.task_table['S1'] == 1
       index_to_send = db.task_table.index[idxs]
   message = SEND_STRINGS[stage] + f'\n' + WHAT_TO_ANSWER + list(STAGE_DICT)[stage-1] + f'\n' + BYE_TEXT
   for ai in index_to_send:
       recipient = db.get_phone(ai)
       send_SMS(recipient, message)






def start_eco_day():
   db.__init__()
   contacts = get_contacts()
   for c in contacts:
       db.add_client_phones(c)
       db.add_task_item(c)




def finish_eco_day():
   dls = get_deals()
   for ai in db.task_table.index:
       if db.task_table.loc[ai, 'S1'] == 0: continue
       current_deals = search_deals_by_contact(dls, ai)
       client_sum = get_deals_sum(current_deals)
       db.task_table.loc[ai, 'sale'] = client_sum
       db.task_table.loc[ai, 'score'] = db.task_table.loc[ai, ['S1', 'S2', 'S3', 'S4', 'S5', 'S6', 'S7', 'S8']].sum()*100+db.task_table.loc[ai, 'sale']
   winner = db.task_table['score'].values.argmax()
   dest = db.get_phone(db.task_table.index[winner])
   send_SMS(dest, 'Вы победили! Ваш рейтинг: '+str(db.task_table.iloc[winner, -1]))

Чтобы отправить клиенту сообщение с предложением пройти игру, заданиями и финальным сообщением, используем SMS API:

def send_SMS(recepient: str, send_str: str):
   payload = {'number': crm_phone, 'destination': recepient, 'text': send_str}
   r = requests.post(r'https://api.exolve.ru/messaging/v1/SendSMS', headers={'Authorization': 'Bearer '+sms_api_key}, data=json.dumps(payload))
   print(r.text)
   return r.text, r.status_code

При создании запроса указываем обязательные параметры: номер телефона отправителя, получателя и текст сообщения. Номер отправителя — один из контактов, привязанных в вашем приложении МТС Exolve.

Параметры передаются в формате JSON. В заголовке запроса обязательно должно быть поле 'Authorization' с ключевым словом 'Bearer' для определения типа авторизации, за ним через пробел идёт сам ключ.

Для получения информации об отправленных клиентами SMS нам нужен веб-сервер, принимающий вебхуки от платформы.

@app.route('/receive_data', methods=['POST'])
def receive_data():
   print('Receiving...')
   SMS_data = request.get_json()
   print(SMS_data)
   if SMS_data.get('event_id') == 'DIRECTION_OUTGOING':
       print('SMS not received')
       return '-2', 200
   stage = text_to_stage(SMS_data.get('text'))
   print(stage)
   if stage == -1:
       return '-3', 200
   num = set_by_tel(SMS_data.get('sender'), stage)
   return str(num), 200

От платформы, по факту приёма на привязанный к приложению номер телефона, мы получаем HTTP-запрос, содержащий JSON с информацией об отправителе и получателе, а также текст сообщения. В поле event_id отражается, было SMS входящим или исходящим. По тексту входящего определяем, относится ли оно к экодню и о прохождении какого задания нас уведомил клиент.

Здесь это реализовано по вхождению в текст ключевых слов, соответствующих каждому заданию, при помощи функции text_to_stage. По номеру телефона отправителя находим ID клиента, проверяем его участие в игре, устанавливаем отметку за выполнение задания.

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

def set_by_tel(tel, stage, num=1):
   client_id = db.phone_table.loc[tel].iloc[0]
   if db.task_table.loc[client_id, 'S1'] == 0 and stage > 1:
       return -1  # Client is not participant!
   db.task_table.loc[client_id, 'S'+str(stage)] = num
   return num






def text_to_stage(text: str):
   for k in STAGE_DICT:
       if k in text:
           return STAGE_DICT[k]
   return -1

Вебхуки, которые принимает сервер, настраиваются в личном кабинете МТС Exolve. Для этого нужно зайти в настройки вашего приложения, во вкладке «Уведомления о событиях» нажать на значок карандаша, вписать адрес эндпоинта и сохранить.

Настройка вебхука в личном кабинете
Настройка вебхука в личном кабинете

Сервис рассылки сообщений можно разместить на бесплатном хостинге, позволяющем развёртывать веб-приложения. Сделать это придётся, поскольку нам нужно принимать запросы, а не только их отправлять. Удобнее всего разместить сервис в докер-контейнере и разворачивать с GitHub, как мы это делали в одной из предыдущих статей.

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

def get_contacts():
   truncated_contact_items = endpoint.get_all("crm.contact.list")
   result = []
   for tci in truncated_contact_items:
       ans = endpoint.call("crm.contact.get", items={"ID": tci["ID"]})
       result.append(ans['order0000000000'])
   return result




def get_deals():
   return endpoint.get_all("crm.deal.list")




def is_contact_in_deal(deal_item: dict, contact: dict | str):
   if type(contact) is dict: contact = contact["ID"]
   return deal_item['CONTACT_ID'] == contact




def search_deals_by_contact(deals: list[dict], contact: dict | str):
   contact_deals = []
   for d in deals:
       if is_contact_in_deal(d, contact): contact_deals.append(d)
   return contact_deals




def get_deals_sum(contact_deals: list[dict]):
   deals_sum = 0
   for d in contact_deals:
       if d['STAGE_SEMANTIC_ID'] != 'S':  # The deal is not successful
           continue
       deals_sum += float(d['OPPORTUNITY'])
   return deals_sum

Объект класса Bitrix позволяет не только удобно обернуть методы API «Битрикс», но и извлечь список запрошенных объектов целиком. Здесь мы получаем полный список контактов клиентов при помощи метода get_contacts, а также перечень сделок — через get_deals.

Следующие функции нужны для поиска сделок, заключённых с клиентом. Последняя функция выбирает успешные сделки по значению в поле STAGE_SEMANTIC_ID — что соответствует стадии в CRM. На последнюю стадию сделка переходит после получения оплаты. Сумма сделки хранится в поле OPPRTUNITY.

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

Как всё выглядит

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

Общение с платформой
Общение с платформой
Закрытая сделка на номер клиента
Закрытая сделка на номер клиента

Сегодня мы рассмотрели, как вовлечь клиентов во взаимодействие и повысить продажи, а заодно простимулировать экологически ответственное поведение. Для этого мы использовали возможности платформы МТС Exolve и совместили их с «Битрикс».

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

Tags:
Hubs:
Total votes 8: ↑8 and ↓0+10
Comments1

Articles

Information

Website
exolve.ru
Founded
Employees
501–1,000 employees