transactions

This commit is contained in:
mahdavi 2019-12-08 23:22:36 +03:30
parent c8276e20c3
commit 961e11015a
11 changed files with 305 additions and 7 deletions

View file

@ -18,6 +18,7 @@ from django.conf.urls.static import static
from django.urls import path, include
from django.contrib import admin
from apps.transactions.views import TransactionList, TransactionDetail, TransactionPay, TransactionReceipt
from apps.users.views import UserListView, UserDetailView, AccountView, RequestOTPView, ChangePasswordView
from django.contrib.auth import urls as auth_urls
@ -32,6 +33,11 @@ urlpatterns = [
path('account/', AccountView.as_view(), name='account'),
path('request_otp/', RequestOTPView.as_view(), name='register'),
path('change_password/', ChangePasswordView.as_view(), name='change_password'),
path('transactions/', TransactionList.as_view(), name='transactions'),
path('transactions/<code>', TransactionDetail.as_view(), name='transaction_detail'),
path('transactions/<code>/pay', TransactionPay.as_view(), name='transaction_pay'),
path('transactions/<code>/receipt', TransactionReceipt.as_view(), name='transaction_receipt'),
]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

0
apps/transactions/__init__.py Executable file
View file

19
apps/transactions/admin.py Executable file
View file

@ -0,0 +1,19 @@
from django.contrib import admin
from .models import Transaction
class TransactionAdmin(admin.ModelAdmin):
fields = [
'code',
'amount',
'payer',
'payee',
'create',
'last_update',
'state',
'delay'
]
readonly_fields = ['last_update', 'create']
admin.site.register(Transaction, TransactionAdmin)

5
apps/transactions/apps.py Executable file
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class TransactionsConfig(AppConfig):
name = 'apps.transactions'

View file

@ -0,0 +1,32 @@
# Generated by Django 3.0 on 2019-12-06 20:24
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Transaction',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.IntegerField(verbose_name='code')),
('amount', models.IntegerField(verbose_name='amount')),
('delay', models.IntegerField(blank=True, default=0)),
('state', models.IntegerField(choices=[(1, 'created'), (2, 'delayed'), (3, 'pending'), (4, 'failed'), (5, 'done')], default=1)),
('create', models.DateTimeField(auto_now=True, null=True, verbose_name='create')),
('last_update', models.DateTimeField(auto_now=True, null=True, verbose_name='last update')),
('detail_id', models.IntegerField(blank=True, null=True, unique=True)),
('payee', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='receipts', to=settings.AUTH_USER_MODEL)),
('payer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='payments', to=settings.AUTH_USER_MODEL)),
],
),
]

View file

132
apps/transactions/models.py Executable file
View file

@ -0,0 +1,132 @@
import datetime
import random
from django.db import models
from django.db.models import F
from django.utils.translation import gettext_lazy as _
from apps.users.models import User
from django.db import transaction
from model_utils.choices import Choices
from rest_framework.exceptions import APIException, ValidationError
class TransactionManager(models.Manager):
def create_transaction(self,
amount,
payee: User,
payer: User = None,
delay=0,
detail_id=None,
state=None):
if not (isinstance(payer, User) or payer is None):
raise ValidationError('payer type not valid')
if not isinstance(payee, User):
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(payer=payer,
payee=payee,
amount=amount,
delay=delay,
detail_id=detail_id
)
if state:
transaction.state = state
transaction.set_code()
transaction.save()
return transaction
class Transaction(models.Model):
STATE_CHOICES = Choices(
(1, 'created', 'created'),
(2, 'delayed', 'delayed'),
(3, 'pending', 'pending'),
(4, 'failed', 'failed'),
(5, 'done', 'done'),
)
code = models.IntegerField(_('code'), blank=False) # unique random number
amount = models.IntegerField(_('amount'), blank=False)
delay = models.IntegerField(default=0, blank=True) # number of days for delayed payments
payer = models.ForeignKey(User, on_delete=models.PROTECT, related_name='payments', blank=True, null=True)
payee = models.ForeignKey(User, on_delete=models.PROTECT, related_name='receipts')
state = models.IntegerField(choices=STATE_CHOICES, default=STATE_CHOICES.created)
create = models.DateTimeField(_('create'), blank=True, null=True, auto_now=True)
last_update = models.DateTimeField(_('last update'), blank=True, null=True, auto_now=True)
type = None # invoice, charge, etc. it may be redundant
detail_id = models.IntegerField(null=True, blank=True, unique=True) # or better detail url or res number
objects = TransactionManager()
def set_code(self):
# TODO: set infinite algorithm
for code in random.sample(range(10000, 100000), 90000):
try:
code = str(code)
self.code = code
return self.save()
except:
pass
raise APIException('system error')
def pay(self, payer=None):
if not self.payer and not payer:
raise APIException('payer identification error')
if self.payer and payer and not self.payer == payer:
raise APIException('payer identification error')
if not self.payer:
self.payer = payer
with transaction.atomic():
# TODO: check bundry conditions
self.last_update = datetime.datetime.now()
if not self.state == self.STATE_CHOICES.created:
raise APIException('this transaction is not ready to pay')
if self.payer.balance < self.amount:
raise APIException('not enough 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')
if self.delay:
self.state = self.STATE_CHOICES.delayed
self.save()
else:
self.payee.balance = F('balance') + self.amount
self.payee.save(update_fields=['balance'])
self.state = self.STATE_CHOICES.done
self.save()
self.save()
return self
def receipt(self):
with transaction.atomic():
self.last_update = datetime.datetime.now()
# TODO: check bundry conditions
if not self.state == self.STATE_CHOICES.delayed:
raise APIException('this transaction is not delayed')
self.payee.balance = F('balance') + self.amount
self.state = self.STATE_CHOICES.done
self.payee.save()
self.save()
def __str__(self):
return f"{self.payer} -> {self.payee} ({self.code}): {self.amount}"

