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.translation import gettext_lazy as _ from rest_framework.exceptions import APIException, NotAcceptable from django.conf import settings from oauth2_provider import settings as oauth2_settings from gooyal_accounts_client.models import PublicUser from utils.accounts_client import get_user_details, get_application_details from utils.models import BaseModel from .constans import StateChoices, TypeChoices from apps.users.models import User from ..gooyal_oauth2.models import Application class WalletQuerySet(models.QuerySet): pass class Wallet(BaseModel): owner_uuid = models.UUIDField(db_index=True, null=False) category = models.ForeignKey('Category', on_delete=models.PROTECT, null=True) owner_type = models.IntegerField(choices=TypeChoices.choices, db_index=True) balance = models.BigIntegerField(_('balance'), default=0) objects = WalletQuerySet.as_manager() def get_owner(self): if self.owner_type == TypeChoices.APPLICATION: owner: Application = Application.objects.get(pk=self.owner_uuid) else: owner: User = User.objects.get(pk=self.owner_uuid) return owner class CategoryQuerySet(models.QuerySet): pass class Category(BaseModel): application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, null=True) label = models.CharField(_('label'), max_length=255) objects = CategoryQuerySet.as_manager() # TODO: move DRF functionalities to view or serializers # class TransactionManager(models.Manager): # def create_transaction( # self, # amount, # payee_id, # payee_type, # payer_id=None, # payer_type=None, # delay=0, # detail=None, # application=None, # uuid=None # ): # tr = self.model( # uuid=uuid, # payer_id=payer_id, # payer_type=payer_type, # payee_id=payee_id, # payee_type=payee_type, # amount=amount, # delay=delay, # application=application, # detail=detail # ) # # tr.save() # return tr class TransactionQuerySet(models.QuerySet): pass class Transaction(BaseModel): amount = models.PositiveBigIntegerField(_('amount'), blank=False, validators=[MinValueValidator(1)]) delay = models.PositiveIntegerField(default=0, blank=True) # number of days for delayed payments _payer_id = models.UUIDField(blank=True, null=True) _payer_type = models.IntegerField(choices=TypeChoices.choices, blank=True, null=True) _payee_id = models.UUIDField(null=True) _payee_type = models.IntegerField(choices=TypeChoices.choices, blank=True, null=True) state = models.IntegerField(choices=StateChoices.choices, default=StateChoices.CREATED) application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, related_name='transactions', null=True) payer_wallet = models.ForeignKey(Wallet, on_delete=models.PROTECT, null=True, related_name='payer_wallet_transactions', db_index=True) payee_wallet = models.ForeignKey(Wallet, on_delete=models.PROTECT, null=True, related_name='payee_wallet_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', # 'reference_id', 'application_details_url'} objects = TransactionQuerySet.as_manager() class Meta: ordering = ['-created_at'] def __str__(self): try: return f"{self.get_payer()} -> {self.get_payee()} ({self.uuid}): {self.amount} - {self.get_state_display()}" except: return str(self.uuid) # unique_together = ('application', 'detail_id',) def get_payer(self): return self.payer_wallet.get_owner() def get_payee(self): return self.payee_wallet.get_owner() @property def payer_id(self): return self.payer_wallet.owner_uuid @property def payer_type(self): return self.payer_wallet.owner_type @property def payee_id(self): return self.payee_wallet.owner_uuid @property def payee_type(self): return self.payee_wallet.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( 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) ######################## category def category_withdraw_from_payer_balance(self): payer_is_allowed_negative_balance = self.payer_wallet.owner_type == TypeChoices.APPLICATION.value and str( self.payer_wallet.owner_uuid) in settings.ALLOWED_NEGATIVE_BALANCE_APPLICATIONS if self.amount > self.payer_wallet.balance and not payer_is_allowed_negative_balance: raise APIException('not enough balance') self.payer_wallet.balance = F('balance') - self.amount self.payer_wallet.save(update_fields=['balance']) def category_deposit_to_payee_balance(self): self.payee_wallet.balance = F('balance') + self.amount self.payee_wallet.save(update_fields=['balance']) def pay(self, payer_id=None, payer_type=None): if not self.payer_id and not (payer_id and payer_type): raise APIException('payer identification error') if self.payer_id and payer_id and not (self.payer_id == payer_id and self.payer_type == payer_type): raise APIException('payer identification error') if not self.state == StateChoices.CREATED: raise APIException('this transaction is not ready to pay') if not self.payer_id: self.payer_id = payer_id self.payer_type = payer_type payer = self.get_payer() if payer.balance < self.amount: raise APIException('not enough balance') with transaction.atomic(): if self.delay: self.change_state( StateChoices.CREATED, StateChoices.DELAYED, error_message='this transaction is not ready to pay', payer_id=payer_id, payer_type=payer_type ) else: self.change_state( StateChoices.CREATED, StateChoices.SUCCESS, error_message='this transaction is not ready to pay', payer_id=payer_id, payer_type=payer_type ) self.category_deposit_to_payee_balance() self.category_withdraw_from_payer_balance() return self def receipt(self): if not self.state == StateChoices.DELAYED: raise APIException('this transaction is not delayed') with transaction.atomic(): self.change_state( StateChoices.DELAYED, StateChoices.SUCCESS, error_message='this transaction is not delayed' ) self.category_deposit_to_payee_balance() return self def category_submit(self): if not self.payer_wallet: raise APIException('payer identification error') if not self.payee_wallet: raise APIException('payee identification error') if not self.state == StateChoices.CREATED: raise APIException('this transaction is not ready to pay') with transaction.atomic(): self.category_withdraw_from_payer_balance() self.change_state( StateChoices.CREATED, StateChoices.PENDING, error_message=_('this transaction is not ready to pay') ) return self def category_verify(self): if not self.state == StateChoices.PENDING: raise APIException('this transaction is not pending') with transaction.atomic(): self.category_deposit_to_payee_balance() self.change_state( StateChoices.PENDING, StateChoices.SUCCESS, error_message=_('this transaction is not pending') ) return self def get_role(self, entity): if self.payer_wallet.owner_uuid == entity.uuid: return 1 elif self.payee_wallet.owner_uuid == entity.uuid: return 2 else: return 0 def update_details(self, force_update=False): if not isinstance(self.details, dict): self.details = {} self.save() # detail = {'payer_name', 'payee_name', 'application_name', 'payer_avatar_url', 'payee_avatar_url', 'application_avatar_url'} is_updated = False if not self.details.get('payer_name'): print('update payer') if self.payer_wallet.owner_type == TypeChoices.USER: payer_details: PublicUser = get_user_details(self.payer_wallet.owner_uuid) self.details['payer_name'] = payer_details.first_name + payer_details.last_name self.details['payer_avatar_url'] = payer_details.avatar else: payer_details = get_application_details(self.payer_id) self.details['payer_name'] = payer_details.name # TODO: get avatar from details is_updated = True print("payer_details:", payer_details) print("is_updated:", is_updated) if not self.details.get('payee_name'): print('update payee') if self.payee_wallet.owner_type == TypeChoices.USER: payee_details: PublicUser = get_user_details(self.payee_wallet.owner_uuid) self.details['payee_name'] = payee_details.first_name + payee_details.last_name self.details['payee_avatar_url'] = payee_details.avatar else: payee_details = get_application_details(self.payee_id) self.details['payee_name'] = payee_details.name # TODO: get avatar from details is_updated = True print("payee_details:", payee_details) print("is_updated:", is_updated) if not self.details.get('application_name'): print('update application') application_details = get_application_details(self.application_id) print('application_details:', application_details) try: self.details['application_name'] = application_details.name # TODO: get avatar from details is_updated = True except Exception as e: print("something went wrong") print(e) if is_updated: print('update details') self.save()