Merge pull request 'develop' (#1) from develop into master

Reviewed-on: https://git.mindplus-dev.ir/sahama/wallet/pulls/1
This commit is contained in:
sahama 2025-09-18 03:42:43 -04:00
commit 3f14e9b9ca
12 changed files with 956 additions and 73 deletions

View file

@ -1,5 +1,5 @@
from django.contrib import admin from django.contrib import admin
from .models import Transaction from .models import Transaction, Wallet, Category
class TransactionAdmin(admin.ModelAdmin): class TransactionAdmin(admin.ModelAdmin):
list_display = ['uuid', 'amount', 'created_at', 'updated_at', 'payer_id', 'payer_type', 'payee_id', 'payee_type', 'state', 'application'] list_display = ['uuid', 'amount', 'created_at', 'updated_at', 'payer_id', 'payer_type', 'payee_id', 'payee_type', 'state', 'application']
@ -7,3 +7,5 @@ class TransactionAdmin(admin.ModelAdmin):
list_filter = ['state', 'payer_type', 'payee_type'] list_filter = ['state', 'payer_type', 'payee_type']
admin.site.register(Transaction, TransactionAdmin) admin.site.register(Transaction, TransactionAdmin)
admin.site.register(Wallet)
admin.site.register(Category)

View file

@ -0,0 +1,71 @@
# Generated by Django 5.1.4 on 2025-08-19 08:46
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wallet', '0007_alter_transaction_created_at_and_more'),
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
]
operations = [
migrations.AlterModelOptions(
name='transaction',
options={'ordering': ['-created_at']},
),
migrations.AlterField(
model_name='transaction',
name='created_at',
field=models.DateTimeField(auto_now_add=True, db_index=True, default=None),
preserve_default=False,
),
migrations.AlterField(
model_name='transaction',
name='updated_at',
field=models.DateTimeField(auto_now=True, db_index=True, default=None),
preserve_default=False,
),
migrations.CreateModel(
name='Category',
fields=[
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, primary_key=True, serialize=False, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True, db_index=True)),
('label', models.CharField(max_length=255, verbose_name='label')),
('application', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Wallet',
fields=[
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, primary_key=True, serialize=False, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True, db_index=True)),
('owner_uuid', models.UUIDField(db_index=True)),
('owner_type', models.IntegerField(choices=[(1, 'user'), (2, 'application')], db_index=True)),
('balance', models.BigIntegerField(default=0, verbose_name='balance')),
('category', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='wallet.category')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='transaction',
name='payee_wallet',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='payee_wallet_transactions', to='wallet.wallet'),
),
migrations.AddField(
model_name='transaction',
name='payer_wallet',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='payer_wallet_transactions', to='wallet.wallet'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-09-02 09:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wallet', '0008_alter_transaction_options_and_more'),
]
operations = [
migrations.AlterField(
model_name='transaction',
name='payee_id',
field=models.UUIDField(null=True),
),
]

View file

