diff --git a/apps/transactions/migrations/0006_auto_20200420_0538.py b/apps/transactions/migrations/0006_auto_20200420_0538.py new file mode 100644 index 0000000..57346cd --- /dev/null +++ b/apps/transactions/migrations/0006_auto_20200420_0538.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.4 on 2020-04-20 05:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('transactions', '0005_auto_20200404_0942'), + ] + + operations = [ + migrations.AlterField( + model_name='transaction', + name='state', + field=models.IntegerField(choices=[(1, 'created'), (2, 'delayed'), (3, 'pending'), (4, 'incomplete'), (5, 'success'), (6, 'failed'), (7, 'incomplete')], default=1), + ), + ] diff --git a/apps/transactions/models.py b/apps/transactions/models.py index 20c37a2..66e2bc2 100755 --- a/apps/transactions/models.py +++ b/apps/transactions/models.py @@ -13,14 +13,17 @@ from apps.users.models import User class TransactionManager(models.Manager): - def create_user_transaction(self, - amount, - payee: User, - payer: User = None, - delay=0, - detail_id=None, - application_client_id=None, - state=None): + def create_transaction( + self, + amount, + payee: User, + payer: User = None, + delay=0, + # detail_id=None, + application_client_id=None, + # state=None, + uuid=None + ): if not (isinstance(payer, User) or payer is None): raise ValidationError('payer type not valid') if not isinstance(payee, User): @@ -30,47 +33,17 @@ class TransactionManager(models.Manager): if not delay >= 0: raise ValidationError('delay should be greater than equal zero') - transaction = self.model(payer=payer, - payee=payee, - amount=amount, - delay=delay, - detail_id=detail_id, - application_client_id=application_client_id - ) - if state: - transaction.state = state - - transaction.save() - return transaction - - def create_service_transaction( - self, - amount, - payee: User = None, - payer: User = None, - # delay=0, - detail_id=None, - application_client_id=None, - # state=None - ): - if not (isinstance(payer, User) or payer is None): - raise ValidationError('payer type not valid') - if not (isinstance(payee, User) or payee is None): - raise ValidationError('payee type not valid') - if not amount > 0: - raise ValidationError('amount should be greater than zero') - # if not delay >= 0: - # raise ValidationError('delay should be greater than equal zero') - transaction = self.model( + uuid=uuid, payer=payer, payee=payee, amount=amount, - # delay=delay, - detail_id=detail_id, - application_client_id=application_client_id, - state=STATE_CHOICES.pending + delay=delay, + # detail_id=detail_id, + application_client_id=application_client_id ) + # if state: + # transaction.state = state transaction.save() return transaction @@ -87,7 +60,7 @@ class Transaction(models.Model): create = models.DateTimeField(_('create'), blank=True, null=True, auto_now=True) last_update = models.DateTimeField(_('last update'), blank=True, null=True, auto_now=True) application_client_id = models.CharField(max_length=100, db_index=True, null=True) - detail_id = models.IntegerField(null=True, blank=True) # or better detail url or res number + detail_id = models.IntegerField(null=True, blank=True) # TODO: or better detail url objects = TransactionManager() @@ -101,16 +74,16 @@ class Transaction(models.Model): if self.payer and payer and not self.payer == payer: raise APIException('payer identification error') + # TODO: check bundry conditions + if not self.state == STATE_CHOICES.created: + raise APIException('this transaction is not ready to pay') + if not self.payer: self.payer = payer with transaction.atomic(): - # TODO: check bundry conditions self.last_update = datetime.datetime.now() - if not self.state == STATE_CHOICES.created: - raise APIException('this transaction is not ready to pay') - if self.payer.balance < self.amount: raise APIException('not enough balance') @@ -136,50 +109,52 @@ class Transaction(models.Model): return self def receipt(self): + # TODO: check bundry conditions + if not self.state == STATE_CHOICES.delayed: + raise APIException('this transaction is not delayed') + with transaction.atomic(): self.last_update = datetime.datetime.now() - # TODO: check bundry conditions - if not self.state == STATE_CHOICES.delayed: - raise APIException('this transaction is not delayed') - self.payee.balance = F('balance') + self.amount self.state = STATE_CHOICES.success self.payee.save() self.save() def submit(self): + if not self.payer: + raise APIException('payer identification error') + + if not self.payee: + raise APIException('payee identification error') + + # TODO: check bundry conditions + if not self.state == STATE_CHOICES.created: + raise APIException('this transaction is not ready to pay') + with transaction.atomic(): - # TODO: check bundry conditions self.last_update = datetime.datetime.now() - if not self.state == STATE_CHOICES.created: - raise APIException('this transaction is not ready to pay') + if self.payer.balance < self.amount: + raise APIException('not enough balance') - if self.payer: - self.payer.balance = F('balance') - self.amount - self.payer.save(update_fields=['balance']) + self.payer.balance = F('balance') - self.amount + self.payer.save(update_fields=['balance']) - self.payer.refresh_from_db() - if self.payer.balance < 0: - raise APIException('not enough balance') - - else: - self.payee.balance = F('balance') + self.amount - self.payee.save(update_fields=['balance']) - - self.state = STATE_CHOICES.success - self.save() + self.payer.refresh_from_db() + self.state = STATE_CHOICES.pending + if self.payer.balance < 0: + raise APIException('not enough balance') self.save() return self def verify(self): + # TODO: check bundry conditions + if not self.state == STATE_CHOICES.pending: + raise APIException('this transaction is not pending') + with transaction.atomic(): self.last_update = datetime.datetime.now() - # TODO: check bundry conditions - if not self.state == STATE_CHOICES.pending: - raise APIException('this transaction is not pending') - self.payee.balance = F('balance') + self.amount self.state = STATE_CHOICES.success self.payee.save() diff --git a/apps/transactions/serializers.py b/apps/transactions/serializers.py index 307083e..e73cd81 100755 --- a/apps/transactions/serializers.py +++ b/apps/transactions/serializers.py @@ -1,5 +1,7 @@ +from django.db import IntegrityError from django.db.transaction import atomic from rest_framework import serializers +from rest_framework.exceptions import APIException from apps.users.serializers import PublicUserSerializer from .models import Transaction @@ -8,6 +10,7 @@ from .models import Transaction class TransactionSerializer(serializers.ModelSerializer): payer = PublicUserSerializer(required=False) payee = PublicUserSerializer(required=False, read_only=True) + # application_client_id = serializers.IntegerField(required=False, read_only=True) class Meta: @@ -20,11 +23,11 @@ class TransactionSerializer(serializers.ModelSerializer): 'amount', 'delay', 'last_update', - 'detail_id', + # 'detail_id', # 'application_client_id' ) - read_only_fields = ['uuid', 'last_update', 'state', 'create', 'detail_id'] #, 'application_client_id'] + read_only_fields = ['uuid', 'last_update', 'state', 'create'] # , 'detail_id'] #, 'application_client_id'] def create(self, validated_data): @@ -34,10 +37,54 @@ class TransactionSerializer(serializers.ModelSerializer): if user == payer: with atomic(): - transaction = Transaction.objects.create_user_transaction(**validated_data) + transaction = Transaction.objects.create_transaction(**validated_data) transaction.pay() else: - transaction = Transaction.objects.create_user_transaction(**validated_data) + transaction = Transaction.objects.create_transaction(**validated_data) + + return transaction + + +class ServiceTransactionSerializer(serializers.ModelSerializer): + # payer = PublicUserSerializer(required=True) + # payee = PublicUserSerializer(required=True) + user = PublicUserSerializer(required=False, write_only=True) + uuid = serializers.UUIDField(required=True) + + # application_client_id = serializers.IntegerField(required=False, read_only=True) + + class Meta: + model = Transaction + fields = ( + 'user', + 'uuid', + 'payer', + 'payee', + "create", + "state", + 'amount', + 'delay', + 'last_update', + # 'detail_id', + # 'application_client_id' + ) + + read_only_fields = ['last_update', 'state', 'create', 'user'] # , 'detail_id'] #, 'application_client_id'] + + def create(self, validated_data): + payer = validated_data.get('payer') + payee = validated_data.get('payee') + user = validated_data.pop('user') + + if user not in [payee, payer]: + raise APIException('user must be in payer or payee') + + with atomic(): + try: + transaction = Transaction.objects.create_transaction(**validated_data) + transaction.submit() + except IntegrityError as e: + raise APIException('duplicate uuid') return transaction diff --git a/apps/transactions/views.py b/apps/transactions/views.py index 7cd95fc..657046f 100755 --- a/apps/transactions/views.py +++ b/apps/transactions/views.py @@ -5,7 +5,19 @@ from rest_framework import generics, permissions from rest_framework.response import Response from apps.transactions.models import Transaction -from apps.transactions.serializers import TransactionSerializer +from apps.transactions.serializers import TransactionSerializer, ServiceTransactionSerializer + + +def get_application_client_id(request): + try: + application = request.auth.application + if application: + application_client_id = application.client_id + else: + application_client_id = request.auth.detail.get('client_id') + except: + application_client_id = None + return application_client_id class TransactionList(generics.ListCreateAPIView): @@ -17,20 +29,9 @@ class TransactionList(generics.ListCreateAPIView): user = self.request.user return Transaction.objects.filter(Q(payee=user) | Q(payer=user)).all() - def get_application_client_id(self): - try: - application = self.request.auth.application - if application: - application_client_id = application.client_id - else: - application_client_id = self.request.auth.detail.get('client_id') - except: - application_client_id = None - return application_client_id - def perform_create(self, serializer): user = self.request.user - serializer.save(payee=user, application_client_id=self.get_application_client_id()) + serializer.save(payee=user, application_client_id=get_application_client_id(self.request)) class TransactionDetail(generics.RetrieveAPIView): @@ -76,3 +77,36 @@ class TransactionReceipt(generics.RetrieveAPIView): instance.receipt() serializer = self.get_serializer(instance) return Response(serializer.data) + + +class ServiceTransactionSubmit(generics.CreateAPIView): + permission_classes = [IsAuthenticatedOrTokenHasScope] + serializer_class = ServiceTransactionSerializer + required_scopes = ['transaction.submit'] + + def get_queryset(self): + # TODO: check if application_client_id is None + application_client_id = get_application_client_id(self.request) + return Transaction.objects.filter(application_client_id=application_client_id).all() + + def perform_create(self, serializer): + user = self.request.auth.application.user + serializer.save(user=user, application_client_id=get_application_client_id(self.request)) + + +class ServiceTransactionVerify(generics.RetrieveAPIView): + permission_classes = [IsAuthenticatedOrTokenHasScope] + serializer_class = ServiceTransactionSerializer + lookup_field = 'uuid' + required_scopes = ['transaction.verify'] + + def get_queryset(self): + # TODO: check if application_client_id is None + application_client_id = get_application_client_id(self.request) + return Transaction.objects.filter(application_client_id=application_client_id).all() + + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + instance.verify() + serializer = self.get_serializer(instance) + return Response(serializer.data) diff --git a/client.py b/client.py index b634a58..c539167 100644 --- a/client.py +++ b/client.py @@ -128,7 +128,7 @@ class ApiClient(): path = f'transactions/{uuid}' return self._request(path=path) - def create_user_transaction(self, amount, delay, payer=None): + def create_transaction(self, amount, delay, payer=None): data = { 'delay': delay, 'amount': amount diff --git a/gooyal_accounts/urls.py b/gooyal_accounts/urls.py index 412dc07..c707a86 100644 --- a/gooyal_accounts/urls.py +++ b/gooyal_accounts/urls.py @@ -19,7 +19,8 @@ from django.contrib.auth.views import LogoutView from django.urls import path, include from django.contrib import admin -from apps.transactions.views import TransactionList, TransactionDetail, TransactionPay, TransactionReceipt +from apps.transactions.views import TransactionList, TransactionDetail, TransactionPay, TransactionReceipt, \ + ServiceTransactionVerify, ServiceTransactionSubmit from apps.users.views import UserListView, UserDetailView, AccountView, RequestOTPView, ChangePasswordView, \ OTPLoginView, home from django.contrib.auth import urls as auth_urls @@ -43,7 +44,9 @@ urlpatterns = [ path('transactions/', TransactionList.as_view(), name='transactions'), path('transactions/', TransactionDetail.as_view(), name='transaction_detail'), path('transactions//pay', TransactionPay.as_view(), name='transaction_pay'), - path('transactions//receipt', TransactionReceipt.as_view(), name='transaction_receipt'), + + path('service-transactions/', ServiceTransactionSubmit.as_view(), name='service_transaction_submit'), + path('service-transactions//verify', ServiceTransactionVerify.as_view(), name='service_transaction_verify'), ] urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)