View file

@ -0,0 +1,41 @@
from django.db.transaction import atomic
from rest_framework import serializers
from apps.users.serializers import PublicUserSerializer
from .models import Transaction
class TransactionSerializer(serializers.ModelSerializer):
payer = PublicUserSerializer(required=False)
payee = PublicUserSerializer(required=False, read_only=True)
class Meta:
model = Transaction
fields = ('payer',
'payee',
"create",
"state",
"code",
'amount',
'delay',
'last_update',
'detail_id'
)
read_only_fields = ['last_update', 'code', 'state', 'create', 'detail_id']
def create(self, validated_data):
payer = validated_data.get('payer')
request = self.context.get('request')
user = request.user if request else payer
if user == payer:
with atomic():
transaction = Transaction.objects.create_transaction(**validated_data)
transaction.pay()
else:
transaction = Transaction.objects.create_transaction(**validated_data)
return transaction

3
apps/transactions/tests.py Executable file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

67
apps/transactions/views.py Executable file
View file

@ -0,0 +1,67 @@
from django.db.models import Q
from oauth2_provider.contrib.rest_framework import TokenHasScope, IsAuthenticatedOrTokenHasScope
from rest_framework import generics, permissions
from rest_framework.response import Response
from apps.transactions.models import Transaction
from apps.transactions.serializers import TransactionSerializer
class TransactionList(generics.ListCreateAPIView):
permission_classes = [permissions.IsAuthenticated, IsAuthenticatedOrTokenHasScope]
serializer_class = TransactionSerializer
required_scopes = []
def get_queryset(self):
user = self.request.user
return Transaction.objects.filter(Q(payee_id=user.id) | Q(payer_id=user.id)).all()
def perform_create(self, serializer):
user = self.request.user
serializer.save(payee=user)
class TransactionDetail(generics.RetrieveAPIView):
permission_classes = [permissions.IsAuthenticated, IsAuthenticatedOrTokenHasScope]
serializer_class = TransactionSerializer
lookup_field = 'code'
required_scopes = []
def get_queryset(self):
user = self.request.user
return Transaction.objects.filter(Q(payee_id=user.id) | Q(payer_id=user.id) | Q(payer__isnull=True)).all()
class TransactionPay(generics.RetrieveAPIView):
permission_classes = [permissions.IsAuthenticated, IsAuthenticatedOrTokenHasScope]
serializer_class = TransactionSerializer
lookup_field = 'code'
required_scopes = []
def get_queryset(self):
user = self.request.user
return Transaction.objects.filter(Q(payer_id=user.id) | 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)
class TransactionReceipt(generics.RetrieveAPIView):
permission_classes = [permissions.IsAuthenticated, IsAuthenticatedOrTokenHasScope]
serializer_class = TransactionSerializer
lookup_field = 'code'
required_scopes = []
def get_queryset(self):
user = self.request.user
return Transaction.objects.filter(payer_id=user.id).all()
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
instance.receipt()
serializer = self.get_serializer(instance)
return Response(serializer.data)

7
utils.py Normal file → Executable file
View file

@ -1,5 +1,3 @@
import random
from time import time
from oauth2_provider.oauth2_validators import OAuth2Validator
from django.contrib.auth import get_user_model
@ -7,11 +5,6 @@ from django.contrib.auth import get_user_model
USER_MODEL = get_user_model()
def random_code(pre_len=5, post_len=5):
code = str(time())[:pre_len] + ''.join(random.choice('0123456789') for _ in range(post_len))
return code
class MultiGatewayOAuth2Validator(OAuth2Validator): # pylint: disable=w0223
""" Primarily extend the functionality of token generation """