@ -1,22 +1,43 @@
import datetime
import uuid
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.db import models from django.db import models
from django.db import transaction from django.db import transaction
from django.db.models import F from django.db.models import F
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import APIException, ValidationError from rest_framework.exceptions import APIException, NotAcceptable
from django.conf import settings from django.conf import settings
from oauth2_provider import settings as oauth2_settings from oauth2_provider import settings as oauth2_settings
from gooyal_accounts_client.models import PublicUser from gooyal_accounts_client.models import PublicUser
from utils.accounts_client import get_user_details, get_application_details from utils.accounts_client import get_user_details, get_application_details
from utils.models import BaseModel
from .constans import StateChoices, TypeChoices from .constans import StateChoices, TypeChoices
from apps.users.models import User from apps.users.models import User
from ..gooyal_oauth2.models import Application from ..gooyal_oauth2.models import Application
class WalletQuerySet(models.QuerySet):
pass
class Wallet(BaseModel):
owner_uuid = models.UUIDField(db_index=True, null=False)
category = models.ForeignKey('Category', on_delete=models.PROTECT, null=True)
owner_type = models.IntegerField(choices=TypeChoices.choices, db_index=True)
balance = models.BigIntegerField(_('balance'), default=0)
objects = WalletQuerySet.as_manager()
class CategoryQuerySet(models.QuerySet):
pass
class Category(BaseModel):
application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, null=True)
label = models.CharField(_('label'), max_length=255)
objects = CategoryQuerySet.as_manager()
# TODO: move DRF functionalities to view or serializers # TODO: move DRF functionalities to view or serializers
# class TransactionManager(models.Manager): # class TransactionManager(models.Manager):
# def create_transaction( # def create_transaction(
@ -47,45 +68,52 @@ from ..gooyal_oauth2.models import Application
# return tr # return tr
class Transaction(models.Model): class TransactionQuerySet(models.QuerySet):
uuid = models.UUIDField(primary_key=True, editable=True, default=uuid.uuid4, unique=True, def migrate(self):
db_index=True) # reserve_code for transaction in self:
payer = transaction.get_payer()
payee = transaction.get_payee()
payer_wallet, created = Wallet.objects.get_or_create(owner_uuid=transaction.payer_id, owner_type=transaction.payer_type, defaults={'balance': payer.balance})
payee_wallet, created = Wallet.objects.get_or_create(owner_uuid=transaction.payee_id, owner_type=transaction.payee_type, defaults={'balance': payee.balance})
transaction.payer_wallet = payer_wallet
transaction.payee_wallet = payee_wallet
transaction.save()
class Transaction(BaseModel):
amount = models.PositiveBigIntegerField(_('amount'), blank=False, validators=[MinValueValidator(1)]) amount = models.PositiveBigIntegerField(_('amount'), blank=False, validators=[MinValueValidator(1)])
delay = models.PositiveIntegerField(default=0, blank=True) # number of days for delayed payments delay = models.PositiveIntegerField(default=0, blank=True) # number of days for delayed payments
payer_id = models.UUIDField(blank=True, null=True) payer_id = models.UUIDField(blank=True, null=True)
payer_type = models.IntegerField(choices=TypeChoices.choices, blank=True, null=True) payer_type = models.IntegerField(choices=TypeChoices.choices, blank=True, null=True)
payee_id = models.UUIDField() payee_id = models.UUIDField(null=True)
payee_type = models.IntegerField(choices=TypeChoices.choices, blank=True, null=True) payee_type = models.IntegerField(choices=TypeChoices.choices, blank=True, null=True)
state = models.IntegerField(choices=StateChoices.choices, default=StateChoices.CREATED) state = models.IntegerField(choices=StateChoices.choices, default=StateChoices.CREATED)
created_at = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
updated_at = models.DateTimeField(auto_now=True, null=True, db_index=True)
application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT,
related_name='transactions', null=True) related_name='transactions', null=True)
payer_wallet = models.ForeignKey(Wallet, on_delete=models.PROTECT, null=True,
related_name='payer_wallet_transactions',
db_index=True)
payee_wallet = models.ForeignKey(Wallet, on_delete=models.PROTECT, null=True,
related_name='payee_wallet_transactions',
db_index=True)
details = models.JSONField(null=True, blank=True, default=dict) details = models.JSONField(null=True, blank=True, default=dict)
# detail = {'payer_name', 'payee_name', 'description', 'application_name', 'payer_avatar_url', 'payee_avatar_url', # detail = {'payer_name', 'payee_name', 'description', 'application_name', 'payer_avatar_url', 'payee_avatar_url',
# 'reference_id', 'application_details_url'} # 'reference_id', 'application_details_url'}
# objects = TransactionManager() objects = TransactionQuerySet.as_manager()
class Meta: class Meta:
ordering = ['-created_at'] ordering = ['-created_at']
def __str__(self):
try:
return f"{self.get_payer()} -> {self.get_payee()} ({self.uuid}): {self.amount} - {self.get_state_display()}"
except:
return str(self.uuid)
# unique_together = ('application', 'detail_id',) # unique_together = ('application', 'detail_id',)
def withdraw_from_payer_balance(self):
payer = self.get_payer()
payer_is_allowed_negative_balance = self.payer_type==TypeChoices.APPLICATION.value and str(self.payer_id) in settings.ALLOWED_NEGATIVE_BALANCE_APPLICATIONS
if self.amount > payer.balance and not payer_is_allowed_negative_balance :
raise APIException('not enough balance')
payer.balance = F('balance') - self.amount
payer.save(update_fields=['balance'])
def deposit_to_payee_balance(self):
payee = self.get_payee()
payee.balance = F('balance') + self.amount
payee.save(update_fields=['balance'])
def get_pay_entity(self, entity_id, entity_type): def get_pay_entity(self, entity_id, entity_type):
if entity_type == TypeChoices.APPLICATION: if entity_type == TypeChoices.APPLICATION:
entity: Application = Application.objects.get(pk=entity_id) entity: Application = Application.objects.get(pk=entity_id)
@ -113,6 +141,19 @@ class Transaction(models.Model):
else: else:
raise APIException(error_message) raise APIException(error_message)
######################## category
def category_withdraw_from_payer_balance(self):
payer_is_allowed_negative_balance = self.payer_wallet.owner_type == TypeChoices.APPLICATION.value and str(
self.payer_wallet.owner_uuid) in settings.ALLOWED_NEGATIVE_BALANCE_APPLICATIONS
if self.amount > self.payer_wallet.balance and not payer_is_allowed_negative_balance:
raise APIException('not enough balance')
self.payer_wallet.balance = F('balance') - self.amount
self.payer_wallet.save(update_fields=['balance'])
def category_deposit_to_payee_balance(self):
self.payee_wallet.balance = F('balance') + self.amount
self.payee_wallet.save(update_fields=['balance'])
def pay(self, payer_id=None, payer_type=None): def pay(self, payer_id=None, payer_type=None):
if not self.payer_id and not (payer_id and payer_type): if not self.payer_id and not (payer_id and payer_type):
raise APIException('payer identification error') raise APIException('payer identification error')
@ -168,51 +209,47 @@ class Transaction(models.Model):
self.deposit_to_payee_balance() self.deposit_to_payee_balance()
return self return self
def submit(self): def category_submit(self):
payer = self.get_payer() if not self.payer_wallet:
payee = self.get_payee()
if not payer:
raise APIException('payer identification error') raise APIException('payer identification error')
if not payee: if not self.payee_wallet:
raise APIException('payee identification error') raise APIException('payee identification error')
if not self.state == StateChoices.CREATED: if not self.state == StateChoices.CREATED:
raise APIException('this transaction is not ready to pay') raise APIException('this transaction is not ready to pay')
with transaction.atomic(): with transaction.atomic():
self.withdraw_from_payer_balance() self.category_withdraw_from_payer_balance()
self.change_state( self.change_state(
StateChoices.CREATED, StateChoices.CREATED,
StateChoices.PENDING, StateChoices.PENDING,
error_message='this transaction is not ready to pay' error_message=_('this transaction is not ready to pay')
) )
return self return self
def verify(self): def category_verify(self):
if not self.state == StateChoices.PENDING: if not self.state == StateChoices.PENDING:
raise APIException('this transaction is not pending') raise APIException('this transaction is not pending')
with transaction.atomic(): with transaction.atomic():
self.deposit_to_payee_balance() self.category_deposit_to_payee_balance()
self.change_state( self.change_state(
StateChoices.PENDING, StateChoices.PENDING,
StateChoices.SUCCESS, StateChoices.SUCCESS,
error_message='this transaction is not pending' error_message=_('this transaction is not pending')
) )
return self return self
def get_role(self, entity): def category_get_role(self, entity):
if self.payer_wallet.owner_uuid == entity.uuid:
if self.payer_id == entity.uuid:
return 1 return 1
elif self.payee_id == entity.uuid: elif self.payee_wallet.owner_uuid == entity.uuid:
return 2 return 2
else: else:
return 0 return 0
def update_details(self, force_update=False): def category_update_details(self, force_update=False):
if not isinstance(self.details, dict): if not isinstance(self.details, dict):
self.details = {} self.details = {}
self.save() self.save()
@ -220,28 +257,28 @@ class Transaction(models.Model):
is_updated = False is_updated = False
if not self.details.get('payer_name'): if not self.details.get('payer_name'):
print('update payer') print('update payer')
if self.payer_type == TypeChoices.USER.value: if self.payer_wallet.owner_type == TypeChoices.USER:
payer_details: PublicUser = get_user_details(self.payer_id) payer_details: PublicUser = get_user_details(self.payer_wallet.owner_uuid)
self.details['payer_name'] = payer_details.first_name + payer_details.last_name self.details['payer_name'] = payer_details.first_name + payer_details.last_name
self.details['payer_avatar_url'] = payer_details.avatar self.details['payer_avatar_url'] = payer_details.avatar
else: else:
payer_details = get_application_details(self.payer_id) payer_details = get_application_details(self.payer_id)
self.details['payer_name'] = payer_details.name self.details['payer_name'] = payer_details.name
#TODO: get avatar from details # TODO: get avatar from details
is_updated = True is_updated = True
print("payer_details:", payer_details) print("payer_details:", payer_details)
print("is_updated:", is_updated) print("is_updated:", is_updated)
if not self.details.get('payee_name'): if not self.details.get('payee_name'):
print('update payee') print('update payee')
if self.payee_type == TypeChoices.USER.value: if self.payee_wallet.owner_type == TypeChoices.USER:
payee_details: PublicUser = get_user_details(self.payee_id) payee_details: PublicUser = get_user_details(self.payee_wallet.owner_uuid)
self.details['payee_name'] = payee_details.first_name + payee_details.last_name self.details['payee_name'] = payee_details.first_name + payee_details.last_name
self.details['payee_avatar_url'] = payee_details.avatar self.details['payee_avatar_url'] = payee_details.avatar
else: else:
payee_details = get_application_details(self.payee_id) payee_details = get_application_details(self.payee_id)
self.details['payee_name'] = payee_details.name self.details['payee_name'] = payee_details.name
#TODO: get avatar from details # TODO: get avatar from details
is_updated = True is_updated = True
print("payee_details:", payee_details) print("payee_details:", payee_details)
@ -253,7 +290,7 @@ class Transaction(models.Model):
print('application_details:', application_details) print('application_details:', application_details)
try: try:
self.details['application_name'] = application_details.name self.details['application_name'] = application_details.name
# TODO: get avatar from details # TODO: get avatar from details
is_updated = True is_updated = True
except Exception as e: except Exception as e:
@ -263,10 +300,3 @@ class Transaction(models.Model):
if is_updated: if is_updated:
print('update details') print('update details')
self.save() self.save()
def __str__(self):
try:
return f"{self.get_payer()} -> {self.get_payee()} ({self.uuid}): {self.amount} - {self.get_state_display()}"
except:
return str(self.uuid)

