Простейший биллинг SnuBill 31.03.2015

UPD. Прекрасный человек по имени Максим адаптировал мой код под FreePBX - его проект вы можете посмотреть по этой ссылке.

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

К написанию этой статьи меня подтолкнул вопрос одного иноземного знакомца, который поинтересовался на тему биллинга в Астериске. Конечно, есть множество систем биллинга - тот же AsteriskNOW, насколько мне известно, содержит зачатки биллинга или, можно сказать, индустриальный стандарт A2Billing. Но порочная страсть все сделать самому победила, и я набросал простой скрипт биллинга.

У данного скрипта следующие ограничения:

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

Логика работы

Скрипт состоит из двух макросов - snubill-call и snubill-finish.

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

После этого производится набор номера (см. ниже). По завершении разговора, вызывается макрос snubill-finish, который обновляет баланс пользователя.

Макрос использует следующую логику тарификации: существует таблица префиксов с ценами на звонок по этому префиксу. Например, пустой префикс - 10р/мин, префикс 8926 - 20р/мин, префикс 8926227 - 30р/мин.

Номер проверяется на соответствие префиксу максимальной длины. Т.е., в данном примере, при наборе номера 8-926-227-0000 - будет использована цена 30р/мин, для номера 8-926-111-0000 - 20р/мин, при наборе 8-903-111-0000 - 10р/мин.

Настройка базы данных

Для работы скрипта, первым делом необходимо настроить подключение к MySQL. В принципе, все запросы простейшие, так что вы можете переписать их под вашу любимую БД. Я использовал MySQL через ODBC. О настройке такой связки можете смело почитать тут.

Для работы скрипта у вас должно быть две таблицы.

Первая - пользователи и их балансы. Я создал такую таблицу:

CREATE TABLE users
(
  `user_id` VARCHAR(255) NOT NULL,
  `current_balance` FLOAT NOT NULL DEFAULT 0,
  PRIMARY KEY (`user_id`)
)

Вторая таблица - префиксы номеров и цена за минуту.

CREATE TABLE tariffs (
  `prefix` VARCHAR(20) NOT NULL,
  `price` FLOAT NOT NULL DEFAULT 0,
  PRIMARY KEY (`prefix`))

Во вторую таблицу лучше сразу вставить тариф по-умолчанию (в данном случае - 10р/мин):

INSERT INTO tariffs(prefix,price) VALUES ('',10);

Теперь создадим запросы в func_odbc.conf:

; Получение цены за минуту по префиксу
[SNUBILL_GET_PRICE]
dsn=asterisk
readsql=SELECT price FROM tariffs WHERE '${SQL_ESC(${ARG1})}' LIKE CONCAT(prefix,'%') ORDER BY CHAR_LENGTH(prefix) DESC LIMIT 0,1

; Получение баланса пользователя
[SNUBILL_GET_BALANCE]
dsn=asterisk
readsql=SELECT current_balance FROM users WHERE user_id='${SQL_ESC(${ARG1})}'

; Обновление баланса
[SNUBILL_UPDATE_BALANCE]
dsn=asterisk
writesql=UPDATE users SET current_balance=current_balance-${VAL1} WHERE user_id='${SQL_ESC(${ARG1})}'

После этого, через консоль Asterisk перегружаем func_odbc и проверяем, что функции появились:

CLI> module reload func_odbc.so
CLI> odbc read
ODBC_SNUBILL_GET_BALANCE     ODBC_SNUBILL_GET_PRICE
ODBC_SNUBILL_UPDATE_BALANCE

Код макросов

Я вынес макросы в отдельный файл, snubill.conf.

; Это файл макросов snubill
; (c) Василий Snussi Шоков, https://www.snussi.ru, snussi@snussi.ru
; Если при использовании макроса упомянете мой сайт - буду рад
;
; Документацию по макросу можно посмотреть по адресу https://www.snussi.ru/asterisk/snubill.html

; snubill-call - звонок на номер
;
; На вход макроса подается два аргумента - идентификатор пользователя и набираемый телефон
; В результате создается переменная канала SNUBILL_MINUTES_COUNT, которая содержит число целых минут,
; доступных пользователю для данного звонка

[macro-snubill-call]
exten => s,1,NoOp(SnuBill-call v0.1)
exten => s,n,NoOp(User: ${ARG1}, target number: ${ARG2})

; Перенесем в переменные канала пользователя
; чтобы потом использовать при работе snubill-finish
exten => s,n,Set(__SNUBILL_USER_ID=${ARG1})

; Проверим, что задан идентификатор пользователя
exten => s,n,GotoIf($['${SNUBILL_USER_ID}' = ''] ?:have_user)
exten => s,n,NoOp(Empty user id for this call)
exten => s,n,MacroExit()

; Установим количество доступных минут в 0
exten => s,n(have_user),Set(__SNUBILL_MINUTES_COUNT=0)

