django

Разработка своей системы биллинга на Django

  • суббота, 30 августа 2014 г. в 03:10:32
http://habrahabr.ru/company/bitcalm/blog/234861/

При разработке большинства сервисов возникает потребность во внутреннем биллинге для аккаунтов сервиса. Так и в нашем сервисе возникла такая задача. Готовые пакеты для её решения мы так и не смогли найти, в итоге пришлось разрабатывать систему биллинга с нуля.
В статье хочу рассказать о нашем опыте и подводных камнях, с которыми пришлось столкнуться во время разработки.

Задачи

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

Транзакции

Основной единицей системы, очевидно, была выбрана транзакция. Для транзакции была написана следующая простая модель:
class UserBalanceChange(models.Model):
    user = models.ForeignKey('User', related_name='balance_changes')
    reason = models.IntegerField(choices=REASON_CHOICES, default=NO_REASON)
    amount = models.DecimalField(_('Amount'), default=0, max_digits=18, decimal_places=6)
    datetime = models.DateTimeField(_('date'), default=timezone.now)
Транзакция состоит из ссылки на пользователя, причины пополнения (или списания), суммы транзакции и времени совершения операции.

Баланс

Баланс пользователя очень легко посчитать при помощи функции annotate из ORM Django (считаем сумму значений одного столбца), но мы столкнулись с тем, что при большом количестве транзакций данная операция сильно нагружает БД. Поэтому было решено денормализовать БД, добавив поле “balance” в модель пользователя. Данное поле обновляется в методе “save” в модели “UserBalanceChange”, а для уверенности в актуальности данных в нем, мы каждую ночь его пересчитываем.
Правильнее, конечно же, хранить информацию о текущем балансе пользователя в кэше (например, в Redis) и инвалидировать при каждом изменении модели.

Прием платежей

Для самых популярных систем приема платежей есть готовые пакеты, поэтому проблем с их установкой и настройкой, как правило, не возникает. Достаточно выполнить несколько простых шагов:
  • Регистрируемся в платежной системе;
  • Получаем API ключи;
  • Устанавливаем соответствующий пакет для Django;
  • Реализовываем форму оплаты;
  • Реализовываем функцию зачисления средств на баланс после оплаты.
Прием платежей реализуется очень гибко, например, для системы Robokassa (используемся приложение django-robokassa) код выглядит так:
from robokassa.signals import result_received
def payment_received(sender, **kwargs):
    order = OrderForPayment.objects.get(id=kwargs['InvId'])
    user = User.objects.get(id=order.user.id)
    order.success=True
    order.save()
    try:
        sum = float(order.payment)
    except Exception, e:
        pass
    else:
        balance_change = UserBalanceChange(user=user, amount=sum, reason=BALANCE_REASONS.ROBOKASSA)
        balance_change.save()
По аналогии можно подключить любую систему оплаты, например PayPal, Яндекс.Касса

Списание средств

Со списаниями чуть сложнее – перед операцией необходимо проверять, каким будет баланс счета после проведения операции, причем “по-честному” – при помощи annotate. Это необходимо делать для того, чтобы не обслуживать пользователя “в кредит”, что особенно важно, когда транзакции выполняются на большие суммы.
payment_sum = 8.32
users = User.objects.filter(id__in=has_clients, balance__gt=payment_sum).select_related('tariff')
Здесь мы написали без annotate, так как в данейшем есть дополнительные проверки.

Повторяющиеся списания

Разобравшись с основами, переходим к самому интересному — повторяющимся списаниям. У нас есть потребность каждый час (назовет это “биллинг-период”) снимать с пользователя определенную сумму в соответствии с его тарифным планом. Для реализации этого механизма мы используем celery – написан task, который выполняется каждый час. Логика в этом моменте получилась сложная, так как необходимо учитывать много факторов:
  • между выполнениями задачи в celery никогда не пройдет ровно час (биллинг-период);
  • пользователь пополняет свой баланс (он становится >0) и получает доступ к услугам между биллинг-периодами, снимать за период было бы нечестно;
  • пользователь может поменять тариф в любое время;
  • celery может по каким-либо причинам перестать выполнять задачи

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

def charge_tariff_hour_rate(user):
    now = datetime.now
    second_rate = user.get_second_rate()
    hour_rate = (now - user.last_hourly_billing).total_seconds() * second_rate
    balance_change_reason = UserBalanceChange.objects.create(
                user=user,
                reason=UserBalanceChange.TARIFF_HOUR_CHARGE,
                amount=-hour_rate,
    )
    balance_change_reason.save()
    user.last_hourly_billing = now
    user.save()

Данная система, к сожалению, не является гибкой: если мы добавим еще один тип повторяющихся платежей — придется добавлять новое поле. Скорее всего, в процессе рефакторинга, мы напишем дополнительную модель. Примерно такую:
class UserBalanceSubscriptionLast(models.Model):
    user = models.ForeignKey('User', related_name='balance_changes')
    subscription = models.ForeignKey('Subscription', related_name='subscription_changes')
    datetime = models.DateTimeField(_('date'), default=timezone.now)

Эта модель позволит очень гибко реализовать повторяющиеся платежи.

Dashboard

Мы используем django-admin-tools для удобного dashboard в панели администрирования. Мы решили, что будем следить за следующими двумя важными показателями:
  • Последние 5 оплат и график платежей пользователей за последний месяц;
  • Пользователи, у которых баланс приближается к 0 (из тех, кто уже платил);

Первый показатель для нас является своего рода показателем роста (traction) нашего стартапа, второй — это возвращаемость (retention) пользователей.
О том, как мы реализовали dashboard и следим за метриками, мы расскажем в одной из следующих статей.
Желаю всем удачной настройки биллинг-системы и получения больших платежей!

P.S. Уже в процессе написания статьи нашел готовый пакет django-account-balances, думаю, что можно обратить внимание, если вы делаете систему лояльности.