diff --git a/apps/gooyal_oauth2/models.py b/apps/gooyal_oauth2/models.py index 1ce7ad3..d620904 100644 --- a/apps/gooyal_oauth2/models.py +++ b/apps/gooyal_oauth2/models.py @@ -5,6 +5,8 @@ from oauth2_provider.models import AbstractApplication, AbstractAccessToken, Abs import uuid from django.utils.translation import gettext_lazy as _ +from apps.wallet.constans import TypeChoices + class AccessToken(AbstractAccessToken): id = None @@ -25,6 +27,14 @@ class Application(AbstractApplication): class Meta: abstract = False + @property + def instance_id(self): + return self.pk + + @property + def instance_type(self): + return TypeChoices.APPLICATION + class Grant(AbstractGrant): id = None diff --git a/apps/users/models.py b/apps/users/models.py index e94e608..97e408b 100644 --- a/apps/users/models.py +++ b/apps/users/models.py @@ -12,6 +12,7 @@ import string import base64 import hashlib +from apps.wallet.constans import TypeChoices class UserManager(BaseUserManager): @@ -75,6 +76,14 @@ class User(AbstractUser): def __str__(self): return str(self.username or self.pk) + @property + def instance_id(self): + return self.pk + + @property + def instance_type(self): + return TypeChoices.APPLICATION + # TODO: user redis instead. # TODO: create a queryset diff --git a/apps/wallet/application_views.py b/apps/wallet/application_views.py deleted file mode 100755 index ede6018..0000000 --- a/apps/wallet/application_views.py +++ /dev/null @@ -1,74 +0,0 @@ -from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.db.models import Q -from django.urls import reverse -from django.utils.decorators import method_decorator -from django.views.generic import DetailView, ListView, UpdateView - -from oauth2_provider.contrib.rest_framework import TokenHasScope, IsAuthenticatedOrTokenHasScope -from rest_framework import generics, permissions, mixins -from rest_framework.permissions import AllowAny -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet, ModelViewSet - -from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements, \ - IsAuthenticatedOrActionMatchesOASRequirements -from apps.wallet.models import Transaction -from apps.wallet.serializers import TransactionSerializer, DepositSerializer, WithdrawSerializer, WalletSerializer -from .constans import StateChoices -from .forms import TransactionActionForm - -from django.utils.translation import gettext_lazy as _ - - -def get_application(request): - try: - application = request.auth.application - except: - application = None - return application - - -class ApplicationWalletBalanceView(generics.RetrieveAPIView): - ''' - API for getting authenticated user balance. - ''' - permission_classes = [TokenHasScope] - serializer_class = WalletSerializer - required_scopes = ['wallet.application:get_balance'] - - def get_object(self): - return self.request.user - - def retrieve(self, request, *args, **kwargs): - return super().retrieve(request, *args, **kwargs) - - -class TransactionDetail(generics.RetrieveAPIView): - permission_classes = [permissions.IsAuthenticated, IsAuthenticatedOrTokenHasScope] - serializer_class = TransactionSerializer - lookup_field = 'uuid' - required_scopes = ['wallet.transaction:retrieve'] - - def get_queryset(self): - user = self.request.user - return Transaction.objects.filter(Q(payee=user) | Q(payer=user) | Q(payer__isnull=True)).all() - - -class TransactionPay(generics.RetrieveAPIView): - permission_classes = [permissions.IsAuthenticated, IsAuthenticatedOrTokenHasScope] - serializer_class = TransactionSerializer - lookup_field = 'uuid' - required_scopes = ['wallet.invoice:pay'] - - def get_queryset(self): - user = self.request.user - return Transaction.objects.filter(Q(payer=user) | Q(payer__isnull=True)).all() - - def retrieve(self, request, *args, **kwargs): - instance = self.get_object() - instance.pay(payer=self.request.user) - serializer = self.get_serializer(instance) - return Response(serializer.data) - - diff --git a/apps/wallet/constans.py b/apps/wallet/constans.py index 73d8545..3531ce6 100644 --- a/apps/wallet/constans.py +++ b/apps/wallet/constans.py @@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _ class StateChoices(IntegerChoices): CREATED = 1 , _('created') DELAYED = 2 , _('delayed') # delayed as user wish - PENDING = 3 , _('pending') # wait for external service response + PENDING = 3 , _('pending') # wait for external service response (or action) INCOMPLETE = 4 , _('incomplete') # started and wait for internal progress to complete SUCCESS = 5 , _('success') FAILED = 6 , _('failed') diff --git a/apps/wallet/migrations/0005_remove_transaction_detail_transaction_details_and_more.py b/apps/wallet/migrations/0005_remove_transaction_detail_transaction_details_and_more.py new file mode 100644 index 0000000..01ae594 --- /dev/null +++ b/apps/wallet/migrations/0005_remove_transaction_detail_transaction_details_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.0.6 on 2024-08-11 10:40 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('wallet', '0004_remove_transaction_payee_remove_transaction_payer_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='transaction', + name='detail', + ), + migrations.AddField( + model_name='transaction', + name='details', + field=models.JSONField(blank=True, default=dict, null=True), + ), + migrations.AlterField( + model_name='transaction', + name='amount', + field=models.PositiveBigIntegerField(validators=[django.core.validators.MinValueValidator(1)], verbose_name='amount'), + ), + migrations.AlterField( + model_name='transaction', + name='delay', + field=models.PositiveIntegerField(blank=True, default=0), + ), + ] diff --git a/apps/wallet/models.py b/apps/wallet/models.py index 2c68398..ceefd00 100755 --- a/apps/wallet/models.py +++ b/apps/wallet/models.py @@ -193,4 +193,4 @@ class Transaction(models.Model): return self def __str__(self): - return f"{self.get_payer()} -> {self.get_payee()} ({self.uuid}): {self.amount}" + 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 b9cd5ae..4bfa9c1 100755 --- a/apps/wallet/serializers.py +++ b/apps/wallet/serializers.py @@ -3,6 +3,7 @@ from django.db.transaction import atomic from rest_framework import serializers from rest_framework.exceptions import APIException +from .constans import TypeChoices from .models import Transaction from ..users.models import User @@ -10,6 +11,7 @@ from ..users.models import User class TransactionSerializer(serializers.ModelSerializer): payee_id = serializers.UUIDField(read_only=True) payee_type = serializers.UUIDField(read_only=True) + class Meta: model = Transaction fields = ('uuid', @@ -92,7 +94,55 @@ class WithdrawSerializer(serializers.ModelSerializer): return transaction -class WalletSerializer(serializers.ModelSerializer): +class MyWalletSerializer(serializers.ModelSerializer): class Meta: - model=User + model = User fields = ['balance'] + + +class WalletBalanceSerializer(serializers.ModelSerializer): + instance_id = serializers.UUIDField(required=True) + instance_type = serializers.ChoiceField(choices=TypeChoices.choices, required=True) + + class Meta: + model = User + fields = ['balance', 'instance_id', 'instance_type'] + + +class TransferSubmitSerializer(serializers.ModelSerializer): + 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 = ['uuid', 'updated_at', 'state', 'created_at'] + + +class TransferVerifySerializer(serializers.ModelSerializer): + 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 = ['uuid', 'payer_id', 'payer_type', 'payee_id', 'payee_type', 'updated_at', 'state', + 'amount', 'created_at'] diff --git a/apps/wallet/urls.py b/apps/wallet/urls.py index f780ed9..0c73da6 100644 --- a/apps/wallet/urls.py +++ b/apps/wallet/urls.py @@ -1,28 +1,30 @@ from django.urls import path -from .application_views import ApplicationWalletBalanceView -from .views import TransactionList, TransactionDetail, TransactionPay, TransactionReceipt, \ - WithdrawVerify, WithdrawSubmit, DepositSubmit, DepositVerify, TransactionListView, TransactionDetailView, \ - WalletBalanceView +from .views import application as application_views +from .views import user as user_views app_name = "wallet" urlpatterns = [ - path('transactions/', TransactionListView.as_view(), name='transaction_list'), - path('transactions/', TransactionDetailView.as_view(), name='transaction_detail'), - - path('api/wallet/balance', WalletBalanceView.as_view(), name='wallet_balance_api'), - - path('api/transactions/', 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'), - path('api/transactions//receipt', TransactionReceipt.as_view(), name='transaction_receipt_api'), + path('transactions/', user_views.TransactionListView.as_view(), name='user_transaction_list'), + path('transactions/', user_views.TransactionDetailView.as_view(), name='user_transaction_detail'), # - path('api/deposit/', DepositSubmit.as_view(), name='deposit_submit_api'), - path('api/deposit//verify', DepositVerify.as_view(), name='deposit_verify_api'), + # path('api/wallet/balance', MyWalletBalanceView.as_view(), name='wallet_balance_api'), # - path('api/withdraw/', WithdrawSubmit.as_view(), name='withdraw_submit_api'), - path('api/withdraw//verify', WithdrawVerify.as_view(), name='withdraw_verify_api'), + # path('api/transactions/', 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'), + # path('api/transactions//receipt', TransactionReceipt.as_view(), name='transaction_receipt_api'), + # # + # path('api/deposit/', DepositSubmit.as_view(), name='deposit_submit_api'), + # path('api/deposit//verify', DepositVerify.as_view(), name='deposit_verify_api'), + # # + # path('api/withdraw/', WithdrawSubmit.as_view(), name='withdraw_submit_api'), + # 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'), - path('api/get_balance/', ApplicationWalletBalanceView.as_view(), name='wallet_balance'), ] \ No newline at end of file diff --git a/apps/wallet/views/__init__.py b/apps/wallet/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/wallet/views/application.py b/apps/wallet/views/application.py new file mode 100755 index 0000000..c8779ba --- /dev/null +++ b/apps/wallet/views/application.py @@ -0,0 +1,83 @@ +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.models import Transaction +from apps.wallet.serializers import TransactionSerializer, WalletBalanceSerializer, TransferSubmitSerializer, \ + TransferVerifySerializer + + +def get_application(request): + try: + application = request.auth.application + except: + application = None + return application + + +class WalletBalanceView(generics.RetrieveAPIView): + ''' + API for getting specified user or application balance. + ''' + permission_classes = [TokenHasScope] + serializer_class = WalletBalanceSerializer + 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) + + +class TransactionDetailView(generics.RetrieveAPIView): + permission_classes = [TokenHasScope] + serializer_class = TransactionSerializer + lookup_field = 'uuid' + required_scopes = ['wallet.application:transaction_detail'] + + +class TransferSubmitView(generics.CreateAPIView): + permission_classes = [TokenHasScope] + serializer_class = TransferSubmitSerializer + lookup_field = 'uuid' + required_scopes = ['wallet.application:transfer_submit'] + + def perform_create(self, serializer): + instance:Transaction = serializer.save() + application = get_application(self.request) + instance.application = application + instance.submit() + + +class TransferVerifyView(generics.UpdateAPIView): + permission_classes = [TokenHasScope] + serializer_class = TransferVerifySerializer + lookup_field = 'uuid' + required_scopes = ['wallet.application:transfer_verify'] + queryset = Transaction.objects.all() + + def perform_update(self, serializer): + instance:Transaction = serializer.save() + instance.verify() + diff --git a/apps/wallet/views.py b/apps/wallet/views/user.py similarity index 95% rename from apps/wallet/views.py rename to apps/wallet/views/user.py index caabc83..1f406e9 100755 --- a/apps/wallet/views.py +++ b/apps/wallet/views/user.py @@ -12,9 +12,10 @@ 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, WalletSerializer -from .constans import StateChoices -from .forms import TransactionActionForm +from apps.wallet.serializers import TransactionSerializer, DepositSerializer, WithdrawSerializer, \ + MyWalletSerializer +from ..constans import StateChoices +from ..forms import TransactionActionForm from django.utils.translation import gettext_lazy as _ @@ -27,12 +28,12 @@ def get_application(request): return application -class WalletBalanceView(generics.RetrieveAPIView): +class MyWalletBalanceView(generics.RetrieveAPIView): ''' API for getting authenticated user balance. ''' permission_classes = [AllowAny, permissions.IsAuthenticated, IsAuthenticatedOrTokenHasScope] - serializer_class = WalletSerializer + serializer_class = MyWalletSerializer required_scopes = ['wallet.application:get_balance'] def get_object(self): @@ -183,7 +184,7 @@ class TransactionListView(ListView): def get_queryset(self): user = self.request.user - return Transaction.objects.filter(Q(payee=user) | Q(payer=user)).all() + return Transaction.objects.filter(Q(payee_id=user.pk) | Q(payer_id=user.pk)).all() @method_decorator(login_required, name='dispatch') @@ -213,4 +214,3 @@ class TransactionDetailView(UpdateView): result = transaction.receipt() messages.error(self.request, _('success.')) return super().form_valid(form) - diff --git a/templates/include/inline_transaction_data.html b/templates/include/inline_transaction_data.html index 62da770..2f3810b 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 d816170..dbe3244 100644 --- a/templates/include/top_bar.html +++ b/templates/include/top_bar.html @@ -16,7 +16,7 @@
diff --git a/wallet/settings.py b/wallet/settings.py index 6a938c1..69828b9 100644 --- a/wallet/settings.py +++ b/wallet/settings.py @@ -144,7 +144,7 @@ TEMPLATES = [ AUTHENTICATION_BACKENDS = ( 'oauth2_provider.backends.OAuth2Backend', - # 'django.contrib.auth.backends.ModelBackend', + 'django.contrib.auth.backends.ModelBackend', ) WSGI_APPLICATION = 'wallet.wsgi.application' diff --git a/wallet/urls.py b/wallet/urls.py index d31a443..9d9d6d9 100644 --- a/wallet/urls.py +++ b/wallet/urls.py @@ -23,8 +23,6 @@ from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, Spec # from apps.gooyal_oauth2.views.introspect import introspect_token -from apps.wallet.views import TransactionList, TransactionDetail, TransactionPay, TransactionReceipt, \ - WithdrawVerify, WithdrawSubmit, DepositSubmit, DepositVerify from django.contrib.auth import urls as auth_urls