basic service transaction submit and verify
This commit is contained in:
parent
24fd4010c0
commit
8365506cfd
6 changed files with 170 additions and 93 deletions
18
apps/transactions/migrations/0006_auto_20200420_0538.py
Normal file
18
apps/transactions/migrations/0006_auto_20200420_0538.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue