Developing your own billing system on Django

  • Tutorial
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 any ready-made 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 I had to face during development.


The tasks that we had to solve were typical for any system of cash accounting: receiving payments, transaction logs, payment and recurring payments (subscription).


The main unit of the system, obviously, was chosen 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'),
A transaction consists of a link to the user, the reason for the replenishment (or write-off), the amount of the transaction and the time of the operation.


It is very easy to calculate the user's balance using the annotate function from ORM Django (we consider the sum of the values ​​of one column), but we are faced with the fact that with a large number of transactions this operation loads the database heavily. 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 be sure of the relevance of the data in it, we recalculate it every night.
It’s more correct, of course, to store information about the user's current balance in the cache (for example, in Redis) and invalidate each time the model changes.

Accept payments

There are ready-made packages for the most popular payment acceptance systems, therefore, as a rule, there are no problems with their installation and configuration. It is enough to follow a few simple steps:
  • We are registered in the payment system;
  • We receive API keys;
  • Install the appropriate package for Django;
  • We realize the form of payment;
  • We realize the function of crediting funds to the balance after payment.
Accepting payments is very flexible, for example, for the Robokassa system (using 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(
        sum = float(order.payment)
    except Exception, e:
        balance_change = UserBalanceChange(user=user, amount=sum, reason=BALANCE_REASONS.ROBOKASSA)
By analogy, you can connect any payment system, for example PayPal, Yandex.Cash


With write-offs it’s a bit more complicated - before the operation it is necessary to check what the account balance will be after the operation, and “honestly” - using annotate. This is necessary in order not to serve the user “on credit”, which is especially important when transactions are carried out in 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 in the future there are additional checks.

Duplicate charges

Having dealt with the basics, we move on to the most interesting - repeated charges. We have a need to withdraw a certain amount from the user every hour (call it a “billing period”) in accordance with his tariff plan. To implement this mechanism, we use celery - a task is written that runs every hour. The logic at this point turned out to be complicated, since many factors must be taken into account:
  • exactly one hour will pass between task executions in celery (billing period);
  • the user replenishes his balance (he becomes> 0) and gets access to services between billing periods, it would be dishonest to take off for a period;
  • the user can change the tariff at any time;
  • celery may stop performing tasks for some reason

We tried to implement this algorithm without introducing an additional field, but it turned out not to be beautiful and not 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.
Logic of work:
  • Each billing period, we look at the time last_hourly_billing and write off the amount according to the tariff plan, then update the field last_hourly_billing;
  • When changing the tariff plan, we write off the amount at the previous tariff and update the last_hourly_billing field;
  • When the service is activated, we update the last_hourly_billing field.

def charge_tariff_hour_rate(user):
    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.last_hourly_billing = now

This system, unfortunately, is not flexible: if we add another type of recurring payments, you 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'),

This model will allow very flexible implementation of recurring payments.


We use django-admin-tools for a convenient dashboard in the admin panel. We decided that we would monitor the following two important indicators:
  • Last 5 payments and user payment schedule for the last month;
  • Users whose balance is close to 0 (of those who have already paid);

The first indicator for us is a kind of indicator of the growth (traction) of our startup, the second is the retention of users.
We will talk about how we implemented dashboard and follow metrics in one of the following articles.
I wish everyone a successful setting up the billing system and receive large payments!

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

Also popular now: