При разработке большинства сервисов существует потребность во внутреннем биллинге для сервисных аккаунтов. У нашего сервиса тоже была такая проблема. Мы не смогли найти готовые пакеты для ее решения и в итоге пришлось разработать биллинговую систему с нуля.
В этой статье я хочу рассказать о нашем опыте и подводных камнях, возникших в процессе разработки.
Задачи
Задачи, которые нам приходилось решать, были типичными для любой системы учета денежных средств: прием платежей, журнал транзакций, типовые платежи и регулярные платежи (подписка).
Сделка
Сделка была явно выбрана в качестве основной единицы системы. Для транзакции мы написали следующую простую модель:
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, на который стоит обратить внимание, если у вас есть система лояльности.