basic service transaction submit and verify

This commit is contained in:
mahdavi 2020-04-20 12:06:29 +04:30
parent 24fd4010c0
commit 8365506cfd
6 changed files with 170 additions and 93 deletions

View file

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

View file

@ -13,14 +13,17 @@ from apps.users.models import User
class TransactionManager(models.Manager): class TransactionManager(models.Manager):
def create_user_transaction(self, def create_transaction(
amount, self,
payee: User, amount,
payer: User = None, payee: User,
delay=0, payer: User = None,
detail_id=None, delay=0,
application_client_id=None, # detail_id=None,
state=None): application_client_id=None,
# state=None,
uuid=None
):
if not (isinstance(payer, User) or payer is None): if not (isinstance(payer, User) or payer is None):
raise ValidationError('payer type not valid') raise ValidationError('payer type not valid')
if not isinstance(payee, User): if not isinstance(payee, User):
@ -30,47 +33,17 @@ class TransactionManager(models.Manager):
if not delay >= 0: if not delay >= 0:
raise ValidationError('delay should be greater than equal zero') 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( transaction = self.model(
uuid=uuid,
payer=payer, payer=payer,
payee=payee, payee=payee,
amount=amount, amount=amount,
# delay=delay, delay=delay,
detail_id=detail_id, # detail_id=detail_id,
application_client_id=application_client_id, application_client_id=application_client_id
state=STATE_CHOICES.pending
) )
# if state:
# transaction.state = state
transaction.save() transaction.save()
return transaction return transaction
@ -87,7 +60,7 @@ class Transaction(models.Model):
create = models.DateTimeField(_('create'), blank=True, null=True, auto_now=True) create = models.DateTimeField(_('create'), blank=True, null=True, auto_now=True)
last_update = models.DateTimeField(_('last update'), 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) 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() objects = TransactionManager()
@ -101,16 +74,16 @@ class Transaction(models.Model):
if self.payer and payer and not self.payer == payer: if self.payer and payer and not self.payer == payer:
raise APIException('payer identification error') 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: if not self.payer:
self.payer = payer self.payer = payer
with transaction.atomic(): with transaction.atomic():
# TODO: check bundry conditions
self.last_update = datetime.datetime.now() 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: if self.payer.balance < self.amount:
raise APIException('not enough balance') raise APIException('not enough balance')
@ -136,50 +109,52 @@ class Transaction(models.Model):
return self return self
def receipt(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(): with transaction.atomic():
self.last_update = datetime.datetime.now() 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.payee.balance = F('balance') + self.amount
self.state = STATE_CHOICES.success self.state = STATE_CHOICES.success
self.payee.save() self.payee.save()
self.save() self.save()
def submit(self): 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(): with transaction.atomic():
# TODO: check bundry conditions
self.last_update = datetime.datetime.now() self.last_update = datetime.datetime.now()
if not self.state == STATE_CHOICES.created: if self.payer.balance < self.amount:
raise APIException('this transaction is not ready to pay') raise APIException('not enough balance')
if self.payer: self.payer.balance = F('balance') - self.amount
self.payer.balance = F('balance') - self.amount self.payer.save(update_fields=['balance'])
self.payer.save(update_fields=['balance'])
self.payer.refresh_from_db() self.payer.refresh_from_db()
if self.payer.balance < 0: self.state = STATE_CHOICES.pending
raise APIException('not enough balance') 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.save() self.save()
return self return self
def verify(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(): with transaction.atomic():
self.last_update = datetime.datetime.now() 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.payee.balance = F('balance') + self.amount
self.state = STATE_CHOICES.success self.state = STATE_CHOICES.success
self.payee.save() self.payee.save()

View file

@ -1,5 +1,7 @@
from django.db import IntegrityError
from django.db.transaction import atomic from django.db.transaction import atomic
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import APIException
from apps.users.serializers import PublicUserSerializer from apps.users.serializers import PublicUserSerializer
from .models import Transaction from .models import Transaction
@ -8,6 +10,7 @@ from .models import Transaction
class TransactionSerializer(serializers.ModelSerializer): class TransactionSerializer(serializers.ModelSerializer):
payer = PublicUserSerializer(required=False) payer = PublicUserSerializer(required=False)
payee = PublicUserSerializer(required=False, read_only=True) payee = PublicUserSerializer(required=False, read_only=True)
# application_client_id = serializers.IntegerField(required=False, read_only=True) # application_client_id = serializers.IntegerField(required=False, read_only=True)
class Meta: class Meta:
@ -20,11 +23,11 @@ class TransactionSerializer(serializers.ModelSerializer):
'amount', 'amount',
'delay', 'delay',
'last_update', 'last_update',
'detail_id', # 'detail_id',
# 'application_client_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): def create(self, validated_data):
@ -34,10 +37,54 @@ class TransactionSerializer(serializers.ModelSerializer):
if user == payer: if user == payer:
with atomic(): with atomic():
transaction = Transaction.objects.create_user_transaction(**validated_data) transaction = Transaction.objects.create_transaction(**validated_data)
transaction.pay() transaction.pay()
else: 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 return transaction

View file

@ -5,7 +5,19 @@ from rest_framework import generics, permissions
from rest_framework.response import Response from rest_framework.response import Response
from apps.transactions.models import Transaction 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): class TransactionList(generics.ListCreateAPIView):
@ -17,20 +29,9 @@ class TransactionList(generics.ListCreateAPIView):
user = self.request.user user = self.request.user
return Transaction.objects.filter(Q(payee=user) | Q(payer=user)).all() 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): def perform_create(self, serializer):
user = self.request.user 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): class TransactionDetail(generics.RetrieveAPIView):
@ -76,3 +77,36 @@ class TransactionReceipt(generics.RetrieveAPIView):
instance.receipt() instance.receipt()
serializer = self.get_serializer(instance) serializer = self.get_serializer(instance)
return Response(serializer.data) 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)

View file

@ -128,7 +128,7 @@ class ApiClient():
path = f'transactions/{uuid}' path = f'transactions/{uuid}'
return self._request(path=path) return self._request(path=path)
def create_user_transaction(self, amount, delay, payer=None): def create_transaction(self, amount, delay, payer=None):
data = { data = {
'delay': delay, 'delay': delay,
'amount': amount 'amount': amount

View file

@ -19,7 +19,8 @@ from django.contrib.auth.views import LogoutView
from django.urls import path, include from django.urls import path, include
from django.contrib import admin 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, \ from apps.users.views import UserListView, UserDetailView, AccountView, RequestOTPView, ChangePasswordView, \
OTPLoginView, home OTPLoginView, home
from django.contrib.auth import urls as auth_urls from django.contrib.auth import urls as auth_urls
@ -43,7 +44,9 @@ urlpatterns = [
path('transactions/', TransactionList.as_view(), name='transactions'), path('transactions/', TransactionList.as_view(), name='transactions'),
path('transactions/<uuid>', TransactionDetail.as_view(), name='transaction_detail'), path('transactions/<uuid>', TransactionDetail.as_view(), name='transaction_detail'),
path('transactions/<uuid>/pay', TransactionPay.as_view(), name='transaction_pay'), path('transactions/<uuid>/pay', TransactionPay.as_view(), name='transaction_pay'),
path('transactions/<uuid>/receipt', TransactionReceipt.as_view(), name='transaction_receipt'),
path('service-transactions/', ServiceTransactionSubmit.as_view(), name='service_transaction_submit'),
path('service-transactions/<uuid>/verify', ServiceTransactionVerify.as_view(), name='service_transaction_verify'),
] ]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)