application deposit with currency

This commit is contained in:
Sayyid Hamid Mahdavi 2025-10-11 17:02:06 +03:30
parent 1443658656
commit ba876e2874
6 changed files with 52 additions and 13 deletions

View file

@ -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

View file

@ -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')

View file

@ -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):

View file

@ -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)

View file

@ -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')

View file

@ -262,4 +262,5 @@ CACHES = {
}
}
ALLOWED_NEGATIVE_BALANCE_APPLICATIONS = config('ALLOWED_NEGATIVE_BALANCE_APPLICATIONS', cast=lambda v: [s.strip() for s in v.split(',')])
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'