Статьи

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

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

В этой статье я хочу рассказать о нашем опыте и подводных камнях, возникших в процессе разработки.

Задачи

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

Сделка

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

class UserBalanceChange(models.Model):
    user = models.ForeignKey('User', related_name='balance_changes')
    reason = models.IntegerField(choices=REASON_CHOICES, default=NO_REASO)
    amount = models.DecimalField(_('Amount'), default=0, max_digits=18, decimal_places=6)
    datetime = models.DateTimeField(_('date'), default=timezone.now)

Транзакция состоит из ссылок на пользователя, причин пополнения (или транзакции), суммы транзакции и времени транзакции.

Баланс

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

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

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

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

  • Зарегистрироваться в платежной системе;

  • Получить ключи API;

  • Установите соответствующий пакет для Django;

  • Внедрить форму оплаты;

  • Реализовать функции зачисления на баланс после оплаты.

Прием платежей осуществляется очень гибко, например, для системы PayPal код выглядит так:

from paypal.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.paypal)
        balance_change.save()

Аналогичным образом вы можете подключить любую платежную систему, такую ​​как B raintree , Stripe и т. Д.

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

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

payment_sum = 8.32
users = User.objects.filter(id__in=has_clients, balance__gt=payment_sum).select_related('tariff')

Здесь мы написали это без «аннотации», потому что в будущем будут некоторые дополнительные проверки.

Повторная транзакция

Разобравшись с основами, перейдем к забавной части — повторяющимся транзакциям. Нам нужно ежечасно (назовем это «биллинговым периодом») списывать определенную сумму денег с пользователя в соответствии с его тарифным планом. Для реализации этого механизма мы используем   задание, написанное на  сельдерее , которое выполняется каждый час. Логика в данный момент сложна для понимания, поскольку необходимо учитывать множество факторов:

  • между заданиями в сельдерее у нас никогда не будет ровно одного часа (расчетный период);

  • пользователь пополняет баланс (становится> 0) и получает доступ к услугам между периодами выставления счетов, съемка за период будет несправедливой;

  • пользователь может изменить тариф в любое время;

  • сельдерей может по какой-то причине перестать выполнять задания

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

Идея заключается в следующем:

  • Каждый расчетный период мы смотрим 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)
Эта модель позволит очень гибко осуществлять регулярные платежи.

Щиток приборов

Мы используем  django-admin-tools   для удобной панели инструментов в панели администрирования. Мы решили, что будем отслеживать следующие два важных параметра:

  • Последние 5 платежей и график платежей пользователей за последний месяц;

  • Пользователи, чей баланс близок к 0 (из тех, кто уже заплатил);

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

Как мы реализовали панель мониторинга и как мы отслеживаем показатели в  нашем проекте   ? Это вопросы к следующей статье.

Желаю всем вам успешной настройки биллинговой системы и надеюсь, что вы получите больше платежей!

PS Уже в процессе написания этой статьи я нашел полный пакет  django-account-balances, на  который стоит обратить внимание, если у вас есть система лояльности.