diff --git a/apps/wallet/models.py b/apps/wallet/models.py index fadc523..fe20004 100755 --- a/apps/wallet/models.py +++ b/apps/wallet/models.py @@ -5,6 +5,7 @@ from django.core.validators import MinValueValidator from django.db import models from django.db import transaction from django.db.models import F +from django.utils import timezone from django.utils.translation import gettext_lazy as _ from rest_framework.exceptions import APIException from django.conf import settings @@ -20,6 +21,7 @@ from ..gooyal_oauth2.models import Application logger = logging.getLogger(__name__) + class CurrencyChoices(models.TextChoices): IRANIAN_RIAL = '166ab7ce33b34799950d45035a9997d6', _('IRANIAN RIAL') # uuid without dash @@ -27,6 +29,7 @@ class CurrencyChoices(models.TextChoices): class AccountQuerySet(models.QuerySet): pass + class Account(BaseModel): owner_uuid = models.UUIDField(db_index=True, null=False) wallet = models.ForeignKey('Wallet', on_delete=models.PROTECT, null=True) @@ -50,6 +53,7 @@ class Account(BaseModel): class WalletQuerySet(models.QuerySet): pass + class Wallet(BaseModel): application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, null=True) label = models.CharField(_('label'), max_length=255) @@ -100,11 +104,11 @@ class Transaction(BaseModel): application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, related_name='transactions', null=True) payer_account = models.ForeignKey(Account, on_delete=models.PROTECT, null=True, - related_name='payer_account_transactions', - db_index=True) + related_name='payer_account_transactions', + db_index=True) payee_account = models.ForeignKey(Account, on_delete=models.PROTECT, null=True, - related_name='payee_account_transactions', - db_index=True) + related_name='payee_account_transactions', + db_index=True) details = models.JSONField(null=True, blank=True, default=dict) # detail = {'payer_name', 'payee_name', 'description', 'application_name', 'payer_avatar_url', 'payee_avatar_url', @@ -153,18 +157,26 @@ class Transaction(BaseModel): def payee_type(self): return self.payee_account.owner_type - def change_state(self, from_state, to_state, error_message=None, **data): - if self.state == from_state: - updated = Transaction.objects.filter(pk=self.pk, state=from_state).update( + def change_state(self, from_states: list | tuple, to_state, same_ok=False, raise_exception=True, + error_message=None, + data: None | dict = None): + data = data or {} + data['updated_at'] = timezone.now() + + if same_ok and self.state == to_state: + return self + + if self.state in from_states: + updated = self.__class__.objects.filter(pk=self.pk, state__in=from_states).update( state=to_state, **data ) if updated: # double check in db level self.state = to_state - else: - raise APIException(error_message) - else: - raise APIException(error_message) + return self + + if raise_exception: + raise Exception(error_message) def withdraw_from_payer_balance(self): logger.debug('start withdraw_from_payer_balance') @@ -186,14 +198,12 @@ class Transaction(BaseModel): else: raise UnprocessableEntity('not enough balance') - def rollback_withdraw_to_payer_balance(self): logger.debug('start rollback deposit_to_payee_balance') logger.info(f'payer_account balance {self.payer_account} -> {self.amount}') self.payer_account.balance = F('balance') + self.amount self.payer_account.save(update_fields=['balance']) - def deposit_to_payee_balance(self): logger.debug('start deposit_to_payee_balance') logger.info(f'payee_account balance {self.payee_account} -> {self.payer_account.balance} + {self.amount}') @@ -223,20 +233,24 @@ class Transaction(BaseModel): with transaction.atomic(): if self.delay: self.change_state( - StateChoices.CREATED, + [StateChoices.CREATED], StateChoices.DELAYED, error_message='this transaction is not ready to pay', - payer_id=payer_id, - payer_type=payer_type + data={ + "payer_id": payer_id, + "payer_type": payer_type + } ) else: self.change_state( - StateChoices.CREATED, + [StateChoices.CREATED], StateChoices.SUCCESS, error_message='this transaction is not ready to pay', - payer_id=payer_id, - payer_type=payer_type + data={ + "payer_id": payer_id, + "payer_type": payer_type + } ) self.deposit_to_payee_balance() @@ -250,7 +264,7 @@ class Transaction(BaseModel): with transaction.atomic(): self.change_state( - StateChoices.DELAYED, + [StateChoices.DELAYED], StateChoices.SUCCESS, error_message='this transaction is not delayed' ) @@ -265,18 +279,18 @@ class Transaction(BaseModel): raise APIException('payee identification error') if not self.payer_account.wallet.currency == self.payee_account.wallet.currency: - self.change_state(from_state=self.state, to_state=StateChoices.FAILED) + self.change_state([self.state], StateChoices.FAILED) logger.info(f'payer({self.payer_account.wallet.currency}) and payee({self.payee_account.wallet.currency}) account currency not equal') raise APIException('payer and payee account currency not equal') if not self.state == StateChoices.CREATED: - logger.debug( f'this transaction is not ready to pay(UUID:{self.uuid}, state:{self.state})') + logger.debug(f'this transaction is not ready to pay(UUID:{self.uuid}, state:{self.state})') raise APIException('this transaction is not ready to pay') with transaction.atomic(): self.withdraw_from_payer_balance() self.change_state( - StateChoices.CREATED, + [StateChoices.CREATED], StateChoices.PENDING, error_message=_('this transaction is not ready to pay') ) @@ -289,7 +303,7 @@ class Transaction(BaseModel): with transaction.atomic(): self.deposit_to_payee_balance() self.change_state( - StateChoices.PENDING, + [StateChoices.PENDING], StateChoices.SUCCESS, error_message=_('this transaction is not pending') ) @@ -302,7 +316,7 @@ class Transaction(BaseModel): with transaction.atomic(): self.rollback_withdraw_to_payer_balance() self.change_state( - self.state, + [StateChoices.PENDING, StateChoices.DELAYED, StateChoices.INCOMPLETE], StateChoices.SUCCESS, error_message=_('TRANSACTION_STATE_NOT_SUITABLE_FOR_ROLLBACK') )