- PVSM.RU - https://www.pvsm.ru -
При разработке большинства сервисов возникает потребность во внутреннем биллинге для аккаунтов сервиса. Так и в нашем сервисе [1] возникла такая задача. Готовые пакеты для её решения мы так и не смогли найти, в итоге пришлось разрабатывать систему биллинга с нуля.
В статье хочу рассказать о нашем опыте и подводных камнях, с которыми пришлось столкнуться во время разработки.
Задачи, которые нам предстояло решить были типичны для любой системы денежного учета: прием платежей, лог транзакций, оплата и повторяющиеся платежи (подписка).
Основной единицей системы, очевидно, была выбрана транзакция. Для транзакции была написана следующая простая модель:
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 [2] из ORM Django (считаем сумму значений одного столбца), но мы столкнулись с тем, что при большом количестве транзакций данная операция сильно нагружает БД. Поэтому было решено денормализовать БД, добавив поле “balance” в модель пользователя. Данное поле обновляется в методе “save” в модели “UserBalanceChange”, а для уверенности в актуальности данных в нем, мы каждую ночь его пересчитываем.
Правильнее, конечно же, хранить информацию о текущем балансе пользователя в кэше (например, в Redis) и инвалидировать при каждом изменении модели.
Для самых популярных систем приема платежей есть готовые пакеты, поэтому проблем с их установкой и настройкой, как правило, не возникает. Достаточно выполнить несколько простых шагов:
Прием платежей реализуется очень гибко, например, для системы Robokassa (используемся приложение django-robokassa [3]) код выглядит так:
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 [4] – написан task, который выполняется каждый час. Логика в этом моменте получилась сложная, так как необходимо учитывать много факторов:
Мы пытались реализовать данный алгоритм без введения дополнительного поля, но получилось не красиво и не удобно. Поэтому нам пришлось в модель User добавить поле 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] для удобного dashboard в панели администрирования. Мы решили, что будем следить за следующими двумя важными показателями:
Первый показатель для нас является своего рода показателем роста (traction) нашего стартапа, второй — это возвращаемость (retention) пользователей.
О том, как мы реализовали dashboard и следим за метриками, мы расскажем в одной из следующих статей.
Желаю всем удачной настройки биллинг-системы и получения больших платежей!
P.S. Уже в процессе написания статьи нашел готовый пакет django-account-balances [6], думаю, что можно обратить внимание, если вы делаете систему лояльности.
Автор: akamoroz
Источник [7]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/68556
Ссылки в тексте:
[1] нашем сервисе: http://bitcalm.com/
[2] annotate: https://docs.djangoproject.com/en/dev/topics/db/aggregation/
[3] django-robokassa: https://bitbucket.org/kmike/django-robokassa/
[4] celery: http://www.celeryproject.org/
[5] django-admin-tools: https://bitbucket.org/izi/django-admin-tools/wiki/Home
[6] django-account-balances: https://pypi.python.org/pypi/django-account-balances
[7] Источник: http://habrahabr.ru/post/234861/
Нажмите здесь для печати.