From ba876e2874ca51b6f243103f549100408e9c6d8b Mon Sep 17 00:00:00 2001 From: Sayyid Hamid Mahdavi Date: Sat, 11 Oct 2025 17:02:06 +0330 Subject: [PATCH] application deposit with currency --- apps/gooyal_oauth2/models.py | 2 +- apps/wallet/models.py | 23 ++++++++++++++++++++--- apps/wallet/serializers.py | 6 +++++- apps/wallet/views/application.py | 25 ++++++++++++++++++------- utils/exceptions.py | 6 ++++++ wallet/settings.py | 3 ++- 6 files changed, 52 insertions(+), 13 deletions(-) diff --git a/apps/gooyal_oauth2/models.py b/apps/gooyal_oauth2/models.py index a500c70..c3709bc 100644 --- a/apps/gooyal_oauth2/models.py +++ b/apps/gooyal_oauth2/models.py @@ -22,7 +22,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.BigIntegerField(_('balance'), default=0) # todo: remove it class Meta: abstract = False diff --git a/apps/wallet/models.py b/apps/wallet/models.py index e899f91..7bceb42 100755 --- a/apps/wallet/models.py +++ b/apps/wallet/models.py @@ -3,10 +3,11 @@ 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 rest_framework.exceptions import APIException from django.conf import settings from oauth2_provider import settings as oauth2_settings +from utils.exceptions import UnprocessableEntity from gooyal_accounts_client.models import PublicUser from utils.accounts_client import get_user_details, get_application_details from utils.models import BaseModel @@ -15,6 +16,10 @@ from apps.users.models import User from ..gooyal_oauth2.models import Application +class CurrencyChoices(models.TextChoices): + IRANIAN_RIAL = '166ab7ce33b34799950d45035a9997d6', _('IRANIAN RIAL') # uuid without dash + + class WalletQuerySet(models.QuerySet): pass @@ -40,8 +45,8 @@ class CategoryQuerySet(models.QuerySet): class Category(BaseModel): application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, null=True) - label = models.CharField(_('label'), max_length=255) + currency = models.UUIDField(choices=CurrencyChoices.choices, db_index=True, default=CurrencyChoices.IRANIAN_RIAL) objects = CategoryQuerySet.as_manager() @@ -120,6 +125,14 @@ class Transaction(BaseModel): def get_payee(self): return self.payee_wallet.get_owner() + @property + def payer_wallet_category(self): + return self.payer_wallet and self.payer_wallet.category_id + + @property + def payee_wallet_category(self): + return self.payee_wallet and self.payee_wallet.category_id + @property def payer_id(self): return self.payer_wallet.owner_uuid @@ -154,7 +167,7 @@ class Transaction(BaseModel): 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') + raise UnprocessableEntity('not enough balance') self.payer_wallet.balance = F('balance') - self.amount self.payer_wallet.save(update_fields=['balance']) @@ -224,6 +237,10 @@ class Transaction(BaseModel): if not self.payee_wallet: raise APIException('payee identification error') + if not self.payer_wallet.category == self.payee_wallet.category: + self.change_state(from_state=self.state, to_state=StateChoices.FAILED) + raise APIException('payer and payee wallet currency not equal') + if not self.state == StateChoices.CREATED: raise APIException('this transaction is not ready to pay') diff --git a/apps/wallet/serializers.py b/apps/wallet/serializers.py index 8d5b669..b40a43f 100755 --- a/apps/wallet/serializers.py +++ b/apps/wallet/serializers.py @@ -174,8 +174,10 @@ class ApplicationTransactionSerializer(BaseTransactionSerializer): pass class ApplicationDepositSerializer(serializers.ModelSerializer): + uuid = serializers.UUIDField(required=False, allow_null=None) payee_id = serializers.UUIDField(required=True) payee_type = serializers.IntegerField(required=True) + payee_wallet_category = serializers.UUIDField(required=True) amount = serializers.IntegerField(required=True, validators=[MinValueValidator(1)]) # TODO: details as fields @@ -185,8 +187,10 @@ class ApplicationDepositSerializer(serializers.ModelSerializer): 'uuid', 'payer_id', 'payer_type', + 'payer_wallet_category', 'payee_id', 'payee_type', + 'payee_wallet_category', "created_at", "state", 'amount', @@ -195,7 +199,7 @@ class ApplicationDepositSerializer(serializers.ModelSerializer): 'details', ) - read_only_fields = ['updated_at', 'state', 'created_at', 'payer_id', 'payer_type'] + read_only_fields = ['updated_at', 'state', 'created_at', 'payer_id', 'payer_type', 'payer_wallet_category'] class ApplicationWithdrawSerializer(serializers.ModelSerializer): diff --git a/apps/wallet/views/application.py b/apps/wallet/views/application.py index 3811029..a81cc39 100755 --- a/apps/wallet/views/application.py +++ b/apps/wallet/views/application.py @@ -13,6 +13,7 @@ from apps.wallet.constans import TypeChoices, StateChoices from apps.wallet.models import Transaction, Category, Wallet from apps.wallet.serializers import ApplicationDepositSerializer, ApplicationWithdrawSerializer, \ ApplicationTransactionSerializer, ApplicationBalanceSerializer, MyWalletSerializer +from utils.exceptions import UnprocessableEntity logger = logging.getLogger(__name__) @@ -277,16 +278,26 @@ class CategoryDepositSubmitAPIView(generics.CreateAPIView): # TODO: get_or_create instead of create if uuid given # TODO: it is possible to merage create and submit in one step for improve performance application = get_application(self.request) - category_uuid = self.kwargs.get('category_uuid') - if category_uuid: - category = get_object_or_404(Category.objects.all(), uuid=category_uuid) + payee_wallet_category_uuid = serializer.validated_data.pop('payee_wallet_category') + payee_wallet_category_uuid = payee_wallet_category_uuid and str(payee_wallet_category_uuid) + + payer_wallet_category_uuid = self.kwargs.get('category_uuid') + + if payer_wallet_category_uuid: + payer_wallet_category = get_object_or_404(Category.objects.all(), uuid=payer_wallet_category_uuid) else: - category = None + payer_wallet_category = None + + payee_wallet_category = get_object_or_404(Category.objects.all(), uuid=payee_wallet_category_uuid) + + if not(payee_wallet_category and payee_wallet_category.currency == payer_wallet_category.currency): + raise UnprocessableEntity('payer and payee wallet currency not equal') + payee_id = serializer.validated_data.pop('payee_id') payee_type = serializer.validated_data.pop('payee_type') payer_wallet, _ = Wallet.objects.get_or_create(owner_uuid=application.uuid, owner_type=TypeChoices.APPLICATION, - category=category) - payee_wallet, _ = Wallet.objects.get_or_create(owner_uuid=payee_id, owner_type=payee_type, category=category) + category=payer_wallet_category) + payee_wallet, _ = Wallet.objects.get_or_create(owner_uuid=payee_id, owner_type=payee_type, category=payee_wallet_category) instance = serializer.save(payer_wallet=payer_wallet, application=application, payee_wallet=payee_wallet) instance.category_submit() @@ -371,7 +382,7 @@ class CategoryWithdrawVerifyAPIView(generics.RetrieveAPIView): def retrieve(self, request, *args, **kwargs): instance = self.get_object() if instance.state == StateChoices.PENDING: - instance.category_verify() + instance.verify() serializer = self.get_serializer(instance) return Response(serializer.data) diff --git a/utils/exceptions.py b/utils/exceptions.py index f0b7e9c..4c7a022 100644 --- a/utils/exceptions.py +++ b/utils/exceptions.py @@ -2,6 +2,7 @@ import logging from rest_framework.exceptions import APIException from rest_framework.views import exception_handler as drf_exception_handler +from django.utils.translation import gettext_lazy as _ logger = logging.getLogger(__name__) @@ -52,3 +53,8 @@ class ErrorMiddleware: }, status=500 ) + +class UnprocessableEntity(APIException): + status_code = 422 + default_detail = _('The request was well-formed but cannot be processed due to semantic errors.') + default_code = _('unprocessable_entity') diff --git a/wallet/settings.py b/wallet/settings.py index 31cabab..19386d1 100644 --- a/wallet/settings.py +++ b/wallet/settings.py @@ -262,4 +262,5 @@ CACHES = { } } -ALLOWED_NEGATIVE_BALANCE_APPLICATIONS = config('ALLOWED_NEGATIVE_BALANCE_APPLICATIONS', cast=lambda v: [s.strip() for s in v.split(',')]) \ No newline at end of file +ALLOWED_NEGATIVE_BALANCE_APPLICATIONS = config('ALLOWED_NEGATIVE_BALANCE_APPLICATIONS', cast=lambda v: [s.strip() for s in v.split(',')]) +DEFAULT_WALLET_CATEGORY_UUID = 'af7d967f-30c0-409b-9066-2549f2da5e5e' \ No newline at end of file