diff --git a/apps/gooyal_oauth2/models.py b/apps/gooyal_oauth2/models.py index 78faa62..1ce7ad3 100644 --- a/apps/gooyal_oauth2/models.py +++ b/apps/gooyal_oauth2/models.py @@ -20,7 +20,7 @@ class AccessToken(AbstractAccessToken): class Application(AbstractApplication): id = None uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4, unique=True, db_index=True) - balance = models.BigIntegerField(_('balance'), default=0) + balance = models.PositiveBigIntegerField(_('balance'), default=0) class Meta: abstract = False diff --git a/apps/users/models.py b/apps/users/models.py index 87d92d5..e94e608 100644 --- a/apps/users/models.py +++ b/apps/users/models.py @@ -61,7 +61,7 @@ class User(AbstractUser): null=True ) last_checkout_request = models.DateTimeField(_('otp expire'), max_length=30, blank=True, null=True) - balance = models.BigIntegerField(_('balance'), default=0) + balance = models.PositiveBigIntegerField(_('balance'), default=0) iban = models.CharField(null=True, max_length=30, blank=True) iban_verified = models.BooleanField(null=True) diff --git a/apps/wallet/migrations/0003_remove_transaction_detail_id_transaction_detail_and_more.py b/apps/wallet/migrations/0003_remove_transaction_detail_id_transaction_detail_and_more.py new file mode 100644 index 0000000..58d03f1 --- /dev/null +++ b/apps/wallet/migrations/0003_remove_transaction_detail_id_transaction_detail_and_more.py @@ -0,0 +1,32 @@ +# Generated by Django 5.0.6 on 2024-08-10 14:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('wallet', '0002_rename_last_update_transaction_updated_at_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='transaction', + name='detail_id', + ), + migrations.AddField( + model_name='transaction', + name='detail', + field=models.JSONField(blank=True, default={}, null=True), + ), + migrations.AlterField( + model_name='transaction', + name='created_at', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='transaction', + name='updated_at', + field=models.DateTimeField(auto_now=True, null=True), + ), + ] diff --git a/apps/wallet/models.py b/apps/wallet/models.py index 97d29c5..b4fe69c 100755 --- a/apps/wallet/models.py +++ b/apps/wallet/models.py @@ -10,40 +10,39 @@ from django.conf import settings from oauth2_provider import settings as oauth2_settings from .constans import StateChoices, TypeChoices from apps.users.models import User +from ..gooyal_oauth2.models import Application +# TODO: move DRF functionalities to view or serializers class TransactionManager(models.Manager): def create_transaction( self, amount, - payee: User, - payer: User = None, + payee, + payee_type, + payer=None, + payer_type=None, delay=0, - # detail_id=None, + detail=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: + if amount <= 0: raise ValidationError('amount should be greater than zero') - if not delay >= 0: + if delay < 0: raise ValidationError('delay should be greater than equal zero') transaction = self.model( uuid=uuid, payer=payer, + payer_type=payer_type, payee=payee, + payee_type=payee_type, amount=amount, delay=delay, - # detail_id=detail_id, - application=application + application=application, + detail=detail ) - # if state: - # transaction.state = state transaction.save() return transaction @@ -61,10 +60,8 @@ class Transaction(models.Model): state = models.IntegerField(choices=StateChoices.choices, default=StateChoices.CREATED) created_at = models.DateTimeField(auto_now_add=True, null=True) updated_at = models.DateTimeField(auto_now=True, null=True) - payer_application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, - related_name='payer_transactions', null=True) - payee_application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, - related_name='payee_transactions', null=True) + application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, + related_name='transactions', null=True) detail = models.JSONField(null=True, blank=True, default={}) objects = TransactionManager() @@ -72,98 +69,132 @@ class Transaction(models.Model): # class Meta: # unique_together = ('application', 'detail_id',) - def pay(self, payer=None): - if not self.payer and not payer: + def withdraw_from_payer_balance(self): + payer = self.get_payer() + payer.balance = F('balance') - self.amount + payer.save(update_fields=['balance']) + + def deposit_to_payee_balance(self): + payee = self.get_payee() + payee.balance = F('balance') + self.amount + payee.save(update_fields=['balance']) + + def get_pay_entity(self, entity_id, entity_type): + if entity_type == TypeChoices.APPLICATION: + entity: Application = Application.objects.get(pk=entity_id) + else: + entity: User = User.objects.get(pk=entity_id) + + return entity + + def get_payer(self): + return self.get_pay_entity(self.payer_id, self.payer_type) + + def get_payee(self): + return self.get_pay_entity(self.payee_id, self.payee_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) + + 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 and payer and not self.payer == payer: + 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') - # TODO: check bundry conditions if not self.state == StateChoices.CREATED: raise APIException('this transaction is not ready to pay') - if not self.payer: - self.payer = payer + 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(): - self.updated_at = 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 = StateChoices.DELAYED - self.save() + 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.payee.balance = F('balance') + self.amount - self.payee.save(update_fields=['balance']) + 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.deposit_to_payee_balance() - self.state = StateChoices.SUCCESS - self.save() - - self.save() + self.withdraw_from_payer_balance() return self def receipt(self): - # TODO: check bundry conditions if not self.state == StateChoices.DELAYED: raise APIException('this transaction is not delayed') with transaction.atomic(): - self.updated_at = datetime.datetime.now() - self.payee.balance = F('balance') + self.amount - self.state = StateChoices.SUCCESS - self.payee.save() - self.save() + self.change_state( + StateChoices.DELAYED, + StateChoices.SUCCESS, + error_message='this transaction is not delayed' + ) + self.deposit_to_payee_balance() + return self def submit(self): - if not self.payer: + payer = self.get_payer() + payee = self.get_payee() + + if not payer: raise APIException('payer identification error') - if not self.payee: + if not payee: raise APIException('payee identification error') - # TODO: check bundry conditions if not self.state == StateChoices.CREATED: raise APIException('this transaction is not ready to pay') with transaction.atomic(): - self.updated_at = 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 = StateChoices.PENDING - if self.payer.balance < 0: - raise APIException('not enough balance') - - self.save() + self.withdraw_from_payer_balance() + self.change_state( + StateChoices.CREATED, + StateChoices.PENDING, + error_message='this transaction is not ready to pay' + ) return self def verify(self): - # TODO: check bundry conditions if not self.state == StateChoices.PENDING: raise APIException('this transaction is not pending') with transaction.atomic(): - self.updated_at = datetime.datetime.now() - self.payee.balance = F('balance') + self.amount - self.state = StateChoices.SUCCESS - self.payee.save() - self.save() + self.deposit_to_payee_balance() + self.change_state( + StateChoices.PENDING, + StateChoices.SUCCESS, + error_message='this transaction is not pending' + ) + return self def __str__(self): - return f"{self.payer} -> {self.payee} ({self.uuid}): {self.amount}" + return f"{self.get_payer()} -> {self.get_payee()} ({self.uuid}): {self.amount}"