View file

@ -1,4 +1,5 @@
from django.db import IntegrityError from django.db import IntegrityError
from django.core.validators import MinValueValidator
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 rest_framework.exceptions import APIException
@ -173,7 +174,9 @@ class ApplicationTransactionSerializer(BaseTransactionSerializer):
pass pass
class ApplicationDepositSerializer(serializers.ModelSerializer): class ApplicationDepositSerializer(serializers.ModelSerializer):
uuid = serializers.UUIDField(required=True) payee_id = serializers.UUIDField(required=True)
payee_type = serializers.IntegerField(required=True)
amount = serializers.IntegerField(required=True, validators=[MinValueValidator(1)])
# TODO: details as fields # TODO: details as fields
class Meta: class Meta:
@ -196,7 +199,9 @@ class ApplicationDepositSerializer(serializers.ModelSerializer):
class ApplicationWithdrawSerializer(serializers.ModelSerializer): class ApplicationWithdrawSerializer(serializers.ModelSerializer):
uuid = serializers.UUIDField(required=True) payer_id = serializers.UUIDField(required=True)
payer_type = serializers.IntegerField(required=True)
amount = serializers.IntegerField(required=True, validators=[MinValueValidator(1)])
class Meta: class Meta:
model = Transaction model = Transaction

View file

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

View file

@ -0,0 +1 @@
from .application import *

460
apps/wallet/tests/application.py Executable file
View file