; Получим тариф на указанный телефон
exten => s,n,Set(__SNUBILL_PRICE=${ODBC_SNUBILL_GET_PRICE(${ARG2})})

; Возможно, номер не подошел ни под один из шаблонов. В этом случае, вернется прайс ''
; В этой ситуации вернемся из макроса с сообщением
exten => s,n,GotoIf($['${SNUBILL_PRICE}' = ''] ?:have_price)
exten => s,n,NoOp(Empty price for this call)
exten => s,n,MacroExit()

; Прайс есть. Получим теперь баланс пользователя
exten => s,n(have_price),Set(SNUBILL_BALANCE=${ODBC_SNUBILL_GET_BALANCE(${ARG1})})


; Возможно, такого пользователя нет. или еще что-то. В этом случае, вернется баланс ''
; В этой ситуации вернемся из макроса с сообщением
exten => s,n,GotoIf($['${SNUBILL_PRICE}' = ''] ?:have_balance)
exten => s,n,NoOp(Empty balance for this user)
exten => s,n,MacroExit()

; Баланс есть.
; Разберемся с количеством минут.
; Возможны два варианта. Стоимость минуты больше нуля или 0 (и меньше)
; Если 0 и меньше, то считаем это безлимитным и возвращаем 3600 минут.
exten => s,n(have_balance),Set(__SNUBILL_MINUTES_COUNT=${IF($[${SNUBILL_PRICE}>0] ?${MATH( ${SNUBILL_BALANCE} / ${SNUBILL_PRICE} ,i)}:3600)})

exten => s,n,NoOp(Price per minute is ${SNUBILL_PRICE}, balance is ${SNUBILL_BALANCE}, minutes count is ${__SNUBILL_MINUTES_COUNT})

; Выходим
exten => s,n,MacroExit()



; snubill-finish
;
; Обновление баланса после звонка.
;
; Обновление использует переменную ANSWEREDTIME, устанавливаемую DIAL,
; а так же SNUBILL_USER_ID и SNUBILL_PRICE, установленные через snubill-call
[macro-snubill-finish]
exten => s,1,NoOp(SnuBill-finish v0.1)

; Проверим, что задан идентификатор пользователя
exten => s,n,GotoIf($['${SNUBILL_USER_ID}' = ''] ?:have_user)
exten => s,n,NoOp(Empty user id for this call)
exten => s,n,MacroExit()

; Проверим, что установлен прайс
exten => s,n(have_user),GotoIf($['${SNUBILL_PRICE}' = ''] ?:have_price)
exten => s,n,NoOp(Empty price for this call)
exten => s,n,MacroExit()

; Наконец, проверим, что есть количество секунд ответа
exten => s,n(have_price),GotoIf($['${ANSWEREDTIME}' = ''] ?:have_answer)
exten => s,n,NoOp(Empty price for this call)
exten => s,n,MacroExit()


; Прайс есть. Получим теперь количество минут, которые проговорил пользователь
; с округлением в большую сторону
exten => s,n(have_answer),Set(SNUBILL_MINUTES=$[CEIL(${ANSWEREDTIME} / 60 )])

; Теперь можно умножить минуты на прайс и обновить баланс пользователя
exten => s,n,Set(ODBC_SNUBILL_UPDATE_BALANCE(${SNUBILL_USER_ID})=${MATH(${SNUBILL_MINUTES} * ${SNUBILL_PRICE},f)})

exten => s,n,MacroExit()

Использование в диалплане

Простой пример использования в диалплане.

Включаем сам макрос в диалплан

#include "snubill.conf"

При вызове некоего экстеншна (в моем примере - TEST_EXT) вызываем макрос

exten => TEST_EXT,1,NoOp(TEST_EXT is called)

; Установим тестовый идентификатор пользователя и номер
exten => TEST_EXT,n,Set(NUMBER_TO_DIAL=89261110000)
exten => TEST_EXT,n,Set(USER_ID=test)

; Вызываем макрос
exten => TEST_EXT,n,Macro(snubill-call,${USER_ID},${NUMBER_TO_DIAL})
exten => TEST_EXT,n,NoOp(${SNUBILL_MINUTES_COUNT})

; Если нет свободных минут, то переходим к завершению вызова (или к сообщению, что нет денег на счете)
exten => TEST_EXT,n,GotoIf($[${SNUBILL_MINUTES_COUNT}>0]?:the_end)

; Набираем номер. При этом, нужен флаг L, чтобы ограничить вызов
; Параметр L передается в миллисекундах, поэтому количество минут надо умножить на 60.000
exten => TEST_EXT,n,Dial(SIP/external_line_mgts/${NUMBER_TO_DIAL},,rL(${MATH(${SNUBILL_MINUTES_COUNT} * 60000,i)}) )


; Вешаем трубку
exten => TEST_EXT,n(the_end),Hangout()

В том же контексте, необходимо прописать для экстеншна h макрос, закрывающий соединение:

; По окончании звонка, вызываем макрос обновления баланса
exten => h,n,Macro(snubill-finish)