📜 ⬆️ ⬇️

Developing your Django billing system

When developing most services, there is a need for internal billing for service accounts. So in our service there was such a problem. We could not find ready packages for its solution, as a result we had to develop a billing system from scratch.
In the article I want to talk about our experience and the pitfalls that had to face during the development.

Tasks

The tasks that we had to solve were typical of any monetary accounting system: payment acceptance, transaction log, payment, and recurring payments (subscription).

Transactions

The main unit of the system, obviously, was selected transaction. The following simple model was written for the transaction:
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) 
A transaction consists of a link to the user, the reasons for the deposit (or write-off), the amount of the transaction and the time of the transaction.
')

Balance

The user’s balance is very easy to calculate using the annotate function from the ORM Django (counting the sum of the values ​​of one column), but we are faced with the fact that with a large number of transactions this operation heavily loads the database. Therefore, it was decided to denormalize the database by adding the “balance” field to the user model. This field is updated in the “save” method in the “UserBalanceChange” model, and to make sure that the data in it is current, we recalculate it every night.
It is more correct, of course, to store information about the current balance of the user in the cache (for example, in Redis) and to invalidate each time the model is changed.

Payment acceptance

For the most popular payment acceptance systems there are ready-made packages, therefore, problems with their installation and configuration, as a rule, do not arise. Just follow a few simple steps:
Payment acceptance is implemented very flexibly, for example, for the Robokassa system (we use the django-robokassa application ), the code looks like this:
 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() 
By analogy, you can connect any payment system, for example, PayPal, Yandex.Money

Debit

With write-offs, it is a little more difficult - before the operation it is necessary to check what the account balance will be after the operation, and moreover, “honestly” with the help of annotate. This should be done in order not to service the user “on credit”, which is especially important when transactions are performed on large amounts.
 payment_sum = 8.32 users = User.objects.filter(id__in=has_clients, balance__gt=payment_sum).select_related('tariff') 
Here we wrote without annotate, since there are additional checks in this case.

Duplicate charges

Having dealt with the basics, go to the most interesting - recurring write-offs. We have a need every hour (call it a “billing period”) to withdraw a certain amount from a user in accordance with his tariff plan. To implement this mechanism, we use celery - written task, which runs every hour. The logic in this moment turned out to be complicated, since many factors must be taken into account:

We tried to implement this algorithm without introducing an additional field, but it turned out to be neither beautiful nor convenient. Therefore, we had to add the last_hourly_billing field to the User model, where we indicate the time of the last repeated operation.
Work Logic:

 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() 

This system, unfortunately, is not flexible: if we add another type of recurring payments, we will have to add a new field. Most likely, in the process of refactoring, we will write an additional model. Something like this:
 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) 

This model will allow very flexible implementation of recurring payments.

Dashboard

We use django-admin-tools for a convenient dashboard in the admin panel. We decided that we would monitor the following two important indicators:

The first indicator for us is a kind of indicator of growth (traction) of our startup, the second is the reversibility (retention) of users.
How we implemented the dashboard and monitor the metrics will be described in one of the following articles.
I wish you all a successful setting of the billing system and receiving large payments!

PS Already in the process of writing an article I found a ready-made django-account-balances package, I think you can pay attention if you are making a loyalty system.

Source: https://habr.com/ru/post/234861/


All Articles