diff --git a/apps/wallet/migrations/0011_category_currency.py b/apps/wallet/migrations/0011_category_currency.py new file mode 100644 index 0000000..57e5fa0 --- /dev/null +++ b/apps/wallet/migrations/0011_category_currency.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.7 on 2025-10-11 13:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('wallet', '0010_rename_payee_id_transaction__payee_id_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='category', + name='currency', + field=models.UUIDField(choices=[('166ab7ce33b34799950d45035a9997d6', 'IRANIAN RIAL')], db_index=True, default='166ab7ce33b34799950d45035a9997d6'), + ), + ] 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 635c7f0..cbea979 100755 --- a/apps/wallet/serializers.py +++ b/apps/wallet/serializers.py @@ -177,6 +177,7 @@ class ApplicationDepositSerializer(serializers.ModelSerializer): uuid = serializers.UUIDField(required=False, validators=[]) payee_id = serializers.UUIDField(required=True) payee_type = serializers.IntegerField(required=True) + payee_wallet_category = serializers.UUIDField(required=False) # TODO: required True amount = serializers.IntegerField(required=True, validators=[MinValueValidator(1)]) # TODO: details as fields @@ -186,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', @@ -196,12 +199,14 @@ 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): + uuid = serializers.UUIDField(required=False, validators=[]) payer_id = serializers.UUIDField(required=True) payer_type = serializers.IntegerField(required=True) + payer_wallet_category = serializers.UUIDField(required=False) # TODO: required True amount = serializers.IntegerField(required=True, validators=[MinValueValidator(1)]) class Meta: @@ -210,8 +215,10 @@ class ApplicationWithdrawSerializer(serializers.ModelSerializer): 'uuid', 'payer_id', 'payer_type', + 'payer_wallet_category', 'payee_id', 'payee_type', + 'payee_wallet_category', "created_at", "state", 'amount', @@ -220,4 +227,4 @@ class ApplicationWithdrawSerializer(serializers.ModelSerializer): 'details', ) - read_only_fields = ['updated_at', 'state', 'created_at', 'payee_id', 'payee_type'] + read_only_fields = ['updated_at', 'state', 'created_at', 'payee_id', 'payee_type', 'payee_wallet_category'] diff --git a/apps/wallet/views/application.py b/apps/wallet/views/application.py index e73b38e..9ca6847 100755 --- a/apps/wallet/views/application.py +++ b/apps/wallet/views/application.py @@ -13,7 +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 wallet.exceptions import UnprocessableEntity +from utils.exceptions import UnprocessableEntity from django.utils.translation import gettext_lazy as _ logger = logging.getLogger(__name__) @@ -281,19 +281,31 @@ class CategoryDepositSubmitAPIView(generics.CreateAPIView): logger.info("Starting perform_create for Transaction") application = get_application(self.request) logger.debug(f"Application UUID: {application.uuid}") - 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) + category=payer_wallet_category) logger.debug(f"Payer wallet: {payer_wallet.uuid}") - payee_wallet, _ = Wallet.objects.get_or_create(owner_uuid=payee_id, owner_type=payee_type, category=category) + payee_wallet, _ = Wallet.objects.get_or_create(owner_uuid=payee_id, owner_type=payee_type, category=payee_wallet_category) logger.debug(f"Payee wallet: {payee_wallet.uuid}") uuid = serializer.validated_data.pop('uuid') + details = serializer.validated_data.get('details', {}) reference_id = details.get('reference_id', None) logger.debug(f"Transaction UUID: {uuid}, Reference ID: {reference_id}") @@ -370,16 +382,25 @@ class CategoryWithdrawSubmitAPIView(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) + payer_wallet_category_uuid = serializer.validated_data.pop('payer_wallet_category') + payer_wallet_category_uuid = payer_wallet_category_uuid and str(payer_wallet_category_uuid) + + payee_wallet_category_uuid = self.kwargs.get('category_uuid') + if payee_wallet_category_uuid: + payee_wallet_category = get_object_or_404(Category.objects.all(), uuid=payee_wallet_category_uuid) else: - category = None + payee_wallet_category = None + + payer_wallet_category = get_object_or_404(Category.objects.all(), uuid=payer_wallet_category_uuid) + + if not(payer_wallet_category and payer_wallet_category.currency == payee_wallet_category.currency): + raise UnprocessableEntity('payer and payee wallet currency not equal') + payer_id = serializer.validated_data.pop('payer_id') payer_type = serializer.validated_data.pop('payer_type') payee_wallet, _ = Wallet.objects.get_or_create(owner_uuid=application.uuid, owner_type=TypeChoices.APPLICATION, - category=category) - payer_wallet, _ = Wallet.objects.get_or_create(owner_uuid=payer_id, owner_type=payer_type, category=category) + category=payee_wallet_category) + payer_wallet, _ = Wallet.objects.get_or_create(owner_uuid=payer_id, owner_type=payer_type, category=payer_wallet_category) instance = serializer.save(payer_wallet=payer_wallet, application=application, payee_wallet=payee_wallet) instance.category_submit() 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')