In the development of most services there is a need in the internal billing for service accounts. Our service also had such a problem. We were not able to find ready-made packages for its solution and in the end had to develop a billing system from scratch.
In this article I want to talk about our experiences and pitfalls encountered during development.
Tasks that we had to solve were typical for any cash accounting system: the acceptance of payments, transaction log, typical payment and recurring payments (subscription).
Transaction was obviously selected as a basic unit of the syste. For the transaction we have written the following simple model:
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)
The transaction consists of links to the user, the reasons for the replenishment (or transaction), the transaction amount and time of the transaction.
Balance of a user is very easy to calculate by using the annotate function of ORM Django (consider the sum of the values of the column), but we have faced with the fact that a with large number of transactions this operation overloads database. It was therefore decided to denormalize the database, add the "balance" field in the user model. This field is updated in the method "save" in the "UserBalanceChange" model, and for the confidence in the relevance of the data in it we recalculate it every night.
It is, of course, more correct to store information about the current balance of a user in a cache (e.g., in Redis) and invalidate the model with each change.
For the most popular system of payments there are ready-made bags, so, as a rule, there are no problems with their installation and configuration. Just follow a few simple steps:
Register in the payment system;
Obtain the API keys;
Install the appropriate package for Django;
Implement a form of payment;
- Implement the functions of the enrollment in the balance after payment.
Payments acceptance is implemented very flexibly, for example, for the PayPal system the code looks like this:
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()
Similarly, you can connect any payment system, such as Braintree, Stripe, etc.
To write off is a bit more complicated - before surgery it is necessary to check what the balance of the account will be after the operation, the "honest" manner - using the annotate. This should be done in order not to serve user "on credit", which is especially important when the transactions are carried out on large sums of money.
payment_sum = 8.32 users = User.objects.filter(id__in=has_clients, balance__gt=payment_sum).select_related('tariff')
Here we have written it without “annotate”, because in the future there are some additional checks.
Having dealt with the basics, move on to the fun part - recurring transactions. We need to hourly (let’s call it a "billing period") write off a certain amount of money from a user in accordance with his tariff planf. To implement this mechanism, we use a celery - written task, which runs every hour. The logic in this moment is difficult to comprehend, since it is necessary to take into account many factors:
between tasks in the celery we will never have exactly one hour (billing period);
the user fills up the balance (it becomes> 0) and gains access to services between billing periods, the shoot for the period would be unfair;
the user can change the tariff at any time;
- celery mayfor some reason cease to perform tasks
We tried to implement this algorithm without introducing an additional field, but it turned out to be inconvenient. So we had to add the last_hourly_billing field to the User model, which indicates the time of the last repeted operation.
The idea is:
Each billing period we look last_hourly_billing and write off according to the tariff plan, then update the last_hourly_billing field;
When changing the tariff plan, we debited for the past rate and update the last_hourly_billing field;
- When you activate the service, we update the last_hourly_billing field.
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. Rather, in the process of refactoring we will write an additional model. It might look 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)
We use django-admin-tools for a convenient dashboard in the administration panel. We decided that we would track the following two important parameters:
Last 5 payments and payment schedule of users in 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 growth (traction) of our start-up, the second is a recurrence (retention) of users.
How we implemented dashboard and how we monitor the metrics in our projec ? These are the questions for the next article.
I wish you all a successful adjustment of a billing system and hope you will receive more payments!
P.S. Already in the process of writing this article I found a complete package django-account-balances I think this is worth paying your attention to, if you have a loyalty system.