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 django.conf import settings from .constans import STATE_CHOICES 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=None, # state=None, uuid=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( uuid=uuid, payer=payer, payee=payee, amount=amount, delay=delay, # detail_id=detail_id, application=application ) # if state: # transaction.state = state transaction.save() return transaction class Transaction(models.Model): state_choices = STATE_CHOICES 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 = models.ForeignKey(settings.OAUTH2_PROVIDER_APPLICATION_MODEL, on_delete=models.PROTECT, related_name='transactions', null=True) detail_id = models.IntegerField(null=True, blank=True) # TODO: or better detail url objects = TransactionManager() # class Meta: # unique_together = ('application', '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') # TODO: check bundry conditions if not self.state == STATE_CHOICES.created: raise APIException('this transaction is not ready to pay') if not self.payer: self.payer = payer with transaction.atomic(): self.last_update = datetime.datetime.now() 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 = STATE_CHOICES.delayed self.save() else: self.payee.balance = F('balance') + self.amount self.payee.save(update_fields=['balance']) self.state = STATE_CHOICES.success self.save() self.save() return self def receipt(self): # TODO: check bundry conditions if not self.state == STATE_CHOICES.delayed: raise APIException('this transaction is not delayed') with transaction.atomic(): self.last_update = datetime.datetime.now() self.payee.balance = F('balance') + self.amount self.state = STATE_CHOICES.success self.payee.save() self.save() def submit(self): if not self.payer: raise APIException('payer identification error') if not self.payee: raise APIException('payee identification error') # TODO: check bundry conditions if not self.state == STATE_CHOICES.created: raise APIException('this transaction is not ready to pay') with transaction.atomic(): self.last_update = datetime.datetime.now() 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() self.state = STATE_CHOICES.pending if self.payer.balance < 0: raise APIException('not enough balance') self.save() return self def verify(self): # TODO: check bundry conditions if not self.state == STATE_CHOICES.pending: raise APIException('this transaction is not pending') with transaction.atomic(): self.last_update = datetime.datetime.now() self.payee.balance = F('balance') + self.amount self.state = STATE_CHOICES.success self.payee.save() self.save() def __str__(self): return f"{self.payer} -> {self.payee} ({self.uuid}): {self.amount}"