import datetime import uuid from django.db import models from django.db import transaction from django.db.models import F from django.utils.translation import gettext_lazy as _ from model_utils.choices import Choices from rest_framework.exceptions import APIException, ValidationError from apps.users.models import User class TransactionManager(models.Manager): def create_transaction(self, amount, payee: User, payer: User = None, delay=0, detail_id=None, application_id=None, state=None): if not (isinstance(payer, User) or payer is None): raise ValidationError('payer type not valid') if not isinstance(payee, User): raise ValidationError('payee type not valid') if not amount > 0: raise ValidationError('amount should be greater than zero') if not delay >= 0: raise ValidationError('delay should be greater than equal zero') transaction = self.model(payer=payer, payee=payee, amount=amount, delay=delay, detail_id=detail_id, application_id=application_id ) if state: transaction.state = state transaction.save() return transaction class Transaction(models.Model): STATE_CHOICES = Choices( (1, 'created', 'created'), (2, 'delayed', 'delayed'), (3, 'pending', 'pending'), (4, 'failed', 'failed'), (5, 'done', 'done'), ) uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4, unique=True, db_index=True) # reserve_code amount = models.IntegerField(_('amount'), blank=False) delay = models.IntegerField(default=0, blank=True) # number of days for delayed payments payer = models.ForeignKey(User, on_delete=models.PROTECT, related_name='payments', blank=True, null=True) payee = models.ForeignKey(User, on_delete=models.PROTECT, related_name='receipts') state = models.IntegerField(choices=STATE_CHOICES, default=STATE_CHOICES.created) create = models.DateTimeField(_('create'), blank=True, null=True, auto_now=True) last_update = models.DateTimeField(_('last update'), blank=True, null=True, auto_now=True) application_id = models.IntegerField("application ID", blank=True, null=True) detail_id = models.IntegerField(null=True, blank=True) # or better detail url or res number objects = TransactionManager() class Meta: unique_together = ('application_id', 'detail_id',) def pay(self, payer=None): if not self.payer and not payer: raise APIException('payer identification error') if self.payer and payer and not self.payer == payer: raise APIException('payer identification error') if not self.payer: self.payer = payer with transaction.atomic(): # TODO: check bundry conditions self.last_update = datetime.datetime.now() if not self.state == self.STATE_CHOICES.created: raise APIException('this transaction is not ready to pay') if self.payer.balance < self.amount: raise APIException('not enough balance') self.payer.balance = F('balance') - self.amount self.payer.save(update_fields=['balance']) self.payer.refresh_from_db() if self.payer.balance < 0: raise APIException('not enough balance') if self.delay: self.state = self.STATE_CHOICES.delayed self.save() else: self.payee.balance = F('balance') + self.amount self.payee.save(update_fields=['balance']) self.state = self.STATE_CHOICES.done self.save() self.save() return self def receipt(self): with transaction.atomic(): self.last_update = datetime.datetime.now() # TODO: check bundry conditions if not self.state == self.STATE_CHOICES.delayed: raise APIException('this transaction is not delayed') self.payee.balance = F('balance') + self.amount self.state = self.STATE_CHOICES.done self.payee.save() self.save() def __str__(self): return f"{self.payer} -> {self.payee} ({self.uuid}): {self.amount}"