@ -0,0 +1,460 @@
import uuid
from datetime import timedelta
from django.urls import reverse
from django.utils import timezone
from oauth2_provider.models import get_access_token_model, get_application_model
from rest_framework.test import APITestCase, override_settings
from apps.users.models import User
from apps.wallet.constans import TypeChoices
from apps.wallet.models import Wallet
from apps.wallet.models import Category
AccessToken = get_access_token_model()
Application = get_application_model()
class ApplicationApiFlowsTests(APITestCase):
client_id = '4INGOCMoulE0fNY1SQlTbPtsWqqxGj2DdqjADq6u'
application_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb470053')
application_access_token = '14naVsdKCbKNOhnElPyXcrwSnqqFbm'
payer_access_token = '24naVsdKCbKNOhnElPyXcrwSnqqFbm'
payer_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb470051')
payee_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb470052')
wallet_category1_uuid = uuid.UUID('114e8b86-8f4a-44d9-b29d-badceb470051')
wallet_category2_uuid = uuid.UUID('214e8b86-8f4a-44d9-b29d-badceb470051')
# application, _ = Application.objects.get_or_create(client_id=client_id, uuid=application_uuid)
scopes = [
'wallet.application.deposit:verify',
'wallet.application:get_balance',
'wallet.application.deposit:submit',
'wallet.deposit:submit',
'wallet.wallet:get_balance',
'wallet.application:get_user_balance',
'wallet.application.withdraw:submit',
'wallet.application.withdraw:verify',
]
def setUp(self):
self.application_user, _ = User.objects.get_or_create(pk=self.application_uuid)
self.payer_user, _ = User.objects.get_or_create(pk=self.payer_uuid)
self.application_wallet, _ = Wallet.objects.get_or_create(owner_uuid=self.application_uuid, owner_type=TypeChoices.APPLICATION, balance=300)
self.payee_wallet, _ = Wallet.objects.get_or_create(owner_uuid=self.payee_uuid, owner_type=TypeChoices.USER)
self.payer_wallet, _ = Wallet.objects.get_or_create(owner_uuid=self.payer_uuid, owner_type=TypeChoices.USER, balance=300)
expire_datetime = timezone.now() + timedelta(seconds=3600)
expire_datetime.isoformat()
self.application_access_token, _created = AccessToken.objects.update_or_create(
token=self.application_access_token,
defaults={
"user": self.application_user,
"client_id": self.client_id,
# "client_owner": owner,
"application_id": self.application_uuid,
"scope": ' '.join(self.scopes),
"expires": expire_datetime.isoformat(),
},
)
self.payer_access_token, _created = AccessToken.objects.update_or_create(
token=self.payer_access_token,
defaults={
"user": self.payer_user,
"client_id": self.client_id,
# "client_owner": owner,
"application_id": self.application_uuid,
"scope": ' '.join(self.scopes),
"expires": expire_datetime.isoformat(),
},
)
self.application, _created = Application.objects.get_or_create(
client_id=self.client_id,
uuid=self.application_uuid,
)
self.wallet_category1, _ = Category.objects.get_or_create(application=self.application, uuid=self.wallet_category1_uuid)
self.wallet_category2, _ = Category.objects.get_or_create(application=self.application, uuid=self.wallet_category2_uuid)
self.application_wallet1, _ = Wallet.objects.get_or_create(owner_uuid=self.application_uuid, owner_type=TypeChoices.APPLICATION, balance=300, category=self.wallet_category1)
self.application_wallet2, _ = Wallet.objects.get_or_create(owner_uuid=self.application_uuid, owner_type=TypeChoices.APPLICATION, balance=300, category=self.wallet_category2)
self.payee_wallet1, _ = Wallet.objects.get_or_create(owner_uuid=self.payee_uuid, owner_type=TypeChoices.USER, category=self.wallet_category1)
self.payee_wallet2, _ = Wallet.objects.get_or_create(owner_uuid=self.payee_uuid, owner_type=TypeChoices.USER, category=self.wallet_category2)
self.payer_wallet1, _ = Wallet.objects.get_or_create(owner_uuid=self.payer_uuid, owner_type=TypeChoices.USER, balance=300, category=self.wallet_category1)
self.payer_wallet2, _ = Wallet.objects.get_or_create(owner_uuid=self.payer_uuid, owner_type=TypeChoices.USER, balance=300, category=self.wallet_category2)
def tearDown(self):
super().tearDown()
def _create_authorization_header(self, token):
return "Bearer {0}".format(token)
def test_authentication_allow(self):
auth = self._create_authorization_header(self.application_access_token.token)
response = self.client.get(reverse('wallet:user_wallet_balance_api'), HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance')
def test_authentication_expired_token(self):
access_token = '34naVsdKCbKNOhnElPyXcrwSnqqFbm'
expired_access_token, _created = AccessToken.objects.update_or_create(
token=access_token,
defaults={
"user": self.application_user,
"client_id": self.client_id,
# "client_owner": owner,
"application_id": self.application_uuid,
"scope": ' '.join(self.scopes),
"expires": (timezone.now() - timedelta(seconds=3600)).isoformat(),
},
)
auth = self._create_authorization_header(expired_access_token.token)
response = self.client.get(reverse('wallet:user_wallet_balance_api'), HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 401)
# def test_authentication_disallow(self):
# auth = self._create_authorization_header('fake_token')
# response = self.client.get(reverse('wallet:user_wallet_balance_api'), HTTP_AUTHORIZATION=auth)
# self.assertEqual(response.status_code, 401)
def test_deposit_flow_success(self):
auth = self._create_authorization_header(self.application_access_token.token)
# Checking wallets initials values(application balance = 300 and payee balance = zero)
response = self.client.get(reverse('wallet:application_wallet_balance_api'), HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance')
self.assertEqual(response.json()['balance'], 300)
params = {
'uuid': self.payee_uuid,
}
response = self.client.get(reverse('wallet:application_user_wallet_balance_api', kwargs=params), HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance')
self.assertEqual(response.json()['balance'], 0)
# Deposit process
data = {
'amount': 100,
'payee_id': self.payee_uuid,
'payee_type': TypeChoices.USER
}
response = self.client.post(reverse('wallet:application_deposit_submit_api'), data=data, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 201)
json_response = response.json()
params = {
'uuid': json_response['uuid']
}
response = self.client.get(reverse('wallet:application_deposit_verify_api', kwargs=params), HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['state'], 5)
# Desired results
response = self.client.get(reverse('wallet:application_wallet_balance_api'), HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance')
self.assertEqual(response.json()['balance'], 200)
params = {
'uuid': self.payee_uuid,
}
response = self.client.get(reverse('wallet:application_user_wallet_balance_api', kwargs=params), HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance')
self.assertEqual(response.json()['balance'], 100)
def test_deposit_flow_category_success(self):
auth = self._create_authorization_header(self.application_access_token.token)
# Checking wallets initials values(application balance = 300 and payee balance = zero)
kwargs_category_uuid = {
'category_uuid': self.wallet_category1_uuid
}
response = self.client.get(reverse('wallet:application_wallet_balance_api', kwargs=kwargs_category_uuid), HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance')
self.assertEqual(response.json()['balance'], 300)
kwargs_category_uuid_payee_uuid = {
'category_uuid': self.wallet_category1_uuid,
'uuid': self.payee_uuid,
}
response = self.client.get(reverse('wallet:application_user_wallet_balance_api', kwargs=kwargs_category_uuid_payee_uuid), HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance')
self.assertEqual(response.json()['balance'], 0)
# Deposit process
data = {
'amount': 100,
'payee_id': self.payee_uuid,
'payee_type': TypeChoices.USER
}
response = self.client.post(reverse('wallet:application_deposit_submit_api', kwargs=kwargs_category_uuid), data=data, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 201)
json_response = response.json()
kwargs_category_uuid_transaction_uuid = {
'category_uuid': self.wallet_category1_uuid,
'uuid': json_response['uuid'],
}
response = self.client.get(reverse('wallet:application_deposit_verify_api', kwargs=kwargs_category_uuid_transaction_uuid), HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['state'], 5)
# Desired results
response = self.client.get(reverse('wallet:application_wallet_balance_api', kwargs=kwargs_category_uuid), HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance')
self.assertEqual(response.json()['balance'], 200)
kwargs_category_uuid_payee_uuid = {
'category_uuid': self.wallet_category2_uuid,
'uuid': self.payee_uuid,
}
response = self.client.get(reverse('wallet:application_user_wallet_balance_api', kwargs=kwargs_category_uuid_payee_uuid), HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance')
self.assertEqual(response.json()['balance'], 0)
def test_deposit_flow_insufficient_balance(self):
auth = self._create_authorization_header(self.application_access_token.token)
# Checking wallets initials values(application balance = 300 and payee balance = zero)
response = self.client.get(reverse('wallet:application_wallet_balance_api'), HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance')
self.assertEqual(response.json()['balance'], 300)
params = {
'uuid': self.payee_uuid,
}
response = self.client.get(reverse('wallet:application_user_wallet_balance_api', kwargs=params), HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance')
self.assertEqual(response.json()['balance'], 0)
# Deposit process
data = {
'amount': 500,
'payee_id': self.payee_uuid,
'payee_type': TypeChoices.USER
}
response = self.client.post(reverse('wallet:application_deposit_submit_api'), data=data, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 500)
@override_settings(ALLOWED_NEGATIVE_BALANCE_APPLICATIONS=['b14e8b86-8f4a-44d9-b29d-badceb470053'])
def test_deposit_flow_skip_insufficient_balance_for_allowed_negative_balance_applications(self):
auth = self._create_authorization_header(self.application_access_token.token)
# Checking wallets initials values(application balance = 300 and payee balance = zero)
response = self.client.get(reverse('wallet:application_wallet_balance_api'), HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance')
self.assertEqual(response.json()['balance'], 300)
params = {
'uuid': self.payee_uuid,
}
response = self.client.get(reverse('wallet:application_user_wallet_balance_api', kwargs=params), HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance')
self.assertEqual(response.json()['balance'], 0)
# Deposit process
data = {
'amount': 500,
'payee_id': self.payee_uuid,
'payee_type': TypeChoices.USER
}
response = self.client.post(reverse('wallet:application_deposit_submit_api'), data=data, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 201)
def test_deposit_flow_zero_amount(self):
auth = self._create_authorization_header(self.application_access_token.token)
# Deposit process
data = {
'amount': 0,
'payee_id': self.payee_uuid,
'payee_type': TypeChoices.USER
}
response = self.client.post(reverse('wallet:application_deposit_submit_api'), data=data, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 400)
def test_deposit_flow_negative_amount(self):
auth = self._create_authorization_header(self.application_access_token.token)
# Deposit process
data = {
'amount': -100,
'payee_id': self.payee_uuid,
'payee_type': TypeChoices.USER
}
response = self.client.post(reverse('wallet:application_deposit_submit_api'), data=data, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 400)
def test_deposit_flow_prevent_double_spending(self):
auth = self._create_authorization_header(self.application_access_token.token)
# Checking wallets initials values(application balance = 300 and payee balance = zero)
response = self.client.get(reverse('wallet:application_wallet_balance_api'), HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance')
self.assertEqual(response.json()['balance'], 300)
params = {
'uuid': self.payee_uuid,
}
response = self.client.get(reverse('wallet:application_user_wallet_balance_api', kwargs=params), HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance')
self.assertEqual(response.json()['balance'], 0)
# Deposit process
data = {
'amount': 200,
'payee_id': self.payee_uuid,
'payee_type': TypeChoices.USER
}
response = self.client.post(reverse('wallet:application_deposit_submit_api'), data=data, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 201)
response = self.client.post(reverse('wallet:application_deposit_submit_api'), data=data, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 500)
def test_withdraw_flow_success(self):
auth = self._create_authorization_header(self.payer_access_token.token)
# Checking wallets initials values(application balance = 300 and payee balance = zero)
response = self.client.get(reverse('wallet:application_wallet_balance_api'), HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance')
self.assertEqual(response.json()['balance'], 300)
params = {
'uuid': self.payer_uuid,
}
response = self.client.get(reverse('wallet:application_user_wallet_balance_api', kwargs=params), HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance')
self.assertEqual(response.json()['balance'], 300)
# Deposit process
data = {
'amount': 100,
'payer_id': self.payer_uuid,
'payer_type': TypeChoices.USER
}
response = self.client.post(reverse('wallet:application_withdraw_submit_api'), data=data, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 201)
json_response = response.json()
params = {
'uuid': json_response['uuid']
}
response = self.client.get(reverse('wallet:application_withdraw_verify_api', kwargs=params), HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['state'], 5)
# Desired results
response = self.client.get(reverse('wallet:application_wallet_balance_api'), HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance')
self.assertEqual(response.json()['balance'], 400)
params = {
'uuid': self.payer_uuid,
}
response = self.client.get(reverse('wallet:application_user_wallet_balance_api', kwargs=params), HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance')
self.assertEqual(response.json()['balance'], 200)
def test_withdraw_flow_category_success(self):
auth = self._create_authorization_header(self.payer_access_token.token)
# Checking wallets initials values(application balance = 300 and payee balance = zero)
kwargs_category_uuid = {
'category_uuid': self.wallet_category1_uuid
}
response = self.client.get(reverse('wallet:application_wallet_balance_api', kwargs=kwargs_category_uuid), HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance')
self.assertEqual(response.json()['balance'], 300)
kwargs_category_uuid_payer_uuid = {
'category_uuid': self.wallet_category1_uuid,
'uuid': self.payer_uuid,
}
response = self.client.get(reverse('wallet:application_user_wallet_balance_api', kwargs=kwargs_category_uuid_payer_uuid), HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance')
self.assertEqual(response.json()['balance'], 300)
# Deposit process
data = {
'amount': 100,
'payer_id': self.payer_uuid,
'payer_type': TypeChoices.USER
}
response = self.client.post(reverse('wallet:application_withdraw_submit_api', kwargs=kwargs_category_uuid), data=data, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 201)
json_response = response.json()
kwargs_category_uuid_transaction_uuid = {
'category_uuid': self.wallet_category1_uuid,
'uuid': json_response['uuid'],
}
response = self.client.get(reverse('wallet:application_withdraw_verify_api', kwargs=kwargs_category_uuid_transaction_uuid), HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['state'], 5)
# Desired results
response = self.client.get(reverse('wallet:application_wallet_balance_api', kwargs=kwargs_category_uuid), HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance')
self.assertEqual(response.json()['balance'], 400)
response = self.client.get(reverse('wallet:application_user_wallet_balance_api', kwargs=kwargs_category_uuid_payer_uuid), HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance')
self.assertEqual(response.json()['balance'], 200)
kwargs_category_uuid_payer_uuid = {
'category_uuid': self.wallet_category2_uuid,
'uuid': self.payer_uuid,
}
response = self.client.get(reverse('wallet:application_user_wallet_balance_api', kwargs=kwargs_category_uuid_payer_uuid), HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance')
self.assertEqual(response.json()['balance'], 300)
def test_withdraw_flow_zero_amount(self):
auth = self._create_authorization_header(self.payer_access_token.token)
# Deposit process
data = {
'amount': 0,
'payer_id': self.payer_uuid,
'payer_type': TypeChoices.USER
}
response = self.client.post(reverse('wallet:application_withdraw_submit_api'), data=data,
HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 400)
def test_withdraw_flow_negative_amount(self):
auth = self._create_authorization_header(self.payer_access_token.token)
# Deposit process
data = {
'amount': -500,
'payer_id': self.payer_uuid,
'payer_type': TypeChoices.USER
}
response = self.client.post(reverse('wallet:application_withdraw_submit_api'), data=data,
HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 400)
def test_withdraw_flow_prevent_double_spending(self):
auth = self._create_authorization_header(self.payer_access_token.token)
# Checking wallets initials values(application balance = 300 and payee balance = zero)
response = self.client.get(reverse('wallet:application_wallet_balance_api'), HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance')
self.assertEqual(response.json()['balance'], 300)
params = {
'uuid': self.payer_uuid,
}
response = self.client.get(reverse('wallet:application_user_wallet_balance_api', kwargs=params), HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance')
self.assertEqual(response.json()['balance'], 300)
# Deposit process
data = {
'amount': 200,
'payer_id': self.payer_uuid,
'payer_type': TypeChoices.USER
}
response = self.client.post(reverse('wallet:application_withdraw_submit_api'), data=data, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 201)
response = self.client.post(reverse('wallet:application_withdraw_submit_api'), data=data, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 500)

View file

@ -8,8 +8,13 @@ app_name = "wallet"
user_urlpatterns = [ user_urlpatterns = [
path('transactions/', user_views.TransactionListView.as_view(), name='user_transaction_list_page'), path('transactions/', user_views.TransactionListView.as_view(), name='user_transaction_list_page'),
path('transactions/<pk>', user_views.TransactionDetailView.as_view(), name='user_transaction_detail_page'), path('transactions/<pk>', user_views.TransactionDetailView.as_view(), name='user_transaction_detail_page'),
path('api/wallet/balance', user_views.MyWalletBalanceView.as_view(), name='user_wallet_balance_api'), path('api/wallet/balance', user_views.CategoryMyWalletBalanceView.as_view(), name='user_wallet_balance_api'),
path('api/transactions/', user_views.TransactionList.as_view(), name='transaction_list_api'), path('api/transactions/', user_views.CategoryTransactionListView.as_view(), name='transaction_list_api'),
path('api/user/<category_uuid>/balance', user_views.CategoryMyWalletBalanceView.as_view(), name='user_wallet_balance_api'),
path('api/user/<category_uuid>/transactions/', user_views.CategoryTransactionListView.as_view(), name='transaction_list_api'),
# path('api/transactions/<uuid>', TransactionDetail.as_view(), name='transaction_detail_api'), # path('api/transactions/<uuid>', TransactionDetail.as_view(), name='transaction_detail_api'),
# path('api/transactions/<uuid>/pay', TransactionPay.as_view(), name='transaction_pay_api'), # path('api/transactions/<uuid>/pay', TransactionPay.as_view(), name='transaction_pay_api'),
# path('api/transactions/<uuid>/receipt', TransactionReceipt.as_view(), name='transaction_receipt_api'), # path('api/transactions/<uuid>/receipt', TransactionReceipt.as_view(), name='transaction_receipt_api'),
@ -25,18 +30,33 @@ user_urlpatterns = [
] ]
application_urlpatterns = [ application_urlpatterns = [
path('api/application/balance', application_views.BalanceAPIView.as_view(), name='application_wallet_balance_api'), path('api/application/balance', application_views.CategoryBalanceAPIView.as_view(), name='application_wallet_balance_api'),
path('api/application/user_balance/<uuid>', application_views.UserBalanceAPIView.as_view(), name='application_user_wallet_balance_api'), path('api/application/user_balance/<uuid>', application_views.CategoryUserBalanceAPIView.as_view(), name='application_user_wallet_balance_api'),
path('api/application/deposit/', application_views.DepositSubmitAPIView.as_view(), name='application_deposit_submit_api'), path('api/application/deposit/', application_views.CategoryDepositSubmitAPIView.as_view(), name='application_deposit_submit_api'),
path('api/application/deposit/<uuid>/verify', application_views.DepositVerifyAPIView.as_view(), name='application_deposit_verify_api'), path('api/application/deposit/<uuid>/verify', application_views.CategoryDepositVerifyAPIView.as_view(), name='application_deposit_verify_api'),
path('api/application/withdraw/', application_views.WithdrawSubmitAPIView.as_view(), name='application_withdraw_submit_api'), path('api/application/withdraw/', application_views.CategoryWithdrawSubmitAPIView.as_view(), name='application_withdraw_submit_api'),
path('api/application/withdraw/<uuid>/verify', application_views.WithdrawVerifyAPIView.as_view(), name='application_withdraw_verify_api'), path('api/application/withdraw/<uuid>/verify', application_views.CategoryWithdrawVerifyAPIView.as_view(), name='application_withdraw_verify_api'),
path('api/application/', application_views.TransactionListAPIView.as_view(), name='application_transaction_list_api'), path('api/application/', application_views.CategoryTransactionListAPIView.as_view(), name='application_transaction_list_api'),
path('api/application/<uuid>/', application_views.TransactionDetailAPIView.as_view(), name='application_transaction_detail'), path('api/application/<uuid>/', application_views.CategoryTransactionDetailAPIView.as_view(), name='application_transaction_detail'),
path('api/application/<category_uuid>/balance', application_views.CategoryBalanceAPIView.as_view(), name='application_wallet_balance_api'),
path('api/application/<category_uuid>/user/<uuid>/balance', application_views.CategoryUserBalanceAPIView.as_view(), name='application_user_wallet_balance_api'),
path('api/application/<category_uuid>/deposit/', application_views.CategoryDepositSubmitAPIView.as_view(), name='application_deposit_submit_api'),
path('api/application/<category_uuid>/deposit/<uuid>/verify', application_views.CategoryDepositVerifyAPIView.as_view(), name='application_deposit_verify_api'),
path('api/application/<category_uuid>/withdraw/', application_views.CategoryWithdrawSubmitAPIView.as_view(), name='application_withdraw_submit_api'),
path('api/application/<category_uuid>/withdraw/<uuid>/verify', application_views.CategoryWithdrawVerifyAPIView.as_view(), name='application_withdraw_verify_api'),
path('api/application/<category_uuid>/', application_views.CategoryTransactionListAPIView.as_view(), name='application_transaction_list_api'),
path('api/application/<category_uuid>/<uuid>/', application_views.CategoryTransactionDetailAPIView.as_view(), name='application_transaction_detail'),
] ]

View file

@ -3,11 +3,12 @@ from oauth2_provider.contrib.rest_framework import TokenHasScope, IsAuthenticate
from rest_framework import generics, permissions, mixins from rest_framework import generics, permissions, mixins
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
from rest_framework.generics import get_object_or_404 from rest_framework.generics import get_object_or_404
from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from apps.users.models import User from apps.users.models import User
from apps.wallet.constans import TypeChoices, StateChoices from apps.wallet.constans import TypeChoices, StateChoices
from apps.wallet.models import Transaction from apps.wallet.models import Transaction, Category, Wallet
from apps.wallet.serializers import ApplicationDepositSerializer, ApplicationWithdrawSerializer, \ from apps.wallet.serializers import ApplicationDepositSerializer, ApplicationWithdrawSerializer, \
ApplicationTransactionSerializer, ApplicationBalanceSerializer, MyWalletSerializer ApplicationTransactionSerializer, ApplicationBalanceSerializer, MyWalletSerializer
@ -20,6 +21,10 @@ def get_application(request):
return application return application
def get_wallet_category(request: Request):
pass
class BalanceAPIView(generics.RetrieveAPIView): class BalanceAPIView(generics.RetrieveAPIView):
''' '''
API for getting specified user or application balance. API for getting specified user or application balance.
@ -75,6 +80,7 @@ class TransactionListAPIView(generics.ListAPIView):
Q(payee_id=application.pk, payee_type=TypeChoices.APPLICATION.value) Q(payee_id=application.pk, payee_type=TypeChoices.APPLICATION.value)
).all() ).all()
# from application to another entity # from application to another entity
# TODO: better name # TODO: better name
class DepositSubmitAPIView(generics.CreateAPIView): class DepositSubmitAPIView(generics.CreateAPIView):
@ -165,3 +171,203 @@ class WithdrawVerifyAPIView(generics.RetrieveAPIView):
serializer = self.get_serializer(instance) serializer = self.get_serializer(instance)
return Response(serializer.data) return Response(serializer.data)
class CategoryBalanceAPIView(generics.RetrieveAPIView):
'''
API for getting specified user or application balance.
'''
permission_classes = [TokenHasScope]
serializer_class = ApplicationBalanceSerializer
required_scopes = ['wallet.application:get_balance']
def get_object(self):
category_uuid = self.kwargs.get('category_uuid')
if category_uuid:
category = get_object_or_404(Category.objects.all(), uuid=category_uuid)
else:
category = None
application = get_application(self.request)
wallet, created = Wallet.objects.get_or_create(owner_uuid=application.uuid, owner_type=TypeChoices.APPLICATION,
category=category)
return wallet
class CategoryUserBalanceAPIView(generics.RetrieveAPIView):
'''
API for getting specified user or application balance.
'''
permission_classes = [IsAuthenticatedOrTokenHasScope]
serializer_class = MyWalletSerializer
required_scopes = ['wallet.application:get_user_balance']
def get_object(self):
category_uuid = self.kwargs.get('category_uuid')
if category_uuid:
category = get_object_or_404(Category.objects.all(), uuid=category_uuid)
else:
category = None
user_uuid = self.kwargs.get('uuid')
user, created = User.objects.get_or_create(uuid=user_uuid)
application = get_application(self.request)
wallet, created = Wallet.objects.get_or_create(owner_uuid=user_uuid, owner_type=TypeChoices.USER,
category=category)
return wallet
class CategoryTransactionDetailAPIView(generics.RetrieveAPIView):
permission_classes = [TokenHasScope]
serializer_class = ApplicationTransactionSerializer
lookup_field = 'uuid'
required_scopes = ['wallet.application:get_transaction_detail']
def get_queryset(self):
# TODO: what about category
application = get_application(self.request)
return Transaction.objects.filter(Q(application=application) |
Q(payer_wallet__owner_uuid=application.pk,
payer_wallet__owner_type=TypeChoices.APPLICATION) |
Q(payee_wallet__owner_uuid=application.pk,
payee_wallet__owner_type=TypeChoices.APPLICATION)
).all()
class CategoryTransactionListAPIView(generics.ListAPIView):
permission_classes = [TokenHasScope]
serializer_class = ApplicationTransactionSerializer
lookup_field = 'uuid'
required_scopes = ['wallet.application:get_transaction_list']
def get_queryset(self):
# TODO: what about category?
application = get_application(self.request)
return Transaction.objects.filter(Q(application=application) |
Q(payer_wallet__owner_uuid=application.pk,
payer_wallet__owner_type=TypeChoices.APPLICATION) |
Q(payee_wallet__owner_uuid=application.pk,
payee_wallet__owner_type=TypeChoices.APPLICATION)
).all()
# from application to another entity
# TODO: better name
class CategoryDepositSubmitAPIView(generics.CreateAPIView):
permission_classes = [IsAuthenticatedOrTokenHasScope]
serializer_class = ApplicationDepositSerializer
required_scopes = ['wallet.application.deposit:submit']
def get_queryset(self):
category_uuid = self.kwargs.get('category_uuid')
if category_uuid:
category = get_object_or_404(Category.objects.all(), uuid=category_uuid)
else:
category = None
application = get_application(self.request)
wallet, created = Wallet.objects.get_or_create(owner_uuid=application.uuid, owner_type=TypeChoices.APPLICATION,
category=category)
return Transaction.objects.filter(application=application, payer_wallet=wallet).all()
def perform_create(self, serializer):
# TODO: get_or_create instead of create if uuid given
# TODO: it is possible to merage create and submit in one step for improve performance
application = get_application(self.request)
category_uuid = self.kwargs.get('category_uuid')
if category_uuid:
category = get_object_or_404(Category.objects.all(), uuid=category_uuid)
else:
category = None
payee_id = serializer.validated_data.pop('payee_id')
payee_type = serializer.validated_data.pop('payee_type')
payer_wallet, _ = Wallet.objects.get_or_create(owner_uuid=application.uuid, owner_type=TypeChoices.APPLICATION,
category=category)
payee_wallet, _ = Wallet.objects.get_or_create(owner_uuid=payee_id, owner_type=payee_type, category=category)
instance = serializer.save(payer_wallet=payer_wallet, application=application, payee_wallet=payee_wallet)
instance.category_submit()
class CategoryDepositVerifyAPIView(generics.RetrieveAPIView):
permission_classes = [IsAuthenticatedOrTokenHasScope]
serializer_class = ApplicationDepositSerializer
lookup_field = 'uuid'
required_scopes = ['wallet.application.deposit:verify']
def get_queryset(self):
application = get_application(self.request)
category_uuid = self.kwargs.get('category_uuid')
if category_uuid:
category = get_object_or_404(Category.objects.all(), uuid=category_uuid)
else:
category = None
wallet = Wallet.objects.get(owner_uuid=application.uuid, owner_type=TypeChoices.APPLICATION, category=category)
return Transaction.objects.filter(application=application, payer_wallet=wallet).all()
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
if instance.state == StateChoices.PENDING:
instance.category_verify()
serializer = self.get_serializer(instance)
return Response(serializer.data)
# from another entity to application
class CategoryWithdrawSubmitAPIView(generics.CreateAPIView):
permission_classes = [TokenHasScope]
serializer_class = ApplicationWithdrawSerializer
required_scopes = ['wallet.application.withdraw:submit']
def get_queryset(self):
category_uuid = self.kwargs.get('category_uuid')
if category_uuid:
category = get_object_or_404(Category.objects.all(), uuid=category_uuid)
else:
category = None
application = get_application(self.request)
wallet, created = Wallet.objects.get_or_create(owner_uuid=application.uuid, owner_type=TypeChoices.APPLICATION,
category=category)
return Transaction.objects.filter(application=application, wallet=wallet).all()
def perform_create(self, serializer):
# TODO: get_or_create instead of create if uuid given
# TODO: it is possible to merage create and submit in one step for improve performance
application = get_application(self.request)
category_uuid = self.kwargs.get('category_uuid')
if category_uuid:
category = get_object_or_404(Category.objects.all(), uuid=category_uuid)
else:
category = None
payer_id = serializer.validated_data.pop('payer_id')
payer_type = serializer.validated_data.pop('payer_type')
payee_wallet, _ = Wallet.objects.get_or_create(owner_uuid=application.uuid, owner_type=TypeChoices.APPLICATION,
category=category)
payer_wallet, _ = Wallet.objects.get_or_create(owner_uuid=payer_id, owner_type=payer_type, category=category)
instance = serializer.save(payer_wallet=payer_wallet, application=application, payee_wallet=payee_wallet)
instance.category_submit()
class CategoryWithdrawVerifyAPIView(generics.RetrieveAPIView):
permission_classes = [TokenHasScope]
serializer_class = ApplicationWithdrawSerializer
lookup_field = 'uuid'
required_scopes = ['wallet.application.withdraw:verify']
def get_queryset(self):
application = get_application(self.request)
category_uuid = self.kwargs.get('category_uuid')
if category_uuid:
category = get_object_or_404(Category.objects.all(), uuid=category_uuid)
else:
category = None
wallet = Wallet.objects.get(owner_uuid=application.uuid, owner_type=TypeChoices.APPLICATION, category=category)
return Transaction.objects.filter(application=application, payee_wallet=wallet).all()
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
if instance.state == StateChoices.PENDING:
instance.category_verify()
serializer = self.get_serializer(instance)
return Response(serializer.data)

View file

@ -8,11 +8,12 @@ from django_filters.rest_framework import DjangoFilterBackend
from oauth2_provider.contrib.rest_framework import TokenHasScope, IsAuthenticatedOrTokenHasScope from oauth2_provider.contrib.rest_framework import TokenHasScope, IsAuthenticatedOrTokenHasScope
from rest_framework import generics, permissions from rest_framework import generics, permissions
from rest_framework.generics import get_object_or_404
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.response import Response from rest_framework.response import Response
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
from apps.wallet.models import Transaction from apps.wallet.models import Transaction, Category, Wallet
from apps.wallet.serializers import UserTransactionSerializer, DepositSerializer, WithdrawSerializer, \ from apps.wallet.serializers import UserTransactionSerializer, DepositSerializer, WithdrawSerializer, \
MyWalletSerializer MyWalletSerializer
from ..constans import StateChoices, TypeChoices from ..constans import StateChoices, TypeChoices
@ -222,3 +223,60 @@ class TransactionDetailView(UpdateView):
result = transaction.receipt() result = transaction.receipt()
messages.error(self.request, _('success.')) messages.error(self.request, _('success.'))
return super().form_valid(form) return super().form_valid(form)
class CategoryMyWalletBalanceView(generics.RetrieveAPIView):
'''
API for getting authenticated user balance.
'''
permission_classes = [IsAuthenticatedOrTokenHasScope]
serializer_class = MyWalletSerializer
required_scopes = ['wallet.wallet:get_balance']
def get_object(self):
user = self.request.user
category_uuid = self.kwargs.get('category_uuid')
if category_uuid:
category = get_object_or_404(Category.objects.all(), uuid=category_uuid)
else:
category = None
wallet, created = Wallet.objects.get_or_create(owner_uuid=user.uuid, owner_type=TypeChoices.USER,
category=category)
return wallet
# this is application create invoice view
class CategoryTransactionListView(generics.ListAPIView):
'''
GET: API for getting list of transactions for authenticated user
POST: API for creating transaction for transferring funds from authenticated user to application owner. (deprecated)
args:
- user: authenticated user
'''
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
serializer_class = UserTransactionSerializer
required_alternate_scopes = {
"GET": [['wallet.user:transaction_list']],
# "POST": [['wallet.invoice:create']],
}
filter_backends = (DjangoFilterBackend,)
filterset_class = UserTransactionFilter
def get_queryset(self):
user = self.request.user
category_uuid = self.kwargs.get('category_uuid')
if category_uuid:
category = get_object_or_404(Category.objects.all(), uuid=category_uuid)
else:
category = None
wallet, created = Wallet.objects.get_or_create(owner_uuid=user.uuid, owner_type=TypeChoices.USER,
category=category)
return Transaction.objects.filter(Q(payee_wallet=wallet) |
Q(payer_wallet=wallet)).all()

15
utils/models.py Normal file
View file

@ -0,0 +1,15 @@
import json
import uuid
from datetime import datetime
from django.db import models
from django.utils.translation import gettext_lazy as _
class BaseModel(models.Model):
id = None
uuid = models.UUIDField(primary_key=True, editable=True, default=uuid.uuid4, unique=True, db_index=True)
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
updated_at = models.DateTimeField(auto_now=True, db_index=True)
class Meta:
abstract = True