From cbcc60b78c4a34020bfa2163c9626242e81c5489 Mon Sep 17 00:00:00 2001 From: Sayyid Hamid Mahdavi Date: Tue, 11 Mar 2025 13:24:50 +0330 Subject: [PATCH] application apis --- .../0005_alter_application_balance.py | 18 +++ apps/gooyal_oauth2/models.py | 2 +- .../migrations/0006_alter_transaction_uuid.py | 19 +++ apps/wallet/models.py | 17 ++ apps/wallet/serializers.py | 73 +++++++-- apps/wallet/urls.py | 29 ++-- apps/wallet/views/application.py | 151 ++++++++++++------ apps/wallet/views/user.py | 12 +- readme.rst | 17 ++ .../include/inline_transaction_data.html | 2 +- templates/include/top_bar.html | 2 +- 11 files changed, 263 insertions(+), 79 deletions(-) create mode 100644 apps/gooyal_oauth2/migrations/0005_alter_application_balance.py create mode 100644 apps/wallet/migrations/0006_alter_transaction_uuid.py create mode 100644 readme.rst diff --git a/apps/gooyal_oauth2/migrations/0005_alter_application_balance.py b/apps/gooyal_oauth2/migrations/0005_alter_application_balance.py new file mode 100644 index 0000000..3fea539 --- /dev/null +++ b/apps/gooyal_oauth2/migrations/0005_alter_application_balance.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.6 on 2025-03-11 09:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('gooyal_oauth2', '0004_alter_application_balance'), + ] + + operations = [ + migrations.AlterField( + model_name='application', + name='balance', + field=models.BigIntegerField(default=0, verbose_name='balance'), + ), + ] diff --git a/apps/gooyal_oauth2/models.py b/apps/gooyal_oauth2/models.py index d620904..a500c70 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.PositiveBigIntegerField(_('balance'), default=0) + balance = models.BigIntegerField(_('balance'), default=0) class Meta: abstract = False diff --git a/apps/wallet/migrations/0006_alter_transaction_uuid.py b/apps/wallet/migrations/0006_alter_transaction_uuid.py new file mode 100644 index 0000000..d243b6b --- /dev/null +++ b/apps/wallet/migrations/0006_alter_transaction_uuid.py @@ -0,0 +1,19 @@ +# Generated by Django 5.0.6 on 2025-03-11 09:49 + +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('wallet', '0005_remove_transaction_detail_transaction_details_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='transaction', + name='uuid', + field=models.UUIDField(db_index=True, default=uuid.uuid4, primary_key=True, serialize=False, unique=True), + ), + ] diff --git a/apps/wallet/models.py b/apps/wallet/models.py index 4f2ebb0..379e768 100755 --- a/apps/wallet/models.py +++ b/apps/wallet/models.py @@ -60,6 +60,10 @@ class Transaction(models.Model): related_name='transactions', null=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 = TransactionManager() # class Meta: @@ -194,5 +198,18 @@ class Transaction(models.Model): ) return self + def get_role(self, entity): + + if self.payer_id == entity.uuid: + return 1 + elif self.payee_id == entity.uuid: + return 2 + else: + return 0 + + def update_details(self, force_update=False): + # detail = {'payer_name', 'payee_name', 'application_name', 'payer_avatar_url', 'payee_avatar_url'} + pass + def __str__(self): return f"{self.get_payer()} -> {self.get_payee()} ({self.uuid}): {self.amount} - {self.get_state_display()}" diff --git a/apps/wallet/serializers.py b/apps/wallet/serializers.py index 647a65d..d824fa9 100755 --- a/apps/wallet/serializers.py +++ b/apps/wallet/serializers.py @@ -5,10 +5,11 @@ from rest_framework.exceptions import APIException from .constans import TypeChoices from .models import Transaction +from ..gooyal_oauth2.models import Application from ..users.models import User -class TransactionSerializer(serializers.ModelSerializer): +class BaseTransactionSerializer(serializers.ModelSerializer): payee_id = serializers.UUIDField(read_only=True) payee_type = serializers.UUIDField(read_only=True) role = serializers.SerializerMethodField() @@ -31,24 +32,17 @@ class TransactionSerializer(serializers.ModelSerializer): read_only_fields = ['uuid', 'updated_at', 'state', 'created_at', 'details', 'role'] + + +class UserTransactionSerializer(BaseTransactionSerializer): def get_role(self, obj: Transaction) -> int: request = self.context.get('request') user = request.user - if obj.payer_id == user.uuid: - return 1 - elif obj.payee_id == user.uuid: - return 2 - else: - return 0 + return obj.get_role(user) - # def create(self, validated_data): - # transaction = Transaction.objects.create_transaction(**validated_data) - # return transaction - - class DepositSerializer(serializers.ModelSerializer): uuid = serializers.UUIDField(required=True) @@ -161,3 +155,58 @@ class TransferVerifySerializer(serializers.ModelSerializer): read_only_fields = ['uuid', 'payer_id', 'payer_type', 'payee_id', 'payee_type', 'updated_at', 'state', 'amount', 'created_at'] + + +#### Application + +class ApplicationBalanceSerializer(serializers.ModelSerializer): + class Meta: + model = Application + fields = ['balance'] + + +class ApplicationTransactionSerializer(BaseTransactionSerializer): + pass + +class ApplicationDepositSerializer(serializers.ModelSerializer): + uuid = serializers.UUIDField(required=True) + + class Meta: + model = Transaction + fields = ( + 'uuid', + 'payer_id', + 'payer_type', + 'payee_id', + 'payee_type', + "created_at", + "state", + 'amount', + # 'delay', + 'updated_at', + 'details', + ) + + read_only_fields = ['updated_at', 'state', 'created_at', 'payer_id', 'payer_type', 'details'] + + +class ApplicationWithdrawSerializer(serializers.ModelSerializer): + uuid = serializers.UUIDField(required=True) + + class Meta: + model = Transaction + fields = ( + 'uuid', + 'payer_id', + 'payer_type', + 'payee_id', + 'payee_type', + "created_at", + "state", + 'amount', + # 'delay', + 'updated_at', + 'details', + ) + + read_only_fields = ['updated_at', 'state', 'created_at', 'payee_id', 'payee_type', 'details'] diff --git a/apps/wallet/urls.py b/apps/wallet/urls.py index 6f701c6..b2abcf8 100644 --- a/apps/wallet/urls.py +++ b/apps/wallet/urls.py @@ -5,12 +5,10 @@ from .views import user as user_views app_name = "wallet" -urlpatterns = [ - path('transactions/', user_views.TransactionListView.as_view(), name='user_transaction_list'), - path('transactions/', user_views.TransactionDetailView.as_view(), name='user_transaction_detail'), - # - path('api/wallet/balance', user_views.MyWalletBalanceView.as_view(), name='wallet_balance_api'), - # +user_urlpatterns = [ + path('transactions/', user_views.TransactionListView.as_view(), name='user_transaction_list_page'), + path('transactions/', user_views.TransactionDetailView.as_view(), name='user_transaction_detail_page'), + path('api/wallet/balance', user_views.MyWalletBalanceView.as_view(), name='user_wallet_balance_api'), path('api/transactions/', user_views.TransactionList.as_view(), name='transaction_list_api'), # path('api/transactions/', TransactionDetail.as_view(), name='transaction_detail_api'), # path('api/transactions//pay', TransactionPay.as_view(), name='transaction_pay_api'), @@ -23,8 +21,19 @@ urlpatterns = [ # path('api/withdraw//verify', WithdrawVerify.as_view(), name='withdraw_verify_api'), # path('api/get_balance/', application_views.WalletBalanceView.as_view(), name='wallet_balance'), - path('api/transaction/transfer_submit/', application_views.TransferSubmitView.as_view(), name='transfer_submit'), - path('api/transaction//transfer_verify/', application_views.TransferVerifyView.as_view(), name='transfer_verify'), - path('api/transaction//', application_views.TransactionDetailView.as_view(), name='transaction_detail'), -] \ No newline at end of file +] + +application_urlpatterns = [ + path('api/application/balance', application_views.BalanceAPIView.as_view(), name='application_wallet_balance_api'), + path('api/application/', application_views.TransactionListAPIView.as_view(), name='application_transaction_list_api'), + path('api/application//', application_views.TransactionDetailAPIView.as_view(), name='application_transaction_detail'), + + path('api/application/deposit/', application_views.DepositSubmitAPIView.as_view(), name='application_deposit_submit_api'), + path('api/application/deposit//verify', application_views.DepositVerifyAPIView.as_view(), name='application_deposit_verify_api'), + + path('api/application/withdraw/', application_views.WithdrawSubmitAPIView.as_view(), name='application_withdraw_submit_api'), + path('api/application/withdraw//verify', application_views.WithdrawVerifyAPIView.as_view(), name='application_withdraw_verify_api'), +] + +urlpatterns = user_urlpatterns + application_urlpatterns \ No newline at end of file diff --git a/apps/wallet/views/application.py b/apps/wallet/views/application.py index cb79055..6c5729f 100755 --- a/apps/wallet/views/application.py +++ b/apps/wallet/views/application.py @@ -1,16 +1,12 @@ from django.db.models import Q -from django.http import Http404 -from django.shortcuts import get_object_or_404 from oauth2_provider.contrib.rest_framework import TokenHasScope, IsAuthenticatedOrTokenHasScope from rest_framework import generics, permissions, mixins from rest_framework.response import Response -from apps.gooyal_oauth2.models import Application -from apps.users.models import User -from apps.wallet.constans import TypeChoices +from apps.wallet.constans import TypeChoices, StateChoices from apps.wallet.models import Transaction -from apps.wallet.serializers import TransactionSerializer, WalletBalanceSerializer, TransferSubmitSerializer, \ - TransferVerifySerializer +from apps.wallet.serializers import ApplicationDepositSerializer, ApplicationWithdrawSerializer, \ + ApplicationTransactionSerializer, ApplicationBalanceSerializer def get_application(request): @@ -21,69 +17,128 @@ def get_application(request): return application -class WalletBalanceView(generics.RetrieveAPIView): +class BalanceAPIView(generics.RetrieveAPIView): ''' API for getting specified user or application balance. ''' permission_classes = [TokenHasScope] - serializer_class = WalletBalanceSerializer + serializer_class = ApplicationBalanceSerializer required_scopes = ['wallet.application:get_balance'] def get_object(self): - serializer = self.get_serializer(data=self.request.data) - serializer.is_valid(raise_exception=True) - instance_id = serializer.data.get('instance_id') - instance_type = serializer.data.get('instance_type') - if instance_type == TypeChoices.USER: - queryset = User.objects.filter(pk=instance_id) - elif instance_type == TypeChoices.APPLICATION: - queryset = Application.objects.filter(pk=instance_id) - else: - raise Http404( - "No object matches the given query." - ) - - instance = get_object_or_404(queryset) - return instance - - def retrieve(self, request, *args, **kwargs): - return super().retrieve(request, *args, **kwargs) + return get_application(self.request) -class TransactionDetailView(generics.RetrieveAPIView): +class TransactionDetailAPIView(generics.RetrieveAPIView): permission_classes = [TokenHasScope] - serializer_class = TransactionSerializer + serializer_class = ApplicationTransactionSerializer lookup_field = 'uuid' - required_scopes = ['wallet.application:transaction_detail'] + required_scopes = ['wallet.application:get_transaction_detail'] + + def get_queryset(self): + application = get_application(self.request) + return Transaction.objects.filter(Q(application=application) | + Q(payer_id=application.pk, payer_type=TypeChoices.APPLICATION.value) | + Q(payee_id=application.pk, payee_type=TypeChoices.APPLICATION.value) + ).all() -class TransferSubmitView(generics.CreateAPIView): +class TransactionListAPIView(generics.ListAPIView): permission_classes = [TokenHasScope] - serializer_class = TransferSubmitSerializer + serializer_class = ApplicationTransactionSerializer lookup_field = 'uuid' - required_scopes = ['wallet.application:transfer_submit'] + required_scopes = ['wallet.application:get_transaction_list'] + + def get_queryset(self): + application = get_application(self.request) + return Transaction.objects.filter(Q(application=application) | + Q(payer_id=application.pk, payer_type=TypeChoices.APPLICATION.value) | + Q(payee_id=application.pk, payee_type=TypeChoices.APPLICATION.value) + ).all() + +# from application to another entity +# TODO: better name +class DepositSubmitAPIView(generics.CreateAPIView): + permission_classes = [TokenHasScope] + serializer_class = ApplicationDepositSerializer + required_scopes = ['wallet.application.deposit:submit'] + + def get_queryset(self): + application = get_application(self.request) + return Transaction.objects.filter(application=application, + payer_id=application.pk, + payer_type=TypeChoices.APPLICATION.value).all() def perform_create(self, serializer): - # TODO: performance issue + # 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) - try: - instance:Transaction = serializer.save() - except Exception as e: - raise APIException(e) - - instance.application = application - instance.save() + instance = serializer.save(payer_id=application.pk, + payer_type=TypeChoices.APPLICATION.value, + application=application) instance.submit() -class TransferVerifyView(generics.UpdateAPIView): +class DepositVerifyAPIView(generics.RetrieveAPIView): permission_classes = [TokenHasScope] - serializer_class = TransferVerifySerializer + serializer_class = ApplicationDepositSerializer lookup_field = 'uuid' - required_scopes = ['wallet.application:transfer_verify'] - queryset = Transaction.objects.all() + required_scopes = ['wallet.application.deposit:verify'] - def perform_update(self, serializer): - instance:Transaction = serializer.save() - instance.verify() + def get_queryset(self): + application = get_application(self.request) + return Transaction.objects.filter(application=application, + payer_id=application.pk, + payer_type=TypeChoices.APPLICATION.value).all() + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + if instance.state == StateChoices.PENDING: + instance.verify() + + serializer = self.get_serializer(instance) + return Response(serializer.data) + + +# from another entity to application +class WithdrawSubmitAPIView(generics.CreateAPIView): + permission_classes = [TokenHasScope] + serializer_class = ApplicationWithdrawSerializer + required_scopes = ['wallet.application.withdraw:submit'] + + def get_queryset(self): + application = get_application(self.request) + return Transaction.objects.filter(application=application, + payee_id=application.pk, + payee_type=TypeChoices.APPLICATION.value).all() + + def perform_create(self, serializer): + # 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) + instance = serializer.save(payee_id=application.pk, + payee_type=TypeChoices.APPLICATION.value, + application=application) + + instance.submit() + + +class WithdrawVerifyAPIView(generics.RetrieveAPIView): + permission_classes = [TokenHasScope] + serializer_class = ApplicationWithdrawSerializer + lookup_field = 'uuid' + required_scopes = ['wallet.application.withdraw:verify'] + + def get_queryset(self): + application = get_application(self.request) + return Transaction.objects.filter(application=application, + payee_id=application.pk, + payee_type=TypeChoices.APPLICATION.value).all() + + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + if instance.state == StateChoices.PENDING: + instance.verify() + + serializer = self.get_serializer(instance) + return Response(serializer.data) diff --git a/apps/wallet/views/user.py b/apps/wallet/views/user.py index 58f0735..8bba8f1 100755 --- a/apps/wallet/views/user.py +++ b/apps/wallet/views/user.py @@ -12,7 +12,7 @@ from rest_framework.response import Response from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements from apps.wallet.models import Transaction -from apps.wallet.serializers import TransactionSerializer, DepositSerializer, WithdrawSerializer, \ +from apps.wallet.serializers import UserTransactionSerializer, DepositSerializer, WithdrawSerializer, \ MyWalletSerializer from ..constans import StateChoices, TypeChoices from ..forms import TransactionActionForm @@ -50,10 +50,10 @@ class TransactionList(generics.ListAPIView): ''' permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements] - serializer_class = TransactionSerializer + serializer_class = UserTransactionSerializer required_alternate_scopes = { "GET": [['wallet.user:transaction_list']], - "POST": [['wallet.invoice:create']], + # "POST": [['wallet.invoice:create']], } def get_queryset(self): @@ -70,7 +70,7 @@ class TransactionList(generics.ListAPIView): class TransactionDetail(generics.RetrieveAPIView): permission_classes = [permissions.IsAuthenticated, IsAuthenticatedOrTokenHasScope] - serializer_class = TransactionSerializer + serializer_class = UserTransactionSerializer lookup_field = 'uuid' required_scopes = ['wallet.transaction:retrieve'] @@ -81,7 +81,7 @@ class TransactionDetail(generics.RetrieveAPIView): class TransactionPay(generics.RetrieveAPIView): permission_classes = [permissions.IsAuthenticated, IsAuthenticatedOrTokenHasScope] - serializer_class = TransactionSerializer + serializer_class = UserTransactionSerializer lookup_field = 'uuid' required_scopes = ['wallet.invoice:pay'] @@ -98,7 +98,7 @@ class TransactionPay(generics.RetrieveAPIView): class TransactionReceipt(generics.RetrieveAPIView): permission_classes = [permissions.IsAuthenticated, IsAuthenticatedOrTokenHasScope] - serializer_class = TransactionSerializer + serializer_class = UserTransactionSerializer lookup_field = 'uuid' required_scopes = ['wallet.invoice:receipt'] diff --git a/readme.rst b/readme.rst new file mode 100644 index 0000000..c48dfa2 --- /dev/null +++ b/readme.rst @@ -0,0 +1,17 @@ +دو ماهیت داریم: + * کاربر + * اپلیکیشن + +در هر کدام بالانس در جایی مربوط به خود نگهداری می شود. + +این موارد برای هر یک قابل تعریف است: + +* مشاهده بالانس دیگران برای اپلیکیشن ؟؟ +* مشاهده بالانس برای هر ماهیت +* لیست تراکنش ها برای هر ماهیت +* درخواست صدور فاکتور برای هر ماهیت +* پرداخت فاکتور توسط کاربر(برداشت از حساب در پرداخت امن) +* تایید انتقال پول به گیرنده (در پرداخت امن) +* واریز مستقیم پول توسط هر ماهیت به ماهیت دیگر +* برداشت پول از حساب هر ماهیت توست اپلیکیشن + diff --git a/templates/include/inline_transaction_data.html b/templates/include/inline_transaction_data.html index 2f3810b..bcc2ebd 100644 --- a/templates/include/inline_transaction_data.html +++ b/templates/include/inline_transaction_data.html @@ -7,7 +7,7 @@ amount: - {{ transaction.amount }} + {{ transaction.amount }}

diff --git a/templates/include/top_bar.html b/templates/include/top_bar.html index dbe3244..a054538 100644 --- a/templates/include/top_bar.html +++ b/templates/include/top_bar.html @@ -16,7 +16,7 @@