Compare commits

...
Sign in to create a new pull request.

69 commits

Author SHA1 Message Date
fba0cf3b64 Merge branch 'master' of https://git.addwin.ir/addwin/wallet 2026-08-19 15:49:19 +03:30
2fae79eb04 FIX(ads): fix query params
fix query params
2026-08-19 15:46:25 +03:30
11327db537 Merge pull request 'update admin panel calendar' (#1) from feature/admin-panel-calendar into master
Reviewed-on: #1
2026-08-15 05:48:40 -04:00
1d555cd031 update admin panel calendar 2026-08-01 15:34:01 +03:30
fb4c79c5c6 run by docker via base:debian13 2026-05-02 13:16:27 +03:30
5b278bddd2 run by docker via base:debian13 2026-05-01 23:31:25 +03:30
8328814dca FEATURE(wallet): add export excel to panel admin
add export excel to panel admin
2026-04-27 13:58:47 +03:30
5d897b0917 FEATURE(wallet): add DateTimeRangeFilter to panel admin
add DateTimeRangeFilter to panel admin
2026-04-27 13:09:34 +03:30
5eff037d10 FEATURE(wallet): add DateTimeRangeFilter to panel admin
add DateTimeRangeFilter to panel admin
2026-04-27 13:02:37 +03:30
Sayyid Hamid Mahdavi
d9b7dbe1b2 new Dockerfile 2026-04-26 14:07:11 +03:30
Sayyid Hamid Mahdavi
e6eafbd6dc better settings 2026-04-26 12:35:29 +03:30
2d8e0da6fa FIX(wallet): export excel
export excel
2026-04-20 12:23:52 +03:30
7b1e9376b4 FIX(wallet): get_user_detailed_info
get_user_detailed_info
2026-04-20 12:21:58 +03:30
615a568294 FIX(wallet): update account client
fix update account client
2026-04-20 12:16:50 +03:30
a8b32a16f4 FIX(settlement): add rollback_deposit_to_payee_balance
add rollback_deposit_to_payee_balance
2026-02-17 15:44:27 +03:30
Sayyid Hamid Mahdavi
0a179bf350 admin 2026-02-15 12:46:50 +03:30
e27fe27bf0 Merge pull request 'FIX(wallet): add cancel state and add success to rollback init states' (#8) from FEATURE/new-commission-calculation-and-toman-api into master
Reviewed-on: https://git.mindplus-dev.ir/sahama/wallet/pulls/8
2026-02-14 08:13:53 -05:00
e3413e33ab FIX(wallet): add CSRF_TRUSTED_ORIGINS
add CSRF_TRUSTED_ORIGINS
2026-02-14 16:31:03 +03:30
7e378e5b54 FIX(wallet): add cancel state and add success to rollback init states
add cancel state and add success to rollback init states
2026-02-14 15:24:46 +03:30
Sayyid Hamid Mahdavi
e5f22d1659 Dockerfile 2026-02-12 12:10:28 +03:30
8acf7bc6c1 Merge pull request 'FIX(wallet): hot fix' (#7) from FIX/gte-in-withdraw into master
Reviewed-on: https://git.mindplus-dev.ir/sahama/wallet/pulls/7
2026-02-09 05:31:42 -05:00
fd244950e1 FIX(wallet): hot fix
hot fix
2026-02-09 13:56:43 +03:30
Sayyid Hamid Mahdavi
cc5b0568a9 role 2026-02-07 16:57:06 +03:30
Sayyid Hamid Mahdavi
a5639b5426 Merge branch 'master' of git.mindplus-dev.ir:sahama/wallet 2026-02-07 16:18:50 +03:30
Sayyid Hamid Mahdavi
2caae02a3c more fields 2026-02-07 16:18:20 +03:30
53cc3f67ee FIX(wallet): remove import
remove import
2026-02-07 16:11:13 +03:30
fb00595d23 Merge pull request 'FIX(wallet): change to rollback status' (#6) from FIX/change-to-rollback-status into master
Reviewed-on: https://git.mindplus-dev.ir/sahama/wallet/pulls/6
2026-02-07 06:31:13 -05:00
Sayyid Hamid Mahdavi
1d961efb69 urls 2026-02-07 13:56:03 +03:30
Sayyid Hamid Mahdavi
15fe0d4531 grouped transactions 2026-02-07 13:47:10 +03:30
Sayyid Hamid Mahdavi
e045ffd9b1 grouped transactions 2026-02-07 13:21:01 +03:30
Sayyid Hamid Mahdavi
27db599e8c new Dockerfile 2026-02-07 12:37:14 +03:30
Sayyid Hamid Mahdavi
95df80f593 new Dockerfile 2026-02-07 12:30:46 +03:30
Sayyid Hamid Mahdavi
1e3a69ece1 new Dockerfile 2026-02-07 12:25:05 +03:30
Sayyid Hamid Mahdavi
ff3cbcf162 new Dockerfile 2026-02-07 12:17:19 +03:30
Sayyid Hamid Mahdavi
7e8e35c746 new Dockerfile 2026-02-07 12:17:13 +03:30
Sayyid Hamid Mahdavi
b8e0cd09fc add migration file 2026-02-07 09:31:52 +03:30
Sayyid Hamid Mahdavi
c24b52e341 add reference field 2026-02-07 09:28:06 +03:30
132a3afcbf FIX(wallet): change to rollback status
change to rollback status
2026-02-02 15:01:16 +03:30
Sayyid Hamid Mahdavi
4393e3878a add rollback api 2025-12-27 15:15:16 +03:30
Sayyid Hamid Mahdavi
bd3438528d new change state 2025-12-27 14:53:45 +03:30
Sayyid Hamid Mahdavi
dae2a2e518 add rollback to model 2025-12-27 14:35:26 +03:30
Sayyid Hamid Mahdavi
9183b1aa78 pass tests 2025-12-27 13:41:34 +03:30
Sayyid Hamid Mahdavi
a35bd6b202 increase time out 2025-12-09 10:19:23 +03:30
Sayyid Hamid Mahdavi
587a0d106b logger 2025-12-09 10:13:13 +03:30
Sayyid Hamid Mahdavi
a9935fe72c admin search bugfix 2025-12-03 14:42:17 +03:30
Sayyid Hamid Mahdavi
ce1b8ccd46 Merge remote-tracking branch 'origin/master' 2025-11-06 13:51:08 +03:30
Sayyid Hamid Mahdavi
d76d23ae7d bugfix 2025-11-06 13:51:00 +03:30
sahama
62fd479e60 Merge pull request 'fix(wallet): fix user wallet' (#5) from FIX/user-wallets into master
Reviewed-on: https://git.mindplus-dev.ir/sahama/wallet/pulls/5
Reviewed-by: sahama <sayyidhamidmahdavi@gmail.com>
2025-11-05 05:25:28 -05:00
29e89fe6cc fix(wallet): fix user wallet
fix user wallet
2025-11-05 13:51:08 +03:30
sahama
b281bcd914 Merge pull request 'fix(wallet): add logger' (#3) from FEATURE/drop_backward_compatibility into develop
Reviewed-on: https://git.mindplus-dev.ir/sahama/wallet/pulls/3
Reviewed-by: sahama <sayyidhamidmahdavi@gmail.com>
2025-11-04 05:14:28 -05:00
fd61b36375 fix(wallet): add logger
add logger
2025-11-04 13:31:53 +03:30
c0b3bb1889 fix(wallet): TODO
TODO
2025-11-02 12:33:59 +03:30
aee2c52a8c fix(wallet): account model admin
fix account model admin
2025-11-02 10:53:30 +03:30
51145bad53 fix(wallet): fix
fix
2025-10-29 18:18:05 +03:30
33a7ab386e fix(pg): uncomment
uncomment
2025-10-29 13:20:28 +03:30
a032eb1ccd fix(wallet): fix bugs
fix bugs
2025-10-29 11:04:43 +03:30
d508e74c7f fix(wallet): refactoring
refactoring
2025-10-28 15:01:58 +03:30
7cec791602 fix(wallet): adapt submit and verify process to new wallet, account concept
adapt submit and verify process to new wallet, account concept

passed tests
2025-10-28 12:15:40 +03:30
65c9c62484 fix(wallet): adapt submit and verify process to new wallet, account concept
adapt submit and verify process to new wallet, account concept

test_withdraw_flow_success
2025-10-28 10:54:44 +03:30
821bad13e7 fix(wallet): adapt submit and verify process to new wallet, account concept
adapt submit and verify process to new wallet, account concept

test_deposit_flow_prevent_double_spending
2025-10-27 15:46:16 +03:30
c6b2040af9 fix(wallet): adapt submit and verify process to new wallet, account concept
adapt submit and verify process to new wallet, account concept

test_deposit_flow_negative_amount
2025-10-27 15:09:30 +03:30
450c6f0191 fix(wallet): adapt submit and verify process to new wallet, account concept
adapt submit and verify process to new wallet, account concept

test_deposit_flow_skip_insufficient_balance_for_allowed_negative_balance_applications
2025-10-27 12:29:56 +03:30
c5ec20e1f0 fix(wallet): adapt submit and verify process to new wallet, account concept
adapt submit and verify process to new wallet, account concept

test_deposit_flow_insufficient_balance
2025-10-27 11:15:42 +03:30
db798dd348 fix(wallet): adapt submit and verify process to new wallet, account concept
adapt submit and verify process to new wallet, account concept
notice: add WALLET_RIAL_CATEGORY to .env
2025-10-26 17:44:59 +03:30
Sayyid Hamid Mahdavi
a84d93a25c wallet refactoring one more step 2025-10-25 15:06:21 +03:30
Sayyid Hamid Mahdavi
2c5087ee02 wallet refactoring one more step 2025-10-25 14:47:32 +03:30
Sayyid Hamid Mahdavi
b89487653f change wallet to account 2025-10-25 14:26:47 +03:30
Sayyid Hamid Mahdavi
c925100fd6 some cleanups 2025-10-23 17:25:15 +03:30
Sayyid Hamid Mahdavi
413a5497e4 some cleanups 2025-10-23 12:45:09 +03:30
75 changed files with 2382 additions and 925 deletions

1
.gitignore vendored
View file

@ -5,3 +5,4 @@ utils/delme.py
*.pyc *.pyc
.env .env
/venv/ /venv/
/.venv/

View file

@ -1,9 +1,12 @@
FROM debian:12 FROM base:debian13
ENV PYTHONUNBUFFERED 1 ENV PYTHONUNBUFFERED 1
WORKDIR /app WORKDIR /app
RUN date #RUN date
RUN apt update #RUN apt update
RUN apt install gnupg2 netcat-traditional libssl-dev libcrypto++-dev lsb-release python3-pip -y #RUN apt install gnupg2 netcat-traditional libssl-dev libcrypto++-dev lsb-release python3-pip -y
COPY requirements.txt /app/requirements.txt #RUN apt install binutils libproj-dev gdal-bin -y
RUN pip3 install --break-system-packages -r requirements.txt #COPY requirements.txt /app/requirements.txt
#RUN pip3 install --break-system-packages -r requirements.txt
#RUN pip3 install --break-system-packages django-admin-rangefilter==0.13.5
#RUN pip3 install --break-system-packages setuptools
#ENTRYPOINT ["./run.sh"] #ENTRYPOINT ["./run.sh"]

8
Dockerfile.build Normal file
View file

@ -0,0 +1,8 @@
FROM debian:13
ENV PYTHONUNBUFFERED 1
WORKDIR /app
RUN apt update
RUN apt install gnupg2 netcat-traditional libssl-dev libcrypto++-dev lsb-release python3-pip nano htop iputils-ping -y
COPY requirements.txt /app/requirements.txt
RUN pip3 install --break-system-packages -r requirements.txt
#ENTRYPOINT ["./run.sh"]

8
Dockerfile.run Normal file
View file

@ -0,0 +1,8 @@
FROM wallet:local_build
ENV PYTHONUNBUFFERED 1
WORKDIR /app
#RUN apt update
#RUN apt install gnupg2 netcat-traditional libssl-dev libcrypto++-dev lsb-release python3-pip nano htop iputils-ping -y
COPY requirements.txt /app/requirements.txt
RUN pip3 install --break-system-packages -r requirements.txt
#ENTRYPOINT ["./run.sh"]

View file

@ -0,0 +1,18 @@
# Generated by Django 6.0 on 2026-02-07 05:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='charge',
name='state',
field=models.IntegerField(choices=[(1, 'created'), (2, 'delayed'), (3, 'pending'), (4, 'incomplete'), (5, 'success'), (6, 'failed'), (7, 'expected_failure'), (8, 'ROLLED_BACK')], default=1),
),
]

View file

@ -5,6 +5,6 @@ app_name = "core"
urlpatterns = [ urlpatterns = [
path('', HomeView.as_view(), name='home'), path('', HomeView.as_view(), name='home'),
path('charge/<pk>/', ChargeView.as_view(), name='charge'), # path('charge/<pk>/', ChargeView.as_view(), name='charge'),
path('api/charge_create', ChargeRequestApiView.as_view(), name='charge_create_api'), # path('api/charge_create', ChargeRequestApiView.as_view(), name='charge_create_api'),
] ]

View file

@ -5,8 +5,8 @@ from django.contrib.admin.widgets import AdminDateWidget
from django.forms.fields import DateField, SplitDateTimeField, TimeField, DateTimeField from django.forms.fields import DateField, SplitDateTimeField, TimeField, DateTimeField
from django.utils.encoding import force_str from django.utils.encoding import force_str
from datetime import date as datetime_date, datetime as datetime_datetime from datetime import date as datetime_date, datetime as datetime_datetime
from jdatetime import GregorianToJalali, datetime as jalali_datetime # from jdatetime import GregorianToJalali, datetime as jalali_datetime
import jdatetime # import jdatetime
class JalaliDateField(DateField): class JalaliDateField(DateField):
def __init__(self, *, input_formats=None, **kwargs): def __init__(self, *, input_formats=None, **kwargs):

View file

@ -32,7 +32,7 @@ class ChargeRequestApiView(generics.CreateAPIView):
user = self.request.user user = self.request.user
serializer.save(user = user) serializer.save(user = user)
# TODO: refactor
class ChargeView(UpdateView): class ChargeView(UpdateView):
template_name = 'core/charge.html' template_name = 'core/charge.html'
form_class = ChargeForm form_class = ChargeForm

View file

@ -0,0 +1,17 @@
# Generated by Django 5.1.7 on 2025-10-23 09:31
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('gooyal_oauth2', '0006_accesstoken_token_checksum_refreshtoken_token_family_and_more'),
]
operations = [
migrations.RemoveField(
model_name='application',
name='balance',
),
]

View file

@ -22,7 +22,6 @@ class AccessToken(AbstractAccessToken):
class Application(AbstractApplication): class Application(AbstractApplication):
id = None id = None
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4, unique=True, db_index=True) uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4, unique=True, db_index=True)
balance = models.BigIntegerField(_('balance'), default=0)
class Meta: class Meta:
abstract = False abstract = False

View file

@ -21,7 +21,7 @@ class OAUTHLoginRequestForm(forms.ModelForm):
fields = ['agreement'] fields = ['agreement']
class AccountUpdateForm(forms.ModelForm): # class AccountUpdateForm(forms.ModelForm):
class Meta: # class Meta:
model = UserModel # model = UserModel
fields = ['iban'] # fields = ['iban']

View file

@ -0,0 +1,25 @@
# Generated by Django 5.1.7 on 2025-10-23 09:31
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('users', '0006_alter_user_balance'),
]
operations = [
migrations.RemoveField(
model_name='user',
name='balance',
),
migrations.RemoveField(
model_name='user',
name='iban',
),
migrations.RemoveField(
model_name='user',
name='iban_verified',
),
]

View file

@ -62,10 +62,6 @@ class User(AbstractUser):
null=True null=True
) )
last_checkout_request = models.DateTimeField(_('otp expire'), max_length=30, blank=True, null=True) last_checkout_request = models.DateTimeField(_('otp expire'), max_length=30, blank=True, null=True)
balance = models.PositiveBigIntegerField(_('balance'), default=0)
iban = models.CharField(null=True, max_length=30, blank=True)
iban_verified = models.BooleanField(null=True)
updated_at = models.DateTimeField(_('last update'), max_length=30, blank=True, null=True, auto_now=True) updated_at = models.DateTimeField(_('last update'), max_length=30, blank=True, null=True, auto_now=True)
last_login = models.DateTimeField(_('last login'), blank=True, null=True) last_login = models.DateTimeField(_('last login'), blank=True, null=True)

View file

@ -13,7 +13,8 @@ from rest_framework import generics
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
from apps.gooyal_oauth2.validators import OAuth2Validator from apps.gooyal_oauth2.validators import OAuth2Validator
from apps.users.forms import OAUTHLoginRequestForm, AccountUpdateForm from apps.users.forms import OAUTHLoginRequestForm
# from apps.users.forms import AccountUpdateForm
from apps.users.models import User, OAuthCode from apps.users.models import User, OAuthCode
UserModel = get_user_model() UserModel = get_user_model()
@ -95,7 +96,7 @@ class AccountDetailView(DetailView):
class AccountUpdateView(UpdateView): class AccountUpdateView(UpdateView):
model = User model = User
template_name = 'users/account_update.html' template_name = 'users/account_update.html'
form_class = AccountUpdateForm # form_class = AccountUpdateForm
def get_success_url(self): def get_success_url(self):
messages.success(self.request, 'عملیات انجام شد.') messages.success(self.request, 'عملیات انجام شد.')

View file

@ -1,11 +1,112 @@
from django.contrib import admin from django.contrib import admin
from .models import Transaction, Wallet, Category from .models import Transaction, Account, Wallet
from django.http import HttpResponse
import io
import xlsxwriter
import jdatetime
from django.utils import timezone
from .admin_filters import JalaliDateTimeRangeFilter
# from utils.accounts_client import get_user_detailed_info
@admin.action(description="export excel")
def export_excel(modeladmin, request, queryset):
output = io.BytesIO()
workbook = xlsxwriter.Workbook(output)
worksheet = workbook.add_worksheet()
for col, header in enumerate(['uuid',
'application',
'amount',
'jalali_datetime',
'payer',
'payer_type',
'payee',
'payee_type',
'state',
'created_at',
'updated_at',
'details',
]):
worksheet.write(0, col, header)
for row, tranaction in enumerate(queryset, 1):
worksheet.write(row, 0, str(tranaction.uuid))
worksheet.write(row, 1, str(tranaction.application))
worksheet.write(row, 2, tranaction.amount)
worksheet.write(row, 3, str(tranaction.jalali_datetime))
worksheet.write(row, 4, str(tranaction.payer))
worksheet.write(row, 5, str(tranaction.payer_type))
worksheet.write(row, 6, str(tranaction.payee))
worksheet.write(row, 7, str(tranaction.payee_type))
worksheet.write(row, 8, str(tranaction.state))
worksheet.write(row, 9, str(tranaction.created_at))
worksheet.write(row, 10, str(tranaction.updated_at))
worksheet.write(row, 11, str(tranaction.details))
workbook.close()
# create a response
response = HttpResponse(content_type='application/vnd.ms-excel')
# tell the browser what the file is named
file_name = jdatetime.date.fromgregorian(date=timezone.now(), locale='fa_IR').strftime("%Y/%m/%d")
response['Content-Disposition'] = f'attachment;filename="{file_name}.xlsx"'
# put the spreadsheet data into the response
response.write(output.getvalue())
# return the response
return response
@admin.action(description="export excel")
def export_accounts_as_excel(modeladmin, request, queryset):
output = io.BytesIO()
workbook = xlsxwriter.Workbook(output)
worksheet = workbook.add_worksheet()
for col, header in enumerate(['uuid',
'owner_uuid',
'owner_type',
'wallet',
'balance',
]):
worksheet.write(0, col, header)
for row, account in enumerate(queryset, 1):
worksheet.write(row, 0, str(account.uuid))
worksheet.write(row, 1, str(account.owner_uuid))
worksheet.write(row, 2, account.owner_type)
worksheet.write(row, 3, str(account.wallet))
worksheet.write(row, 4, str(account.balance))
workbook.close()
# create a response
response = HttpResponse(content_type='application/vnd.ms-excel')
# tell the browser what the file is named
file_name = jdatetime.date.fromgregorian(date=timezone.now(), locale='fa_IR').strftime("%Y/%m/%d")
response['Content-Disposition'] = f'attachment;filename="{file_name}.xlsx"'
# put the spreadsheet data into the response
response.write(output.getvalue())
# return the response
return response
class TransactionAdmin(admin.ModelAdmin): class TransactionAdmin(admin.ModelAdmin):
list_display = ['uuid', 'amount', 'created_at', 'updated_at', 'payer', 'payer_type', 'payee', 'payee_type', 'state', 'application'] list_display = ['uuid', 'application', 'splited_amount', 'jalali_datetime', 'payer', 'payer_type', 'payee', 'payee_type', 'state', 'amount', 'created_at', 'updated_at']
search_fields = ['uuid', 'created_at', 'updated_at', 'payer', 'payee', 'application__uuid'] search_fields = ['uuid', 'created_at', 'updated_at', 'payer_account__owner_uuid', 'payee_account__owner_uuid', 'application__uuid']
list_filter = ['state'] list_filter = ['application', 'state', ('created_at', JalaliDateTimeRangeFilter), ('updated_at', JalaliDateTimeRangeFilter),]
actions = [export_excel]
class Media:
css = {'all': ('admin/css/django_jalali.min.css',)}
js = ('admin/js/django_jalali.min.js',)
class AccountAdmin(admin.ModelAdmin):
list_display = ['uuid', 'owner_uuid', 'wallet', 'owner_type', 'balance', 'get_name']
search_fields = ['uuid', 'owner_uuid']
list_filter = ['wallet__label', 'owner_type']
actions = [export_accounts_as_excel]
admin.site.register(Transaction, TransactionAdmin) admin.site.register(Transaction, TransactionAdmin)
admin.site.register(Account, AccountAdmin)
admin.site.register(Wallet) admin.site.register(Wallet)
admin.site.register(Category)

View file

@ -0,0 +1,46 @@
from collections import OrderedDict
from django.contrib.admin.widgets import AdminSplitDateTime
from django.utils.translation import gettext_lazy as _
from rangefilter.filters import DateTimeRangeFilter
from jalali_date.fields import SplitJalaliDateTimeField
from jalali_date.widgets import AdminSplitJalaliDateTime as _AdminSplitJalaliDateTime
class AdminSplitJalaliDateTime(_AdminSplitJalaliDateTime):
# jalali_date's own template lives under the (unused, see below) 'jalali_date'
# app, which we deliberately don't register in INSTALLED_APPS: its
# templatetags module imports the stdlib `distutils`, removed in Python 3.12+,
# and Django auto-imports every installed app's templatetags at startup.
# Django's stock split-datetime template renders subwidgets generically, so
# it works fine here without needing that app registered.
template_name = AdminSplitDateTime.template_name
class JalaliDateTimeRangeFilter(DateTimeRangeFilter):
def _get_form_fields(self):
return OrderedDict(
(
(
self.lookup_kwarg_gte,
SplitJalaliDateTimeField(
label='',
widget=AdminSplitJalaliDateTime(attrs={'placeholder': _('From date')}),
localize=True,
required=False,
initial=self.default_gte,
),
),
(
self.lookup_kwarg_lte,
SplitJalaliDateTimeField(
label='',
widget=AdminSplitJalaliDateTime(attrs={'placeholder': _('To date')}),
localize=True,
required=False,
initial=self.default_lte,
),
),
)
)

View file

@ -10,6 +10,8 @@ class StateChoices(IntegerChoices):
SUCCESS = 5 , _('success') SUCCESS = 5 , _('success')
FAILED = 6 , _('failed') FAILED = 6 , _('failed')
EXPECTED_FAILURE = 7 , _('expected_failure') # no exact data available but guessed to be failed EXPECTED_FAILURE = 7 , _('expected_failure') # no exact data available but guessed to be failed
ROLLED_BACK = 8 , _('ROLLED_BACK')
CANCELED = 9, _('CANCELED')
class TypeChoices(IntegerChoices): class TypeChoices(IntegerChoices):

View file

@ -1,15 +1,46 @@
import django_filters import django_filters
from django.db import models
from django.db.models import Q from django.db.models import Q
from .constans import TypeChoices
from .models import Transaction, Account
from .models import Transaction
class WalletUUIDFilter(django_filters.BaseInFilter, django_filters.UUIDFilter):
pass
class UserTransactionWalletFilter(django_filters.BaseInFilter, django_filters.UUIDFilter):
def filter(self, queryset, value):
request = self.parent.request
user = request.user
return queryset.filter(
Q(payer_account__owner_uuid=user.pk,
payer_account__owner_type=TypeChoices.USER,
payer_account__wallet_id__in=value,
) |
Q(payee_account__owner_uuid=user.pk,
payee_account__owner_type=TypeChoices.USER,
payee_account__wallet_id__in=value,
))
class UserTransactionFilter(django_filters.FilterSet): class UserTransactionFilter(django_filters.FilterSet):
payer_id = django_filters.UUIDFilter(field_name='payer_wallet__owner_uuid', lookup_expr='iexact') payer = django_filters.UUIDFilter(field_name='payer_account__owner_uuid', lookup_expr='iexact')
payee_id = django_filters.UUIDFilter(field_name='payee_wallet__owner_uuid', lookup_expr='iexact') payee = django_filters.UUIDFilter(field_name='payee_account__owner_uuid', lookup_expr='iexact')
wallets = UserTransactionWalletFilter(field_name='payee_account__wallet', required=True, lookup_expr='in')
class Meta: class Meta:
model = Transaction model = Transaction
fields = { fields = {
} }
class AccountFilter(django_filters.FilterSet):
wallets = WalletUUIDFilter(field_name='wallet', required=True, lookup_expr='in')
class Meta:
model = Account
fields = {
}

View file

@ -0,0 +1,29 @@
# Generated by Django 5.1.7 on 2025-10-23 09:31
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('wallet', '0011_category_currency'),
]
operations = [
migrations.RemoveField(
model_name='transaction',
name='_payee_id',
),
migrations.RemoveField(
model_name='transaction',
name='_payee_type',
),
migrations.RemoveField(
model_name='transaction',
name='_payer_id',
),
migrations.RemoveField(
model_name='transaction',
name='_payer_type',
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-10-25 10:41
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wallet', '0012_remove_transaction__payee_id_and_more'),
]
operations = [
migrations.RenameModel(
old_name='Wallet',
new_name='Account',
),
]

View file

@ -0,0 +1,24 @@
# Generated by Django 5.1.7 on 2025-10-25 10:42
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wallet', '0013_rename_wallet_account_and_more'),
]
operations = [
migrations.RenameField(
model_name='transaction',
old_name='payee_wallet',
new_name='payee_account',
),
migrations.RenameField(
model_name='transaction',
old_name='payer_wallet',
new_name='payer_account',
),
]

View file

@ -0,0 +1,24 @@
# Generated by Django 5.1.7 on 2025-10-25 10:44
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wallet', '0014_remove_transaction_payee_wallet_and_more'),
]
operations = [
migrations.AlterField(
model_name='transaction',
name='payee_account',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='payee_account_transactions', to='wallet.account'),
),
migrations.AlterField(
model_name='transaction',
name='payer_account',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='payer_account_transactions', to='wallet.account'),
),
]

View file

@ -0,0 +1,24 @@
# Generated by Django 5.1.7 on 2025-10-25 11:35
from django.conf import settings
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('wallet', '0015_alter_transaction_payee_account_and_more'),
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
]
operations = [
migrations.RenameModel(
old_name='Category',
new_name='Wallet',
),
migrations.RenameField(
model_name='account',
old_name='category',
new_name='wallet',
),
]

View file

@ -0,0 +1,30 @@
# Generated by Django 6.0 on 2026-02-07 05:57
import django.contrib.postgres.indexes
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wallet', '0016_rename_category_wallet_and_more'),
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
]
operations = [
migrations.AddField(
model_name='transaction',
name='reference',
field=models.UUIDField(db_index=True, null=True),
),
migrations.AlterField(
model_name='transaction',
name='state',
field=models.IntegerField(choices=[(1, 'created'), (2, 'delayed'), (3, 'pending'), (4, 'incomplete'), (5, 'success'), (6, 'failed'), (7, 'expected_failure'), (8, 'ROLLED_BACK')], default=1),
),
migrations.AddIndex(
model_name='transaction',
index=django.contrib.postgres.indexes.GinIndex(fields=['details'], name='wallet_transaction_de_a7ff79aa'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2026-02-14 11:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wallet', '0017_transaction_reference_alter_transaction_state_and_more'),
]
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, 'expected_failure'), (8, 'ROLLED_BACK'), (9, 'CANCELED')], default=1),
),
]

View file

@ -1,8 +1,16 @@
import logging
from functools import cached_property
from django.contrib.postgres.indexes import GinIndex
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 import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
import jdatetime
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
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
@ -15,21 +23,27 @@ 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
logger = logging.getLogger(__name__)
class CurrencyChoices(models.TextChoices): class CurrencyChoices(models.TextChoices):
IRANIAN_RIAL = '166ab7ce33b34799950d45035a9997d6', _('IRANIAN RIAL') # uuid without dash IRANIAN_RIAL = '166ab7ce33b34799950d45035a9997d6', _('IRANIAN RIAL') # uuid without dash
class WalletQuerySet(models.QuerySet): class AccountQuerySet(models.QuerySet):
pass pass
class Wallet(BaseModel):
class Account(BaseModel):
owner_uuid = models.UUIDField(db_index=True, null=False) 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) owner_type = models.IntegerField(choices=TypeChoices.choices, db_index=True)
wallet = models.ForeignKey('Wallet', on_delete=models.PROTECT, null=True)
balance = models.BigIntegerField(_('balance'), default=0) balance = models.BigIntegerField(_('balance'), default=0)
objects = WalletQuerySet.as_manager() # TODO: unique together wallet and owner_uuid
# TODO: performance check for performance issue of unique together
objects = AccountQuerySet.as_manager()
def get_owner(self): def get_owner(self):
if self.owner_type == TypeChoices.APPLICATION: if self.owner_type == TypeChoices.APPLICATION:
@ -39,16 +53,25 @@ class Wallet(BaseModel):
return owner return owner
def get_name(self):
if self.owner_type == TypeChoices.APPLICATION:
owner: Application = Application.objects.get(pk=self.owner_uuid)
return owner.name
class CategoryQuerySet(models.QuerySet):
class WalletQuerySet(models.QuerySet):
pass pass
class Category(BaseModel):
class Wallet(BaseModel):
application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, null=True) application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, null=True)
label = models.CharField(_('label'), max_length=255) label = models.CharField(_('label'), max_length=255)
currency = models.UUIDField(choices=CurrencyChoices.choices, db_index=True, default=CurrencyChoices.IRANIAN_RIAL) currency = models.UUIDField(choices=CurrencyChoices.choices, db_index=True, default=CurrencyChoices.IRANIAN_RIAL)
objects = CategoryQuerySet.as_manager() objects = WalletQuerySet.as_manager()
def __str__(self):
return self.label
# TODO: move DRF functionalities to view or serializers # TODO: move DRF functionalities to view or serializers
@ -82,27 +105,59 @@ class Category(BaseModel):
class TransactionQuerySet(models.QuerySet): class TransactionQuerySet(models.QuerySet):
pass def available(self):
return self.select_related('payer_account', 'payee_account')
def group_by_reference(self, reference):
from collections import defaultdict
from django.db.models import Count, Sum
# 'id': id,
# 'appid': appid,
# 'message': message,
# 'title': title,
# 'priority': priority,
# 'date': date.toIso8601String(),
Transaction.objects.values('reference').annotate(
total=Sum('amount'),
count=Count('id')
).values_list('refrence', 'total', 'count')
# کوئری ۲: همه تراکنش‌ها
transactions = Transaction.objects.all().order_by('refrence', 'created_at')
# گروه‌بندی در پایتون
groups = defaultdict(list)
for t in transactions:
groups[t.refrence].append(t)
# ترکیب
result = []
for ref, items in groups.items():
result.append({
'refrence': ref,
'stats': {'total': stats.get(ref, 0), 'count': len(items)},
'transactions': items
})
class Transaction(BaseModel): 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_type = models.IntegerField(choices=TypeChoices.choices, blank=True, null=True)
_payee_id = models.UUIDField(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)
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, payer_account = models.ForeignKey(Account, on_delete=models.PROTECT, null=True,
related_name='payer_wallet_transactions', related_name='payer_account_transactions',
db_index=True) db_index=True)
payee_wallet = models.ForeignKey(Wallet, on_delete=models.PROTECT, null=True, payee_account = models.ForeignKey(Account, on_delete=models.PROTECT, null=True,
related_name='payee_wallet_transactions', related_name='payee_account_transactions',
db_index=True) db_index=True)
details = models.JSONField(null=True, blank=True, default=dict) details = models.JSONField(null=True, blank=True, default=dict)
reference = models.UUIDField(db_index=True, null=True)
# 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'}
@ -110,6 +165,13 @@ class Transaction(BaseModel):
class Meta: class Meta:
ordering = ['-created_at'] ordering = ['-created_at']
indexes = [
GinIndex(
fields=['details'],
# hashlib.md5("wallet_transaction_details_GinIndex".encode()).hexdigest()[:8] used for hash
name='wallet_transaction_de_a7ff79aa'
),
]
def __str__(self): def __str__(self):
try: try:
@ -119,62 +181,117 @@ class Transaction(BaseModel):
# unique_together = ('application', 'detail_id',) # unique_together = ('application', 'detail_id',)
@property
def jalali_datetime(self):
def to_jalali(value):
if value:
return jdatetime.datetime.fromgregorian(date=value, locale='fa_IR').strftime("%Y/%m/%d-%H:%M:%S")
else:
return value
return to_jalali(self.created_at)
@property
def splited_amount(self):
def amount_split_3digit(value):
text = str(value)[::-1]
return ','.join([l[::-1] for l in [text[i:i + 3] for i in range(0, len(text), 3)][::-1]])
if self.amount:
amount_str = amount_split_3digit(str(self.amount))
return amount_str
return
def get_payer(self): def get_payer(self):
return self.payer_wallet.get_owner() return self.payer_account.get_owner()
def get_payee(self): def get_payee(self):
return self.payee_wallet.get_owner() return self.payee_account.get_owner()
@property @cached_property
def payer_wallet_category(self): def payer_wallet(self):
return self.payer_wallet and self.payer_wallet.category_id return self.payer_account and self.payer_account.wallet_id
@property @cached_property
def payee_wallet_category(self): def payee_wallet(self):
return self.payee_wallet and self.payee_wallet.category_id return self.payee_account and self.payee_account.wallet_id
@property @cached_property
def payer(self): def payer(self):
return self.payer_wallet.owner_uuid return self.payer_account.owner_uuid
@property @cached_property
def payer_type(self): def payer_type(self):
return self.payer_wallet.owner_type return self.payer_account.owner_type
@property @cached_property
def payee(self): def payee(self):
return self.payee_wallet.owner_uuid return self.payee_account.owner_uuid
@property @cached_property
def payee_type(self): def payee_type(self):
return self.payee_wallet.owner_type return self.payee_account.owner_type
def change_state(self, from_state, to_state, error_message=None, **data): def change_state(self, from_states: list | tuple, to_state, same_ok=False, raise_exception=True,
if self.state == from_state: error_message=None,
updated = Transaction.objects.filter(pk=self.pk, state=from_state).update( data: None | dict = None):
data = data or {}
data['updated_at'] = timezone.now()
if same_ok and self.state == to_state:
return self
if self.state in from_states:
updated = self.__class__.objects.filter(pk=self.pk, state__in=from_states).update(
state=to_state, state=to_state,
**data **data
) )
if updated: # double check in db level if updated: # double check in db level
self.state = to_state self.state = to_state
else: return self
raise APIException(error_message)
else:
raise APIException(error_message)
######################## category if raise_exception:
def category_withdraw_from_payer_balance(self): raise Exception(error_message)
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 def withdraw_from_payer_balance(self):
if self.amount > self.payer_wallet.balance and not payer_is_allowed_negative_balance: logger.debug('start withdraw_from_payer_balance')
payer_is_allowed_negative_balance = self.payer_account.owner_type == TypeChoices.APPLICATION.value and str(
self.payer_account.owner_uuid) in settings.ALLOWED_NEGATIVE_BALANCE_APPLICATIONS
if self.amount > self.payer_account.balance and not payer_is_allowed_negative_balance:
logger.error('not enough balance')
raise UnprocessableEntity('not enough balance') raise UnprocessableEntity('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): if not payer_is_allowed_negative_balance:
self.payee_wallet.balance = F('balance') + self.amount updated = Account.objects.filter(pk=self.payer_account.pk, balance__gte=self.amount).update(balance=F('balance') - self.amount)
self.payee_wallet.save(update_fields=['balance']) else:
updated = Account.objects.filter(pk=self.payer_account.pk).update(balance=F('balance') - self.amount)
if updated:
self.payer_account.refresh_from_db()
logger.info(f'payer_account balance {self.payee_account} -> {self.payer_account.balance} - {self.amount}')
else:
raise UnprocessableEntity('not enough balance')
def rollback_withdraw_from_payer_balance(self):
logger.debug('start rollback withdraw_from_payer_balance')
logger.info(f'payer_account balance {self.payer_account} -> {self.amount}')
self.payer_account.balance = F('balance') + self.amount
self.payer_account.save(update_fields=['balance'])
def deposit_to_payee_balance(self):
logger.debug('start deposit_to_payee_balance')
logger.info(f'payee_account balance {self.payee_account} -> {self.payer_account.balance} + {self.amount}')
self.payee_account.balance = F('balance') + self.amount
self.payee_account.save(update_fields=['balance'])
def rollback_deposit_to_payee_balance(self):
logger.debug('start rollback deposit_to_payee_balance')
logger.info(f'payee_account balance {self.payee_account} -> {self.amount}')
self.payee_account.balance = F('balance') - self.amount
self.payee_account.save(update_fields=['balance'])
# TODO: refactor
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')
@ -197,79 +314,120 @@ class Transaction(BaseModel):
with transaction.atomic(): with transaction.atomic():
if self.delay: if self.delay:
self.change_state( self.change_state(
StateChoices.CREATED, [StateChoices.CREATED],
StateChoices.DELAYED, StateChoices.DELAYED,
error_message='this transaction is not ready to pay', error_message='this transaction is not ready to pay',
payer_id=payer_id, data={
payer_type=payer_type "payer_id": payer_id,
"payer_type": payer_type
}
) )
else: else:
self.change_state( self.change_state(
StateChoices.CREATED, [StateChoices.CREATED],
StateChoices.SUCCESS, StateChoices.SUCCESS,
error_message='this transaction is not ready to pay', error_message='this transaction is not ready to pay',
payer_id=payer_id, data={
payer_type=payer_type "payer_id": payer_id,
"payer_type": payer_type
}
) )
self.category_deposit_to_payee_balance() self.deposit_to_payee_balance()
self.category_withdraw_from_payer_balance() self.withdraw_from_payer_balance()
return self return self
# TODO: refactor
def receipt(self): def receipt(self):
if not self.state == StateChoices.DELAYED: if not self.state == StateChoices.DELAYED:
raise APIException('this transaction is not delayed') raise APIException('this transaction is not delayed')
with transaction.atomic(): with transaction.atomic():
self.change_state( self.change_state(
StateChoices.DELAYED, [StateChoices.DELAYED],
StateChoices.SUCCESS, StateChoices.SUCCESS,
error_message='this transaction is not delayed' error_message='this transaction is not delayed'
) )
self.category_deposit_to_payee_balance() self.deposit_to_payee_balance()
return self return self
def category_submit(self): def submit(self):
if not self.payer_wallet: if not self.payer_account:
raise APIException('payer identification error') raise APIException('payer identification error')
if not self.payee_wallet: if not self.payee_account:
raise APIException('payee identification error') raise APIException('payee identification error')
if not self.payer_wallet.category == self.payee_wallet.category: if not self.payer_account.wallet.currency == self.payee_account.wallet.currency:
self.change_state(from_state=self.state, to_state=StateChoices.FAILED) self.change_state([self.state], StateChoices.FAILED)
raise APIException('payer and payee wallet currency not equal') logger.info(f'payer({self.payer_account.wallet.currency}) and payee({self.payee_account.wallet.currency}) account currency not equal')
raise APIException('payer and payee account currency not equal')
if not self.state == StateChoices.CREATED: if not self.state == StateChoices.CREATED:
logger.debug(f'this transaction is not ready to pay(UUID:{self.uuid}, state:{self.state})')
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.category_withdraw_from_payer_balance() self.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 category_verify(self): def 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.category_deposit_to_payee_balance() self.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 cancel(self):
if self.state not in [StateChoices.PENDING, StateChoices.DELAYED, StateChoices.INCOMPLETE]:
raise APIException('this transaction is not pending')
with transaction.atomic():
self.rollback_withdraw_from_payer_balance()
self.change_state(
[StateChoices.PENDING, StateChoices.DELAYED, StateChoices.INCOMPLETE],
StateChoices.CANCELED,
error_message=_('TRANSACTION_STATE_NOT_SUITABLE_FOR_CANCELING')
)
return self
def rollback(self):
if self.state not in [StateChoices.PENDING, StateChoices.SUCCESS, StateChoices.DELAYED, StateChoices.INCOMPLETE]:
raise APIException('this transaction is not pending')
with transaction.atomic():
self.rollback_withdraw_from_payer_balance()
if self.state == StateChoices.SUCCESS:
self.rollback_deposit_to_payee_balance()
self.change_state(
[StateChoices.SUCCESS],
StateChoices.ROLLED_BACK,
error_message=_('TRANSACTION_STATE_NOT_SUITABLE_FOR_ROLLBACK')
)
else:
self.change_state(
[StateChoices.PENDING, StateChoices.DELAYED, StateChoices.INCOMPLETE],
StateChoices.CANCELED,
error_message=_('TRANSACTION_STATE_NOT_SUITABLE_FOR_CANCEL')
)
return self
def get_role(self, entity): def get_role(self, entity):
if self.payer_wallet.owner_uuid == entity.uuid: if self.payer_account.owner_uuid == entity.uuid:
return 1 return 1
elif self.payee_wallet.owner_uuid == entity.uuid: elif self.payee_account.owner_uuid == entity.uuid:
return 2 return 2
else: else:
return 0 return 0
@ -281,27 +439,27 @@ class Transaction(BaseModel):
# detail = {'payer_name', 'payee_name', 'application_name', 'payer_avatar_url', 'payee_avatar_url', 'application_avatar_url'} # detail = {'payer_name', 'payee_name', 'application_name', 'payer_avatar_url', 'payee_avatar_url', 'application_avatar_url'}
is_updated = False is_updated = False
if not self.details.get('payer_name'): if not self.details.get('payer_name'):
print('update payer') logger.info('update payer')
if self.payer_wallet.owner_type == TypeChoices.USER: if self.payer_account.owner_type == TypeChoices.USER:
payer_details: PublicUser = get_user_details(self.payer_wallet.owner_uuid) payer_details: PublicUser = get_user_details(self.payer_account.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)
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) logger.debug("payer_details:", payer_details)
print("is_updated:", is_updated) logger.info("is_updated:", is_updated)
if not self.details.get('payee_name'): if not self.details.get('payee_name'):
print('update payee') logger.info('update payee')
if self.payee_wallet.owner_type == TypeChoices.USER: if self.payee_account.owner_type == TypeChoices.USER:
payee_details: PublicUser = get_user_details(self.payee_wallet.owner_uuid) payee_details: PublicUser = get_user_details(self.payee_account.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_account.owner_uuid)
self.details['payee_name'] = payee_details.name self.details['payee_name'] = payee_details.name
# TODO: get avatar from details # TODO: get avatar from details
@ -325,3 +483,29 @@ class Transaction(BaseModel):
if is_updated: if is_updated:
print('update details') print('update details')
self.save() self.save()
# TODO: bulk
# class PaymentRequest(BaseModel):
# state = models.IntegerField(choices=StateChoices.choices, default=StateChoices.CREATED)
# application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT,
# related_name='transactions', null=True)
#
# payer_uuid = models.UUIDField(db_index=True, null=False)
# payer_type = models.IntegerField(choices=TypeChoices.choices, db_index=True)
#
# payee_uuid = models.UUIDField(db_index=True, null=False)
# payee_type = models.IntegerField(choices=TypeChoices.choices, db_index=True)
#
# details = models.JSONField(null=True, blank=True, default=dict)
#
# def submit(self):
# with transaction.atomic():
# pass
#
# def verify(self):
# with transaction.atomic():
# pass
#
# def rollback(self):
# with transaction.atomic():
# pass

View file

@ -5,7 +5,7 @@ from rest_framework import serializers
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
from .constans import TypeChoices from .constans import TypeChoices
from .models import Transaction, Wallet from .models import Transaction, Account
from ..gooyal_oauth2.models import Application from ..gooyal_oauth2.models import Application
from ..users.models import User from ..users.models import User
@ -13,8 +13,8 @@ from ..users.models import User
class TransactionSerializer(serializers.ModelSerializer): class TransactionSerializer(serializers.ModelSerializer):
payee = serializers.UUIDField(read_only=True) payee = serializers.UUIDField(read_only=True)
payee_type = serializers.IntegerField(read_only=True) payee_type = serializers.IntegerField(read_only=True)
payee_wallet = serializers.UUIDField(source='payee_wallet_category', read_only=True) # payee_wallet = serializers.UUIDField(source='payee_wallet', read_only=True)
payer_wallet = serializers.UUIDField(source='payer_wallet_category', read_only=True) # payer_wallet = serializers.UUIDField(source='payer_wallet', read_only=True)
role = serializers.SerializerMethodField() role = serializers.SerializerMethodField()
class Meta: class Meta:
@ -74,7 +74,7 @@ class DepositSerializer(serializers.ModelSerializer):
with atomic(): with atomic():
try: try:
transaction = Transaction.objects.create_transaction(**validated_data) transaction = Transaction.objects.create_transaction(**validated_data)
transaction.category_submit() transaction.submit()
except IntegrityError as e: except IntegrityError as e:
raise APIException('duplicate uuid') raise APIException('duplicate uuid')
@ -104,14 +104,14 @@ class WithdrawSerializer(serializers.ModelSerializer):
with atomic(): with atomic():
try: try:
transaction = Transaction.objects.create_transaction(**validated_data) transaction = Transaction.objects.create_transaction(**validated_data)
transaction.category_submit() transaction.submit()
except IntegrityError as e: except IntegrityError as e:
raise APIException('duplicate uuid') raise APIException('duplicate uuid')
return transaction return transaction
class WalletBalanceSerializer(serializers.ModelSerializer): class AccountBalanceSerializer(serializers.ModelSerializer):
instance_id = serializers.UUIDField(required=True) instance_id = serializers.UUIDField(required=True)
instance_type = serializers.ChoiceField(choices=TypeChoices.choices, required=True) instance_type = serializers.ChoiceField(choices=TypeChoices.choices, required=True)
@ -160,18 +160,18 @@ class TransferVerifySerializer(serializers.ModelSerializer):
#### Application #### Application
class AccountSerializer(serializers.ModelSerializer):
class BalanceSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Wallet model = Account
fields = ['balance', 'category'] fields = ['balance', 'wallet']
class ApplicationDepositSerializer(serializers.ModelSerializer): class ApplicationDepositSerializer(serializers.ModelSerializer):
uuid = serializers.UUIDField(required=False, validators=[]) uuid = serializers.UUIDField(required=False, validators=[])
payee = serializers.UUIDField(required=True) payee = serializers.UUIDField(required=True)
payee_type = serializers.IntegerField(required=True) payee_type = serializers.IntegerField(required=True)
payee_wallet = serializers.UUIDField(source='payee_wallet_category', required=False) # TODO: required True payee_wallet = serializers.UUIDField(required=True)
payer_wallet = serializers.UUIDField(source='payer_wallet_category', read_only=True) payer_wallet = serializers.UUIDField(read_only=True)
amount = serializers.IntegerField(required=True, validators=[MinValueValidator(1)]) amount = serializers.IntegerField(required=True, validators=[MinValueValidator(1)])
# TODO: details as fields # TODO: details as fields
@ -193,15 +193,15 @@ class ApplicationDepositSerializer(serializers.ModelSerializer):
'details', 'details',
) )
read_only_fields = ['updated_at', 'state', 'created_at', 'payer_id', 'payer_type', 'payer_wallet'] read_only_fields = ['updated_at', 'state', 'created_at', 'payer', 'payer_type', 'payer_wallet']
class ApplicationWithdrawSerializer(serializers.ModelSerializer): class ApplicationWithdrawSerializer(serializers.ModelSerializer):
uuid = serializers.UUIDField(required=False, validators=[]) uuid = serializers.UUIDField(required=False, validators=[])
payer = serializers.UUIDField(required=True) payer = serializers.UUIDField(required=True)
payer_type = serializers.IntegerField(required=True) payer_type = serializers.IntegerField(required=True)
payer_wallet = serializers.UUIDField(source='payer_wallet_category',required=False) # TODO: required True payee_wallet = serializers.UUIDField(read_only=True)
payee_wallet = serializers.UUIDField(source='payee_wallet_category', read_only=True) # TODO: required True payer_wallet = serializers.UUIDField(required=True)
amount = serializers.IntegerField(required=True, validators=[MinValueValidator(1)]) amount = serializers.IntegerField(required=True, validators=[MinValueValidator(1)])
class Meta: class Meta:
@ -223,3 +223,20 @@ class ApplicationWithdrawSerializer(serializers.ModelSerializer):
) )
read_only_fields = ['updated_at', 'state', 'created_at', 'payee', 'payee_type', 'payee_wallet'] read_only_fields = ['updated_at', 'state', 'created_at', 'payee', 'payee_type', 'payee_wallet']
class TransactionGroupedSerializer(serializers.Serializer):
reference = serializers.UUIDField(source='details__reference_id')
application = serializers.UUIDField()
total_amount = serializers.FloatField()
total_count = serializers.IntegerField()
state = serializers.IntegerField()
description = serializers.CharField()
created_at = serializers.DateTimeField()
role = serializers.IntegerField()
transactions = TransactionSerializer(many=True)
class Meta:
fields = ('reference', 'application', 'total_amount', 'total_count', 'transactions', 'state', 'description', 'created_at', 'role')
read_only_fields = ['reference', 'application', 'total_amount', 'total_count', 'transactions', 'state', 'description', 'created_at', 'role']

View file

@ -8,22 +8,24 @@ from rest_framework.test import APITestCase, override_settings
from apps.users.models import User from apps.users.models import User
from apps.wallet.constans import TypeChoices from apps.wallet.constans import TypeChoices
from apps.wallet.models import Account
from apps.wallet.models import Wallet from apps.wallet.models import Wallet
from apps.wallet.models import Category
AccessToken = get_access_token_model() AccessToken = get_access_token_model()
Application = get_application_model() Application = get_application_model()
application_uuid_hex = 'b14e8b86-8f4a-44d9-b29d-badceb470053'
class ApplicationApiFlowsTests(APITestCase): class ApplicationApiFlowsTests(APITestCase):
client_id = '4INGOCMoulE0fNY1SQlTbPtsWqqxGj2DdqjADq6u' client_id = '4INGOCMoulE0fNY1SQlTbPtsWqqxGj2DdqjADq6u'
application_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb470053') application_uuid = uuid.UUID(application_uuid_hex)
application_access_token = '14naVsdKCbKNOhnElPyXcrwSnqqFbm' application_access_token = '14naVsdKCbKNOhnElPyXcrwSnqqFbm'
payer_access_token = '24naVsdKCbKNOhnElPyXcrwSnqqFbm' payer_access_token = '24naVsdKCbKNOhnElPyXcrwSnqqFbm'
payer_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb470051') payer_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb470051')
payee_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb470052') payee_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb470052')
wallet_category1_uuid = uuid.UUID('114e8b86-8f4a-44d9-b29d-badceb470051') # wallet_rial_uuid = uuid.UUID('114e8b86-8f4a-44d9-b29d-badceb470051')
wallet_category2_uuid = uuid.UUID('214e8b86-8f4a-44d9-b29d-badceb470051') wallet_rial_uuid = uuid.UUID('114e8b86-8f4a-44d9-b29d-badceb470051')
wallet_reward_uuid = uuid.UUID('214e8b86-8f4a-44d9-b29d-badceb470051')
# application, _ = Application.objects.get_or_create(client_id=client_id, uuid=application_uuid) # application, _ = Application.objects.get_or_create(client_id=client_id, uuid=application_uuid)
scopes = [ scopes = [
@ -33,6 +35,7 @@ class ApplicationApiFlowsTests(APITestCase):
'wallet.deposit:submit', 'wallet.deposit:submit',
'wallet.wallet:get_balance', 'wallet.wallet:get_balance',
'wallet.application:get_user_balance', 'wallet.application:get_user_balance',
'wallet.user:transaction_list',
'wallet.application.withdraw:submit', 'wallet.application.withdraw:submit',
'wallet.application.withdraw:verify', 'wallet.application.withdraw:verify',
@ -41,9 +44,9 @@ class ApplicationApiFlowsTests(APITestCase):
def setUp(self): def setUp(self):
self.application_user, _ = User.objects.get_or_create(pk=self.application_uuid) 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.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.application_account, _ = Account.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.payee_account, _ = Account.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) self.payer_account, _ = Account.objects.get_or_create(owner_uuid=self.payer_uuid, owner_type=TypeChoices.USER, balance=300)
expire_datetime = timezone.now() + timedelta(seconds=3600) expire_datetime = timezone.now() + timedelta(seconds=3600)
expire_datetime.isoformat() expire_datetime.isoformat()
@ -75,14 +78,14 @@ class ApplicationApiFlowsTests(APITestCase):
client_id=self.client_id, client_id=self.client_id,
uuid=self.application_uuid, uuid=self.application_uuid,
) )
self.wallet_category1, _ = Category.objects.get_or_create(application=self.application, uuid=self.wallet_category1_uuid) self.wallet_rial, _ = Wallet.objects.get_or_create(application=self.application, uuid=self.wallet_rial_uuid)
self.wallet_category2, _ = Category.objects.get_or_create(application=self.application, uuid=self.wallet_category2_uuid) self.wallet_reward, _ = Wallet.objects.get_or_create(application=self.application, uuid=self.wallet_reward_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_account1, _ = Account.objects.get_or_create(owner_uuid=self.application_uuid, owner_type=TypeChoices.APPLICATION, balance=300, wallet=self.wallet_rial)
self.application_wallet2, _ = Wallet.objects.get_or_create(owner_uuid=self.application_uuid, owner_type=TypeChoices.APPLICATION, balance=300, category=self.wallet_category2) self.application_account2, _ = Account.objects.get_or_create(owner_uuid=self.application_uuid, owner_type=TypeChoices.APPLICATION, balance=300, wallet=self.wallet_reward)
self.payee_wallet1, _ = Wallet.objects.get_or_create(owner_uuid=self.payee_uuid, owner_type=TypeChoices.USER, category=self.wallet_category1) self.payee_account1, _ = Account.objects.get_or_create(owner_uuid=self.payee_uuid, owner_type=TypeChoices.USER, wallet=self.wallet_rial)
self.payee_wallet2, _ = Wallet.objects.get_or_create(owner_uuid=self.payee_uuid, owner_type=TypeChoices.USER, category=self.wallet_category2) self.payee_account2, _ = Account.objects.get_or_create(owner_uuid=self.payee_uuid, owner_type=TypeChoices.USER, wallet=self.wallet_reward)
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_account1, _ = Account.objects.get_or_create(owner_uuid=self.payer_uuid, owner_type=TypeChoices.USER, balance=300, wallet=self.wallet_rial)
self.payer_wallet2, _ = Wallet.objects.get_or_create(owner_uuid=self.payer_uuid, owner_type=TypeChoices.USER, balance=300, category=self.wallet_category2) self.payer_account2, _ = Account.objects.get_or_create(owner_uuid=self.payer_uuid, owner_type=TypeChoices.USER, balance=300, wallet=self.wallet_reward)
def tearDown(self): def tearDown(self):
super().tearDown() super().tearDown()
@ -91,9 +94,13 @@ class ApplicationApiFlowsTests(APITestCase):
return "Bearer {0}".format(token) return "Bearer {0}".format(token)
def test_authentication_allow(self): def test_authentication_allow(self):
auth = self._create_authorization_header(self.application_access_token.token) auth = self._create_authorization_header(self.payer_access_token.token)
response = self.client.get(reverse('wallet:user_wallet_balance_api'), HTTP_AUTHORIZATION=auth) wallets = [self.wallet_rial_uuid.hex, self.wallet_reward_uuid.hex]
self.assertContains(response, 'balance') response = self.client.get(reverse('wallet:user_wallet_account_api'), query_params={'wallet': ','.join(wallets)}, HTTP_AUTHORIZATION=auth)
print(response.json())
self.assertGreater(response.json()['count'], 1)
from_wallet = response.json()['results'][0]
self.assertEqual(from_wallet['balance'], 300)
def test_authentication_expired_token(self): def test_authentication_expired_token(self):
@ -110,7 +117,7 @@ class ApplicationApiFlowsTests(APITestCase):
}, },
) )
auth = self._create_authorization_header(expired_access_token.token) auth = self._create_authorization_header(expired_access_token.token)
response = self.client.get(reverse('wallet:user_wallet_balance_api'), HTTP_AUTHORIZATION=auth) response = self.client.get(reverse('wallet:user_wallet_account_api'), HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 401) self.assertEqual(response.status_code, 401)
# def test_authentication_disallow(self): # def test_authentication_disallow(self):
@ -122,396 +129,313 @@ class ApplicationApiFlowsTests(APITestCase):
auth = self._create_authorization_header(self.application_access_token.token) auth = self._create_authorization_header(self.application_access_token.token)
# Checking wallets initials values(application balance = 300 and payee balance = zero) # Checking wallets initials values(application balance = 300 and payee balance = zero)
response = self.client.get(reverse('wallet:application_wallet_balance_api'), HTTP_AUTHORIZATION=auth) wallets = [self.wallet_rial_uuid.hex, self.wallet_reward_uuid.hex]
self.assertContains(response, 'balance') response = self.client.get(reverse('wallet:application_account_list_api'), query_params={'wallet': ','.join(wallets)}, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.json()['balance'], 300) self.assertGreater(response.json()['count'], 1)
from_wallet = response.json()['results'][0]
self.assertEqual(from_wallet['balance'], 300)
params = { params = {
'uuid': self.payee_uuid, 'owner': self.payee_uuid,
'category_uuid': self.wallet_category1_uuid,
} }
response = self.client.get(reverse('wallet:application_user_wallet_balance_api', kwargs=params), HTTP_AUTHORIZATION=auth) response = self.client.get(reverse('wallet:application_user_account_list_api', kwargs=params), query_params={'wallet': ','.join(wallets)}, HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance') self.assertGreater(response.json()['count'], 1)
self.assertEqual(response.json()['balance'], 0) to_wallet = response.json()['results'][0]
self.assertEqual(to_wallet['balance'], 0)
# Deposit process # Deposit process
data = { data = {
'amount': 100, 'amount': 100,
'payee': self.payee_uuid, 'payee': self.payee_uuid,
'payee_type': TypeChoices.USER, 'payee_type': TypeChoices.USER,
'payee_wallet': self.wallet_category1_uuid, 'payee_wallet': self.wallet_rial_uuid,
} }
kwargs_category_uuid_payee_uuid = { kwargs_payer_wallet = {
'category_uuid': self.wallet_category1_uuid, 'payer_wallet': self.wallet_rial_uuid,
} }
response = self.client.post(reverse('wallet:application_deposit_submit_api', kwargs=kwargs_category_uuid_payee_uuid), data=data, HTTP_AUTHORIZATION=auth) response = self.client.post(reverse('wallet:application_deposit_submit_api', kwargs=kwargs_payer_wallet), data=data, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
json_response = response.json() json_response = response.json()
params = { kwargs_verify = {
'uuid': json_response['uuid'], 'uuid': json_response['uuid'],
'category_uuid': self.wallet_category1_uuid, 'payer_wallet': self.wallet_rial_uuid,
} }
response = self.client.get(reverse('wallet:application_deposit_verify_api', kwargs=params), HTTP_AUTHORIZATION=auth) response = self.client.get(reverse('wallet:application_deposit_verify_api', kwargs=kwargs_verify), HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['state'], 5) self.assertEqual(response.json()['state'], 5)
# Desired results # Desired results
response = self.client.get(reverse('wallet:application_wallet_balance_api', kwargs=kwargs_category_uuid_payee_uuid), HTTP_AUTHORIZATION=auth) response = self.client.get(reverse('wallet:application_account_list_api'), query_params={'wallet': ','.join(wallets)}, HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance') self.assertGreater(response.json()['count'], 1)
self.assertEqual(response.json()['balance'], 200) from_wallet = response.json()['results'][0]
self.assertEqual(from_wallet['balance'], 200)
params = { params = {
'uuid': self.payee_uuid, 'owner': self.payee_uuid,
'category_uuid': self.wallet_category1_uuid,
} }
response = self.client.get(reverse('wallet:application_user_wallet_balance_api', kwargs=params), HTTP_AUTHORIZATION=auth) response = self.client.get(reverse('wallet:application_user_account_list_api', kwargs=params), query_params={'wallet': ','.join(wallets)}, HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance') self.assertGreater(response.json()['count'], 1)
self.assertEqual(response.json()['balance'], 100) to_wallet = response.json()['results'][0]
self.assertEqual(to_wallet['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': self.payee_uuid,
'payee_type': TypeChoices.USER,
'payee_wallet': self.wallet_category1_uuid
}
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): def test_deposit_flow_insufficient_balance(self):
auth = self._create_authorization_header(self.application_access_token.token) auth = self._create_authorization_header(self.application_access_token.token)
# Checking wallets initials values(application balance = 300 and payee balance = zero) # Checking wallets initials values(application balance = 300 and payee balance = zero)
response = self.client.get(reverse('wallet:application_wallet_balance_api'), HTTP_AUTHORIZATION=auth) wallets = [self.wallet_rial_uuid.hex, self.wallet_reward_uuid.hex]
self.assertContains(response, 'balance') wallets = [self.wallet_rial_uuid.hex, self.wallet_reward_uuid.hex]
self.assertEqual(response.json()['balance'], 300) response = self.client.get(reverse('wallet:application_account_list_api'), query_params={'wallet': ','.join(wallets)}, HTTP_AUTHORIZATION=auth)
self.assertGreater(response.json()['count'], 1)
from_wallet = response.json()['results'][0]
self.assertEqual(from_wallet['balance'], 300)
params = { params = {
'uuid': self.payee_uuid, 'owner': self.payee_uuid,
} }
kwargs_category_uuid_payee_uuid = { response = self.client.get(reverse('wallet:application_user_account_list_api', kwargs=params), query_params={'wallet': ','.join(wallets)}, HTTP_AUTHORIZATION=auth)
'category_uuid': self.wallet_category1_uuid, self.assertGreater(response.json()['count'], 1)
} to_wallet = response.json()['results'][0]
self.assertEqual(to_wallet['balance'], 0)
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 # Deposit process
data = { data = {
'amount': 500, 'amount': 500,
'payee': self.payee_uuid, 'payee': self.payee_uuid,
'payee_type': TypeChoices.USER, 'payee_type': TypeChoices.USER,
'payee_wallet': self.wallet_category1_uuid 'payee_wallet': self.wallet_rial_uuid,
} }
response = self.client.post(reverse('wallet:application_deposit_submit_api', kwargs=kwargs_category_uuid_payee_uuid), data=data, HTTP_AUTHORIZATION=auth) kwargs_payer_wallet = {
'payer_wallet': self.wallet_rial_uuid,
}
response = self.client.post(reverse('wallet:application_deposit_submit_api', kwargs=kwargs_payer_wallet),
data=data, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 422) self.assertEqual(response.status_code, 422)
@override_settings(ALLOWED_NEGATIVE_BALANCE_APPLICATIONS=['b14e8b86-8f4a-44d9-b29d-badceb470053']) @override_settings(ALLOWED_NEGATIVE_BALANCE_APPLICATIONS=[application_uuid_hex])
def test_deposit_flow_skip_insufficient_balance_for_allowed_negative_balance_applications(self): def test_deposit_flow_skip_insufficient_balance_for_allowed_negative_balance_applications(self):
auth = self._create_authorization_header(self.application_access_token.token) auth = self._create_authorization_header(self.application_access_token.token)
# Checking wallets initials values(application balance = 300 and payee balance = zero) # Checking wallets initials values(application balance = 300 and payee balance = zero)
response = self.client.get(reverse('wallet:application_wallet_balance_api'), HTTP_AUTHORIZATION=auth) wallets = [self.wallet_rial_uuid.hex, self.wallet_reward_uuid.hex]
self.assertContains(response, 'balance') response = self.client.get(reverse('wallet:application_account_list_api'), query_params={'wallet': ','.join(wallets)}, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.json()['balance'], 300) self.assertGreater(response.json()['count'], 1)
from_wallet = response.json()['results'][0]
self.assertEqual(from_wallet['balance'], 300)
params = { params = {
'uuid': self.payee_uuid, 'owner': self.payee_uuid,
'category_uuid': self.wallet_category1_uuid,
} }
response = self.client.get(reverse('wallet:application_user_wallet_balance_api', kwargs=params), HTTP_AUTHORIZATION=auth) response = self.client.get(reverse('wallet:application_user_account_list_api', kwargs=params), query_params={'wallet': ','.join(wallets)}, HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance') self.assertGreater(response.json()['count'], 1)
self.assertEqual(response.json()['balance'], 0) to_wallet = response.json()['results'][0]
self.assertEqual(to_wallet['balance'], 0)
# Deposit process # Deposit process
data = { data = {
'amount': 500, 'amount': 500,
'payee': self.payee_uuid, 'payee': self.payee_uuid,
'payee_type': TypeChoices.USER, 'payee_type': TypeChoices.USER,
'payee_wallet': self.wallet_category1_uuid, 'payee_wallet': self.wallet_rial_uuid,
} }
kwargs_payer_wallet = {
kwargs_category_uuid_payee_uuid = { 'payer_wallet': self.wallet_rial_uuid,
'category_uuid': self.wallet_category1_uuid,
} }
response = self.client.post(reverse('wallet:application_deposit_submit_api', kwargs=kwargs_payer_wallet),
response = self.client.post(reverse('wallet:application_deposit_submit_api', kwargs=kwargs_category_uuid_payee_uuid), data=data, HTTP_AUTHORIZATION=auth) data=data, HTTP_AUTHORIZATION=auth)
print(response.json())
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
def test_deposit_flow_zero_amount(self): def test_deposit_flow_zero_amount(self):
# TODO: category # TODO: wallet
auth = self._create_authorization_header(self.application_access_token.token) auth = self._create_authorization_header(self.application_access_token.token)
# Deposit process # Deposit process
data = { data = {
'amount': 0, 'amount': 0,
'payee': self.payee_uuid, 'payee': self.payee_uuid,
'payee_type': TypeChoices.USER 'payee_type': TypeChoices.USER,
'payee_wallet': self.wallet_rial_uuid,
} }
response = self.client.post(reverse('wallet:application_deposit_submit_api'), data=data, HTTP_AUTHORIZATION=auth) kwargs_payer_wallet = {
'payer_wallet': self.wallet_rial_uuid,
}
response = self.client.post(reverse('wallet:application_deposit_submit_api', kwargs=kwargs_payer_wallet), data=data, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
def test_deposit_flow_negative_amount(self): def test_deposit_flow_negative_amount(self):
# TODO: category # TODO: wallet
auth = self._create_authorization_header(self.application_access_token.token) auth = self._create_authorization_header(self.application_access_token.token)
# Deposit process # Deposit process
data = { data = {
'amount': -100, 'amount': -100,
'payee': self.payee_uuid, 'payee': self.payee_uuid,
'payee_type': TypeChoices.USER 'payee_type': TypeChoices.USER,
'payee_wallet': self.wallet_rial_uuid,
} }
response = self.client.post(reverse('wallet:application_deposit_submit_api'), data=data, HTTP_AUTHORIZATION=auth) kwargs_payer_wallet = {
'payer_wallet': self.wallet_rial_uuid,
}
response = self.client.post(reverse('wallet:application_deposit_submit_api', kwargs=kwargs_payer_wallet), data=data, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
def test_deposit_flow_prevent_double_spending(self): def test_deposit_flow_prevent_double_spending(self):
# TODO: check this test
auth = self._create_authorization_header(self.application_access_token.token) auth = self._create_authorization_header(self.application_access_token.token)
# Checking wallets initials values(application balance = 300 and payee balance = zero) # Checking wallets initials values(application balance = 300 and payee balance = zero)
response = self.client.get(reverse('wallet:application_wallet_balance_api'), HTTP_AUTHORIZATION=auth) wallets = [self.wallet_rial_uuid.hex, self.wallet_reward_uuid.hex]
self.assertContains(response, 'balance') response = self.client.get(reverse('wallet:application_account_list_api'), query_params={'wallet': ','.join(wallets)}, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.json()['balance'], 300) self.assertGreater(response.json()['count'], 1)
from_wallet = response.json()['results'][0]
self.assertEqual(from_wallet['balance'], 300)
params = { params = {
'uuid': self.payee_uuid, 'owner': self.payee_uuid,
'category_uuid': self.wallet_category1_uuid,
} }
response = self.client.get(reverse('wallet:application_user_wallet_balance_api', kwargs=params), HTTP_AUTHORIZATION=auth) response = self.client.get(reverse('wallet:application_user_account_list_api', kwargs=params), query_params={'wallet': ','.join(wallets)}, HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance') self.assertGreater(response.json()['count'], 1)
self.assertEqual(response.json()['balance'], 0) to_wallet = response.json()['results'][0]
self.assertEqual(to_wallet['balance'], 0)
# Deposit process # Deposit process
data = { data = {
'uuid': "b14e8b86-8f4a-44d9-b29d-badceb470053",
'amount': 200, 'amount': 200,
'payee': self.payee_uuid, 'payee': self.payee_uuid,
'payee_type': TypeChoices.USER, 'payee_type': TypeChoices.USER,
'payee_wallet': self.wallet_category1_uuid, 'payee_wallet': self.wallet_rial_uuid,
} }
kwargs_payer_wallet = {
kwargs_category_uuid_payee_uuid = { 'payer_wallet': self.wallet_rial_uuid,
'category_uuid': self.wallet_category1_uuid,
} }
response = self.client.post(reverse('wallet:application_deposit_submit_api', kwargs=kwargs_category_uuid_payee_uuid), data=data, HTTP_AUTHORIZATION=auth) response = self.client.post(reverse('wallet:application_deposit_submit_api', kwargs=kwargs_payer_wallet), data=data, HTTP_AUTHORIZATION=auth)
print(response.json())
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
response = self.client.post(reverse('wallet:application_deposit_submit_api', kwargs=kwargs_category_uuid_payee_uuid), data=data, HTTP_AUTHORIZATION=auth) response = self.client.post(reverse('wallet:application_deposit_submit_api', kwargs=kwargs_payer_wallet), data=data, HTTP_AUTHORIZATION=auth)
# self.assertEqual(response.status_code, 422) self.assertEqual(response.status_code, 422)
self.assertEqual(response.status_code, 201)
def test_withdraw_flow_success(self): def test_withdraw_flow_success(self):
auth = self._create_authorization_header(self.payer_access_token.token) auth = self._create_authorization_header(self.payer_access_token.token)
# Checking wallets initials values(application balance = 300 and payee balance = zero) # Checking wallets initials values(application balance = 300 and payee balance = zero)
response = self.client.get(reverse('wallet:application_wallet_balance_api'), HTTP_AUTHORIZATION=auth) wallets = [self.wallet_rial_uuid.hex, self.wallet_reward_uuid.hex]
self.assertContains(response, 'balance') response = self.client.get(reverse('wallet:application_account_list_api'), query_params={'wallet': ','.join(wallets)}, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.json()['balance'], 300) self.assertGreater(response.json()['count'], 1)
from_wallet = response.json()['results'][0]
self.assertEqual(from_wallet['balance'], 300)
params = { params = {
'uuid': self.payer_uuid, 'owner': self.payer_uuid,
'category_uuid': self.wallet_category1_uuid,
} }
response = self.client.get(reverse('wallet:application_user_wallet_balance_api', kwargs=params), HTTP_AUTHORIZATION=auth) response = self.client.get(reverse('wallet:application_user_account_list_api', kwargs=params), query_params={'wallet': ','.join(wallets)}, HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance') self.assertGreater(response.json()['count'], 1)
self.assertEqual(response.json()['balance'], 300) to_wallet = response.json()['results'][0]
self.assertEqual(to_wallet['balance'], 300)
# Deposit process # Deposit process
data = { data = {
'amount': 100, 'amount': 100,
'payer': self.payer_uuid, 'payer': self.payer_uuid,
'payer_type': TypeChoices.USER, 'payer_type': TypeChoices.USER,
'payer_wallet': self.wallet_category1_uuid, 'payer_wallet': self.wallet_rial_uuid,
} }
kwargs_category_uuid = { kwargs_payee_wallet = {
'category_uuid': self.wallet_category1_uuid, 'payee_wallet': self.wallet_rial_uuid,
} }
response = self.client.post(reverse('wallet:application_withdraw_submit_api', kwargs=kwargs_category_uuid), data=data, HTTP_AUTHORIZATION=auth) response = self.client.post(reverse('wallet:application_withdraw_submit_api', kwargs=kwargs_payee_wallet), data=data, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
json_response = response.json() json_response = response.json()
params = { kwargs_payee_wallet_uuid = {
'uuid': json_response['uuid'], 'uuid': json_response['uuid'],
'category_uuid': self.wallet_category1_uuid, 'payee_wallet': self.wallet_rial_uuid,
} }
response = self.client.get(reverse('wallet:application_withdraw_verify_api', kwargs=params), HTTP_AUTHORIZATION=auth) response = self.client.get(reverse('wallet:application_withdraw_verify_api', kwargs=kwargs_payee_wallet_uuid), HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['state'], 5) self.assertEqual(response.json()['state'], 5)
# Desired results # Desired results
response = self.client.get(reverse('wallet:application_wallet_balance_api', kwargs=kwargs_category_uuid), HTTP_AUTHORIZATION=auth) wallets = [self.wallet_rial_uuid.hex, self.wallet_reward_uuid.hex]
self.assertContains(response, 'balance') response = self.client.get(reverse('wallet:application_account_list_api'), query_params={'wallet': ','.join(wallets)}, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.json()['balance'], 400) self.assertGreater(response.json()['count'], 1)
from_wallet = response.json()['results'][0]
self.assertEqual(from_wallet['balance'], 400)
params = { params = {
'uuid': self.payer_uuid, 'owner': self.payer_uuid,
'category_uuid': self.wallet_category1_uuid,
} }
response = self.client.get(reverse('wallet:application_user_wallet_balance_api', kwargs=params), HTTP_AUTHORIZATION=auth) response = self.client.get(reverse('wallet:application_user_account_list_api', kwargs=params), query_params={'wallet': ','.join(wallets)}, HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance') self.assertGreater(response.json()['count'], 1)
self.assertEqual(response.json()['balance'], 200) to_wallet = response.json()['results'][0]
self.assertEqual(to_wallet['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': self.payer_uuid,
'payer_type': TypeChoices.USER,
'payer_wallet': self.wallet_category1_uuid,
}
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): def test_withdraw_flow_zero_amount(self):
auth = self._create_authorization_header(self.payer_access_token.token) auth = self._create_authorization_header(self.payer_access_token.token)
kwargs_payee_wallet = {
'payee_wallet': self.wallet_rial_uuid,
}
# Deposit process # Deposit process
data = { data = {
'amount': 0, 'amount': 0,
'payer': self.payer_uuid, 'payer': self.payer_uuid,
'payer_type': TypeChoices.USER, 'payer_type': TypeChoices.USER,
'payer_wallet': self.wallet_category1_uuid, 'payer_account': self.wallet_rial_uuid,
} }
kwargs_category_uuid = { response = self.client.post(reverse('wallet:application_withdraw_submit_api', kwargs=kwargs_payee_wallet),
'category_uuid': self.wallet_category1_uuid, data=data, HTTP_AUTHORIZATION=auth)
}
response = self.client.post(reverse('wallet:application_withdraw_submit_api', kwargs=kwargs_category_uuid), data=data,
HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
def test_withdraw_flow_negative_amount(self): def test_withdraw_flow_negative_amount(self):
auth = self._create_authorization_header(self.payer_access_token.token) auth = self._create_authorization_header(self.payer_access_token.token)
kwargs_payee_wallet = {
'payee_wallet': self.wallet_rial_uuid,
}
# Deposit process # Deposit process
data = { data = {
'amount': -500, 'amount': -500,
'payer': self.payer_uuid, 'payer': self.payer_uuid,
'payer_type': TypeChoices.USER 'payer_type': TypeChoices.USER,
'payer_account': self.wallet_rial_uuid,
} }
response = self.client.post(reverse('wallet:application_withdraw_submit_api'), data=data, response = self.client.post(reverse('wallet:application_withdraw_submit_api', kwargs=kwargs_payee_wallet), data=data, HTTP_AUTHORIZATION=auth)
HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
def test_withdraw_flow_prevent_double_spending(self): def test_withdraw_flow_prevent_double_spending(self):
auth = self._create_authorization_header(self.payer_access_token.token) auth = self._create_authorization_header(self.payer_access_token.token)
kwargs_category_uuid = { kwargs_wallet_uuid = {
'category_uuid': self.wallet_category1_uuid, 'wallet_uuid': self.wallet_rial_uuid,
} }
# Checking wallets initials values(application balance = 300 and payee balance = zero) # Checking wallets initials values(application balance = 300 and payee balance = zero)
response = self.client.get(reverse('wallet:application_wallet_balance_api', kwargs=kwargs_category_uuid), HTTP_AUTHORIZATION=auth) wallets = [self.wallet_rial_uuid.hex, self.wallet_reward_uuid.hex]
self.assertContains(response, 'balance') response = self.client.get(reverse('wallet:application_account_list_api'), query_params={'wallet': ','.join(wallets)}, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.json()['balance'], 300) self.assertGreater(response.json()['count'], 1)
from_wallet = response.json()['results'][0]
self.assertEqual(from_wallet['balance'], 300)
params = { params = {
'uuid': self.payer_uuid, 'owner': self.payer_uuid,
'category_uuid': self.wallet_category1_uuid,
} }
response = self.client.get(reverse('wallet:application_user_wallet_balance_api', kwargs=params), HTTP_AUTHORIZATION=auth) response = self.client.get(reverse('wallet:application_user_account_list_api', kwargs=params), query_params={'wallet': ','.join(wallets)}, HTTP_AUTHORIZATION=auth)
self.assertContains(response, 'balance') self.assertGreater(response.json()['count'], 1)
self.assertEqual(response.json()['balance'], 300) to_wallet = response.json()['results'][0]
self.assertEqual(to_wallet['balance'], 300)
# Deposit process # Deposit process
data = { data = {
'uuid': 'b14e8b86-8f4a-44d9-b29d-badceb470053', 'amount': 100,
'amount': 200,
'payer': self.payer_uuid, 'payer': self.payer_uuid,
'payer_type': TypeChoices.USER, 'payer_type': TypeChoices.USER,
'payer_wallet': self.wallet_category1_uuid, 'payer_wallet': self.wallet_rial_uuid,
} }
kwargs_category_uuid = { kwargs_payee_wallet = {
'category_uuid': self.wallet_category1_uuid, 'payee_wallet': self.wallet_rial_uuid,
} }
response = self.client.post(reverse('wallet:application_withdraw_submit_api', kwargs=kwargs_category_uuid), data=data, HTTP_AUTHORIZATION=auth)
response = self.client.post(reverse('wallet:application_withdraw_submit_api', kwargs=kwargs_payee_wallet), data=data, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
response = self.client.post(reverse('wallet:application_withdraw_submit_api', kwargs=kwargs_category_uuid), data=data, HTTP_AUTHORIZATION=auth) response = self.client.post(reverse('wallet:application_withdraw_submit_api', kwargs=kwargs_payee_wallet), data=data, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)

View file

@ -1,63 +1,43 @@
from django.urls import path from django.urls import path
from rest_framework.routers import DefaultRouter
from .views import application as application_views from .views import application as application_views
from .views import user as user_views from .views import user as user_views
from .views.user import UserTransactionViewSet
app_name = "wallet" app_name = "wallet"
router = DefaultRouter()
router.register('api/transactions', UserTransactionViewSet, basename='transactions')
urlpatterns = router.urls
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.CategoryMyWalletBalanceView.as_view(), name='user_wallet_balance_api'), path('api/wallets/', user_views.WalletAccountListAPIView.as_view(), name='user_wallet_account_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>/pay', TransactionPay.as_view(), name='transaction_pay_api'),
# path('api/transactions/<uuid>/receipt', TransactionReceipt.as_view(), name='transaction_receipt_api'),
# #
# path('api/deposit/', DepositSubmit.as_view(), name='deposit_submit_api'),
# path('api/deposit/<uuid>/verify', DepositVerify.as_view(), name='deposit_verify_api'),
# #
# path('api/withdraw/', WithdrawSubmit.as_view(), name='withdraw_submit_api'),
# path('api/withdraw/<uuid>/verify', WithdrawVerify.as_view(), name='withdraw_verify_api'),
# path('api/get_balance/', application_views.WalletBalanceView.as_view(), name='wallet_balance'),
# path('api/<wallet>/balance', user_views.WalletMyAccountBalanceView.as_view(), name='user_wallet_balance_api'),
# path('api/<wallet>/transactions/', user_views.WalletTransactionListView.as_view(), name='transaction_list_api'),
] ]
application_urlpatterns = [ application_urlpatterns = [
path('api/application/balance', application_views.CategoryBalanceAPIView.as_view(), name='application_wallet_balance_api'), path('api/application/wallets/', application_views.AccountListAPIView.as_view(), name='application_account_list_api'),
path('api/application/user/wallets/<owner>/', application_views.UserAccountListAPIView.as_view(), name='application_user_account_list_api'),
path('api/application/user_balance/<uuid>', application_views.CategoryUserBalanceAPIView.as_view(), name='application_user_wallet_balance_api'), path('api/application/transactions/', application_views.TransactionListAPIView.as_view(), name='application_transaction_list_api'),
path('api/application/transactions/<uuid>/', application_views.TransactionDetailAPIView.as_view(), name='application_transaction_detail_api'),
path('api/application/deposit/', application_views.CategoryDepositSubmitAPIView.as_view(), name='application_deposit_submit_api'), path('api/application/<payer_wallet>/deposit/', application_views.DepositSubmitAPIView.as_view(), name='application_deposit_submit_api'),
path('api/application/deposit/<uuid>/verify', application_views.CategoryDepositVerifyAPIView.as_view(), name='application_deposit_verify_api'), path('api/application/<payer_wallet>/deposit/<uuid>/verify', application_views.DepositVerifyAPIView.as_view(), name='application_deposit_verify_api'),
path('api/application/<payer_wallet>/deposit/<uuid>/rollback', application_views.DepositRollbackAPIView.as_view(), name='application_deposit_rollback_api'),
path('api/application/withdraw/', application_views.CategoryWithdrawSubmitAPIView.as_view(), name='application_withdraw_submit_api'), path('api/application/<payee_wallet>/withdraw/', application_views.WithdrawSubmitAPIView.as_view(), name='application_withdraw_submit_api'),
path('api/application/withdraw/<uuid>/verify', application_views.CategoryWithdrawVerifyAPIView.as_view(), name='application_withdraw_verify_api'), path('api/application/<payee_wallet>/withdraw/<uuid>/verify', application_views.WithdrawVerifyAPIView.as_view(), name='application_withdraw_verify_api'),
path('api/application/<payee_wallet>/withdraw/<uuid>/rollback', application_views.WithdrawRollbackAPIView.as_view(), name='application_withdraw_rollback_api'),
path('api/application/', application_views.CategoryTransactionListAPIView.as_view(), name='application_transaction_list_api'), # path('api/application/wallets/<wallet>/', application_views.WalletTransactionListAPIView.as_view(), name='application_transaction_list_api'),
path('api/application/<uuid>/', application_views.CategoryTransactionDetailAPIView.as_view(), name='application_transaction_detail'), # path('api/application/wallets/<wallet>/<uuid>/', application_views.WalletTransactionDetailAPIView.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'),
] ]
urlpatterns = user_urlpatterns + application_urlpatterns urlpatterns += user_urlpatterns + application_urlpatterns

View file

@ -1,18 +1,21 @@
import logging import logging
from django.db.models import Q from django.db.models import Q
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, 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.request import Request
from rest_framework.response import Response from rest_framework.response import Response
import uuid
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, Category, Wallet from apps.wallet.filters import AccountFilter, UserTransactionFilter
from apps.wallet.models import Transaction, Wallet, Account
from apps.wallet.serializers import ApplicationDepositSerializer, ApplicationWithdrawSerializer, \ from apps.wallet.serializers import ApplicationDepositSerializer, ApplicationWithdrawSerializer, \
TransactionSerializer, BalanceSerializer TransactionSerializer, AccountSerializer
from utils.exceptions import UnprocessableEntity from utils.exceptions import UnprocessableEntity
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -27,64 +30,143 @@ def get_application(request):
return application return application
def get_wallet_category(request: Request): class AccountListAPIView(generics.ListAPIView):
pass
class BalanceAPIView(generics.RetrieveAPIView):
''' '''
API for getting specified user or application balance. API for getting specified user or application balance.
''' '''
permission_classes = [TokenHasScope] serializer_class = AccountSerializer
serializer_class = BalanceSerializer filter_backends = (DjangoFilterBackend,)
filterset_class = AccountFilter
permission_classes = [IsAuthenticatedOrTokenHasScope]
required_scopes = ['wallet.application:get_balance'] required_scopes = ['wallet.application:get_balance']
def get_object(self): def get_object(self):
return get_application(self.request) wallet_uuid = self.kwargs.get('wallet')
wallet = get_object_or_404(Wallet.objects.all(), uuid=wallet_uuid)
application = get_application(self.request)
class UserBalanceAPIView(generics.RetrieveAPIView): account, created = Account.objects.get_or_create(owner_uuid=application.uuid, owner_type=TypeChoices.APPLICATION,
''' wallet=wallet)
API for getting specified user or application balance. return account
'''
permission_classes = [IsAuthenticatedOrTokenHasScope]
serializer_class = BalanceSerializer
required_scopes = ['wallet.application:get_user_balance']
def get_object(self):
user_uuid = self.kwargs.get('uuid')
user, created = User.objects.get_or_create(uuid=user_uuid)
# user = get_object_or_404(User.objects.all(), uuid=user_uuid)
return user
class TransactionDetailAPIView(generics.RetrieveAPIView):
permission_classes = [TokenHasScope]
serializer_class = TransactionSerializer
lookup_field = 'uuid'
required_scopes = ['wallet.application:get_transaction_detail']
def get_queryset(self): def get_queryset(self):
application = get_application(self.request)
return Account.objects.filter(owner_uuid=application.uuid, owner_type=TypeChoices.APPLICATION).all()
class UserAccountListAPIView(generics.ListAPIView):
'''
API for getting specified user wallet.
'''
serializer_class = AccountSerializer
filter_backends = (DjangoFilterBackend,)
filterset_class = AccountFilter
permission_classes = [IsAuthenticatedOrTokenHasScope]
required_scopes = ['wallet.application:get_user_balance']
queryset = Account.objects.all()
def get_queryset(self):
qs = super().get_queryset()
owner_uuid = self.kwargs.get('owner')
user, created = User.objects.get_or_create(uuid=self.kwargs.get('uuid'))
return qs.filter(owner_uuid=owner_uuid, owner_type=TypeChoices.USER).all()
class TransactionDetailAPIView(generics.RetrieveAPIView):
serializer_class = TransactionSerializer
lookup_field = 'uuid'
filter_backends = (DjangoFilterBackend,)
permission_classes = [IsAuthenticatedOrTokenHasScope]
required_scopes = ['wallet.application:get_transaction_detail']
queryset = Transaction.objects.all()
def get_queryset(self):
qs = super().get_queryset()
application = get_application(self.request) application = get_application(self.request)
return Transaction.objects.filter(Q(application=application) | return Transaction.objects.filter(Q(application=application) |
Q(payer_id=application.pk, payer_type=TypeChoices.APPLICATION.value) | Q(payer_account__owner_uuid=application.pk,
Q(payee_id=application.pk, payee_type=TypeChoices.APPLICATION.value) payer_account__owner_type=TypeChoices.APPLICATION,
) |
Q(payee_account__owner_uuid=application.pk,
payee_account__owner_type=TypeChoices.APPLICATION,
)
).all() ).all()
class TransactionListAPIView(generics.ListAPIView): class TransactionListAPIView(generics.ListAPIView):
serializer_class = TransactionSerializer
lookup_field = 'uuid'
filter_backends = (DjangoFilterBackend,)
filterset_class = UserTransactionFilter
permission_classes = [IsAuthenticatedOrTokenHasScope]
required_scopes = ['wallet.application:get_transaction_list']
queryset = Transaction.objects.all()
def get_queryset(self):
qs = super().get_queryset()
application = get_application(self.request)
return qs.filter(Q(application=application) |
Q(payer_account__owner_uuid=application.pk,
payer_account__owner_type=TypeChoices.APPLICATION) |
Q(payee_account__owner_uuid=application.pk,
payee_account__owner_type=TypeChoices.APPLICATION,
)
).all()
class WalletTransactionDetailAPIView(generics.RetrieveAPIView):
permission_classes = [TokenHasScope]
serializer_class = TransactionSerializer
lookup_field = 'uuid'
required_scopes = ['wallet.application:get_transaction_detail']
queryset = Transaction.objects.all()
def get_queryset(self):
qs = super().get_queryset()
wallet_uuid = self.kwargs.get('wallet_uuid')
if wallet_uuid:
wallet = get_object_or_404(Wallet.objects.all(), uuid=wallet_uuid)
else:
wallet = None
application = get_application(self.request)
return Transaction.objects.filter(Q(application=application) |
Q(payer_account__owner_uuid=application.pk,
payer_account__owner_type=TypeChoices.APPLICATION,
payer_account__wallet=wallet) |
Q(payee_account__owner_uuid=application.pk,
payee_account__owner_type=TypeChoices.APPLICATION,
payee_account__wallet=wallet)
).all()
class WalletTransactionListAPIView(generics.ListAPIView):
permission_classes = [TokenHasScope] permission_classes = [TokenHasScope]
serializer_class = TransactionSerializer serializer_class = TransactionSerializer
lookup_field = 'uuid' lookup_field = 'uuid'
required_scopes = ['wallet.application:get_transaction_list'] required_scopes = ['wallet.application:get_transaction_list']
queryset = Transaction.objects.all()
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset()
wallet_uuid = self.kwargs.get('wallet_uuid')
if wallet_uuid:
wallet = get_object_or_404(Wallet.objects.all(), uuid=wallet_uuid)
else:
wallet = None
application = get_application(self.request) application = get_application(self.request)
return Transaction.objects.filter(Q(application=application) | return qs.filter(Q(application=application) |
Q(payer_id=application.pk, payer_type=TypeChoices.APPLICATION.value) | Q(payer_account__owner_uuid=application.pk,
Q(payee_id=application.pk, payee_type=TypeChoices.APPLICATION.value) payer_account__owner_type=TypeChoices.APPLICATION,
).all() payer_account__wallet=wallet) |
Q(payee_account__owner_uuid=application.pk,
payee_account__owner_type=TypeChoices.APPLICATION,
payee_account__wallet=wallet)
).all()
# from application to another entity # from application to another entity
@ -95,23 +177,66 @@ class DepositSubmitAPIView(generics.CreateAPIView):
required_scopes = ['wallet.application.deposit:submit'] required_scopes = ['wallet.application.deposit:submit']
def get_queryset(self): def get_queryset(self):
payer_wallet_uuid = self.kwargs.get('payer_wallet')
wallet = get_object_or_404(Wallet.objects.all(), uuid=payer_wallet_uuid)
application = get_application(self.request) application = get_application(self.request)
return Transaction.objects.filter(application=application, payer_account, created = Account.objects.get_or_create(owner_uuid=application.uuid, owner_type=TypeChoices.APPLICATION,
payer_id=application.pk, wallet=wallet)
payer_type=TypeChoices.APPLICATION.value).all() return Transaction.objects.filter(application=application, payer_account=payer_account).all()
def perform_create(self, serializer): def perform_create(self, serializer):
# TODO: get_or_create instead of create if uuid given # 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 # TODO: it is possible to merage create and submit in one step for improve performance
logger.info("Starting perform_create for Transaction")
application = get_application(self.request) application = get_application(self.request)
instance = serializer.save(payer_id=application.pk, logger.debug(f"Application UUID: {application.uuid}")
payer_type=TypeChoices.APPLICATION.value, payer_wallet_uuid = self.kwargs.get('payer_wallet')
application=application) if payer_wallet_uuid:
try: payer_wallet = get_object_or_404(Wallet.objects.all(), uuid=payer_wallet_uuid)
instance.category_submit() else:
except Exception as e: payer_wallet = None
print(e) payee_wallet_uuid = serializer.validated_data.pop('payee_wallet')
APIException('Failed to submit deposit') payee_wallet_uuid = payee_wallet_uuid and str(payee_wallet_uuid)
payee_wallet = get_object_or_404(Wallet.objects.all(), uuid=payee_wallet_uuid)
if not (payee_wallet and payee_wallet.currency == payer_wallet.currency):
raise UnprocessableEntity('payer and payee wallet currency not equal')
payee_id = serializer.validated_data.pop('payee')
payee_type = serializer.validated_data.pop('payee_type')
payer_account, _ = Account.objects.get_or_create(owner_uuid=application.uuid, owner_type=TypeChoices.APPLICATION,
wallet=payer_wallet)
logger.debug(f"Payer account: {payer_account.uuid}")
payee_account, _ = Account.objects.get_or_create(owner_uuid=payee_id, owner_type=payee_type,
wallet=payee_wallet)
logger.debug(f"Payee account: {payee_account.uuid}")
uuid_param = serializer.validated_data.pop('uuid', uuid.uuid4()) # if uuid not provided generate new one to handle get_or_create
details = serializer.validated_data.get('details', {})
reference_id = details.get('reference_id', None)
logger.debug(f"Transaction UUID: {uuid_param}, Reference ID: {reference_id}")
transaction_params = {
'uuid': uuid_param,
'application': application,
'payer_account': payer_account,
'payee_account': payee_account,
'amount': serializer.validated_data.get('amount'),
}
# TODO: checking reference_id
# if reference_id:
# transaction_params['details__reference_id'] = reference_id
instance, created = Transaction.objects.get_or_create(**transaction_params, defaults={
'details': details
})
serializer.instance = instance
logger.info(f"Transaction {'created' if created else 'retrieved'} successfully: {instance.uuid}")
if instance.state == StateChoices.PENDING:
return
if created or instance.state == StateChoices.CREATED:
instance.submit()
else:
logger.warning(f"Transaction was not in processable state! for {instance.uuid}: {instance.state}")
raise UnprocessableEntity(_('Transaction was not in processable state!'))
class DepositVerifyAPIView(generics.RetrieveAPIView): class DepositVerifyAPIView(generics.RetrieveAPIView):
@ -122,15 +247,45 @@ class DepositVerifyAPIView(generics.RetrieveAPIView):
def get_queryset(self): def get_queryset(self):
application = get_application(self.request) application = get_application(self.request)
return Transaction.objects.filter(application=application, payer_wallet_uuid = self.kwargs.get('payer_wallet')
payer_id=application.pk, if payer_wallet_uuid:
payer_type=TypeChoices.APPLICATION.value).all() wallet = get_object_or_404(Wallet.objects.all(), uuid=payer_wallet_uuid)
else:
wallet = None
account = Account.objects.get(owner_uuid=application.uuid, owner_type=TypeChoices.APPLICATION, wallet=wallet)
return Transaction.objects.filter(application=application, payer_account=account).all()
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
if instance.state == StateChoices.PENDING: if instance.state == StateChoices.PENDING:
instance.category_verify() instance.verify()
logger.warning(f"Transaction was verified for {instance.uuid}: {instance.state}")
serializer = self.get_serializer(instance)
return Response(serializer.data)
class DepositRollbackAPIView(generics.RetrieveAPIView):
permission_classes = [IsAuthenticatedOrTokenHasScope]
serializer_class = ApplicationDepositSerializer
lookup_field = 'uuid'
required_scopes = ['wallet.application.deposit:rollback']
def get_queryset(self):
application = get_application(self.request)
payer_wallet_uuid = self.kwargs.get('payer_wallet')
if payer_wallet_uuid:
wallet = get_object_or_404(Wallet.objects.all(), uuid=payer_wallet_uuid)
else:
wallet = None
account = Account.objects.get(owner_uuid=application.uuid, owner_type=TypeChoices.APPLICATION, wallet=wallet)
return Transaction.objects.filter(application=application, payer_account=account).all()
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
instance.rollback()
logger.warning(f"Transaction was rolled back for {instance.uuid}: {instance.state}")
serializer = self.get_serializer(instance) serializer = self.get_serializer(instance)
return Response(serializer.data) return Response(serializer.data)
@ -142,20 +297,72 @@ class WithdrawSubmitAPIView(generics.CreateAPIView):
required_scopes = ['wallet.application.withdraw:submit'] required_scopes = ['wallet.application.withdraw:submit']
def get_queryset(self): def get_queryset(self):
payee_wallet_uuid = self.kwargs.get('payee_wallet')
wallet = get_object_or_404(Wallet.objects.all(), uuid=payee_wallet_uuid)
application = get_application(self.request) application = get_application(self.request)
return Transaction.objects.filter(application=application, payee_account, created = Account.objects.get_or_create(owner_uuid=application.uuid, owner_type=TypeChoices.APPLICATION,
payee_id=application.pk, wallet=wallet)
payee_type=TypeChoices.APPLICATION.value).all() return Transaction.objects.filter(application=application, payee_account=payee_account).all()
def perform_create(self, serializer): def perform_create(self, serializer):
# TODO: get_or_create instead of create if uuid given # 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 # TODO: it is possible to merage create and submit in one step for improve performance
logger.info("Starting perform_create for Transaction")
application = get_application(self.request) application = get_application(self.request)
instance = serializer.save(payee_id=application.pk, logger.debug(f"Application UUID: {application.uuid}")
payee_type=TypeChoices.APPLICATION.value,
application=application)
instance.category_submit() payer_wallet_uuid = serializer.validated_data.pop('payer_wallet')
payer_wallet_uuid = payer_wallet_uuid and str(payer_wallet_uuid)
payee_wallet_uuid = self.kwargs.get('payee_wallet')
payee_wallet = get_object_or_404(Wallet.objects.all(), uuid=payee_wallet_uuid)
payer_wallet = get_object_or_404(Wallet.objects.all(), uuid=payer_wallet_uuid)
if not (payer_wallet and payer_wallet.currency == payee_wallet.currency):
raise UnprocessableEntity('payer and payee wallet currency not equal')
payer_id = serializer.validated_data.pop('payer')
payer_type = serializer.validated_data.pop('payer_type')
payee_account, _ = Account.objects.get_or_create(owner_uuid=application.uuid, owner_type=TypeChoices.APPLICATION,
wallet=payee_wallet)
logger.debug(f"Payee account: {payee_account.uuid}")
payer_account, _ = Account.objects.get_or_create(owner_uuid=payer_id, owner_type=payer_type,
wallet=payer_wallet)
logger.debug(f"Payer account: {payer_account.uuid}")
uuid_param = serializer.validated_data.pop('uuid', uuid.uuid4()) # if uuid not provided generate new one to handle get_or_create
details = serializer.validated_data.get('details', {})
reference_id = details.get('reference_id', None)
logger.debug(f"Transaction UUID: {uuid}, Reference ID: {reference_id}")
transaction_params = {
'uuid': uuid_param,
'application': application,
'payer_account': payer_account,
'payee_account': payee_account,
'amount': serializer.validated_data.get('amount'),
}
# TODO: checking reference_id
# if reference_id:
# transaction_params['details__reference_id'] = reference_id
instance, created = Transaction.objects.get_or_create(**transaction_params, defaults={
'details': details
})
serializer.instance = instance
logger.info(f"Transaction {'created' if created else 'retrieved'} successfully: {instance.uuid}")
if instance.state == StateChoices.PENDING:
return
if created or instance.state == StateChoices.CREATED:
instance.submit()
else:
logger.warning(f"Transaction was not in processable state! for {instance.uuid}: {instance.state}")
raise UnprocessableEntity(_('Transaction was not in processable state!'))
class WithdrawVerifyAPIView(generics.RetrieveAPIView): class WithdrawVerifyAPIView(generics.RetrieveAPIView):
@ -166,311 +373,37 @@ class WithdrawVerifyAPIView(generics.RetrieveAPIView):
def get_queryset(self): def get_queryset(self):
application = get_application(self.request) application = get_application(self.request)
return Transaction.objects.filter(application=application, payee_wallet_uuid = self.kwargs.get('payee_wallet')
payee_id=application.pk, wallet = get_object_or_404(Wallet.objects.all(), uuid=payee_wallet_uuid)
payee_type=TypeChoices.APPLICATION.value).all() payee_account = Account.objects.get(owner_uuid=application.uuid, owner_type=TypeChoices.APPLICATION, wallet=wallet)
return Transaction.objects.filter(application=application, payee_account=payee_account).all()
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
if instance.state == StateChoices.PENDING: if instance.state == StateChoices.PENDING:
instance.category_verify() instance.verify()
serializer = self.get_serializer(instance) serializer = self.get_serializer(instance)
return Response(serializer.data) return Response(serializer.data)
class CategoryBalanceAPIView(generics.RetrieveAPIView): class WithdrawRollbackAPIView(generics.RetrieveAPIView):
'''
API for getting specified user or application balance.
'''
permission_classes = [TokenHasScope]
serializer_class = BalanceSerializer
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 = BalanceSerializer
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 = TransactionSerializer
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 = TransactionSerializer
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
logger.info("Starting perform_create for Transaction")
application = get_application(self.request)
logger.debug(f"Application UUID: {application.uuid}")
payee_wallet_category_uuid = serializer.validated_data.pop('payee_wallet_category')
payee_wallet_category_uuid = payee_wallet_category_uuid and str(payee_wallet_category_uuid)
payer_wallet_category_uuid = self.kwargs.get('category_uuid')
if payer_wallet_category_uuid:
payer_wallet_category = get_object_or_404(Category.objects.all(), uuid=payer_wallet_category_uuid)
else:
payer_wallet_category = None
payee_wallet_category = get_object_or_404(Category.objects.all(), uuid=payee_wallet_category_uuid)
if not (payee_wallet_category and payee_wallet_category.currency == payer_wallet_category.currency):
raise UnprocessableEntity('payer and payee wallet currency not equal')
payee_id = serializer.validated_data.pop('payee')
payee_type = serializer.validated_data.pop('payee_type')
payer_wallet, _ = Wallet.objects.get_or_create(owner_uuid=application.uuid, owner_type=TypeChoices.APPLICATION,
category=payer_wallet_category)
logger.debug(f"Payer wallet: {payer_wallet.uuid}")
payee_wallet, _ = Wallet.objects.get_or_create(owner_uuid=payee_id, owner_type=payee_type,
category=payee_wallet_category)
logger.debug(f"Payee wallet: {payee_wallet.uuid}")
uuid = serializer.validated_data.pop('uuid', None)
details = serializer.validated_data.get('details', {})
reference_id = details.get('reference_id', None)
logger.debug(f"Transaction UUID: {uuid}, Reference ID: {reference_id}")
transaction_params = {
'application': application,
'payer_wallet': payer_wallet,
'payee_wallet': payee_wallet,
'amount': serializer.validated_data.get('amount'),
}
if uuid:
transaction_params['uuid'] = uuid
if reference_id:
transaction_params['details__reference_id'] = reference_id
# else:
# logger.error('Missing UUID and Reference ID')
# raise UnprocessableEntity('Missing UUID or reference_id')
instance, created = Transaction.objects.get_or_create(**transaction_params, defaults={
'details': details
})
serializer.instance = instance
logger.info(f"Transaction {'created' if created else 'retrieved'} successfully: {instance.uuid}")
if instance.state == StateChoices.PENDING:
return
# TODO: status code 200 if not created before
if created or instance.state == StateChoices.CREATED:
instance.category_submit()
else:
logger.warning(f"Transaction was not in processable state! for {instance.uuid}: {instance.state}")
raise UnprocessableEntity(_('Transaction was not in processable state!'))
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()
logger.warning(f"Transaction was retrieved for {instance.uuid}: {instance.state}")
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
logger.info("Starting perform_create for Transaction")
application = get_application(self.request)
logger.debug(f"Application UUID: {application.uuid}")
payer_wallet_category_uuid = serializer.validated_data.pop('payer_wallet_category')
payer_wallet_category_uuid = payer_wallet_category_uuid and str(payer_wallet_category_uuid)
payee_wallet_category_uuid = self.kwargs.get('category_uuid')
if payee_wallet_category_uuid:
payee_wallet_category = get_object_or_404(Category.objects.all(), uuid=payee_wallet_category_uuid)
else:
payee_wallet_category = None
payer_wallet_category = get_object_or_404(Category.objects.all(), uuid=payer_wallet_category_uuid)
if not (payer_wallet_category and payer_wallet_category.currency == payee_wallet_category.currency):
raise UnprocessableEntity('payer and payee wallet currency not equal')
payer_id = serializer.validated_data.pop('payer')
payer_type = serializer.validated_data.pop('payer_type')
payee_wallet, _ = Wallet.objects.get_or_create(owner_uuid=application.uuid, owner_type=TypeChoices.APPLICATION,
category=payee_wallet_category)
logger.debug(f"Payee wallet: {payee_wallet.uuid}")
payer_wallet, _ = Wallet.objects.get_or_create(owner_uuid=payer_id, owner_type=payer_type,
category=payer_wallet_category)
logger.debug(f"Payer wallet: {payer_wallet.uuid}")
uuid = serializer.validated_data.pop('uuid', None)
details = serializer.validated_data.get('details', {})
reference_id = details.get('reference_id', None)
logger.debug(f"Transaction UUID: {uuid}, Reference ID: {reference_id}")
transaction_params = {
'application': application,
'payer_wallet': payer_wallet,
'payee_wallet': payee_wallet,
'amount': serializer.validated_data.get('amount'),
}
if uuid:
transaction_params['uuid'] = uuid
if reference_id:
transaction_params['details__reference_id'] = reference_id
# else:
# logger.error('Missing UUID and Reference ID')
# raise UnprocessableEntity('Missing UUID or reference_id')
instance, created = Transaction.objects.get_or_create(**transaction_params, defaults={
'details': details
})
serializer.instance = instance
logger.info(f"Transaction {'created' if created else 'retrieved'} successfully: {instance.uuid}")
if instance.state == StateChoices.PENDING:
return
# TODO: status code 200 if not created before
if created or instance.state == StateChoices.CREATED:
instance.category_submit()
else:
logger.warning(f"Transaction was not in processable state! for {instance.uuid}: {instance.state}")
raise UnprocessableEntity(_('Transaction was not in processable state!'))
class CategoryWithdrawVerifyAPIView(generics.RetrieveAPIView):
permission_classes = [TokenHasScope] permission_classes = [TokenHasScope]
serializer_class = ApplicationWithdrawSerializer serializer_class = ApplicationWithdrawSerializer
lookup_field = 'uuid' lookup_field = 'uuid'
required_scopes = ['wallet.application.withdraw:verify'] required_scopes = ['wallet.application.withdraw:rollback']
def get_queryset(self): def get_queryset(self):
application = get_application(self.request) application = get_application(self.request)
category_uuid = self.kwargs.get('category_uuid') payee_wallet_uuid = self.kwargs.get('payee_wallet')
if category_uuid: wallet = get_object_or_404(Wallet.objects.all(), uuid=payee_wallet_uuid)
category = get_object_or_404(Category.objects.all(), uuid=category_uuid) payee_account = Account.objects.get(owner_uuid=application.uuid, owner_type=TypeChoices.APPLICATION, wallet=wallet)
else: return Transaction.objects.filter(application=application, payee_account=payee_account).all()
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): def retrieve(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
if instance.state == StateChoices.PENDING:
instance.category_verify()
instance.rollback()
logger.warning(f"Transaction was rolled back for {instance.uuid}: {instance.state}")
serializer = self.get_serializer(instance) serializer = self.get_serializer(instance)
return Response(serializer.data) return Response(serializer.data)

View file

@ -1,23 +1,31 @@
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.postgres.aggregates import ArrayAgg
from django.db.models import Q from django.db.models import Q
from django.urls import reverse from django.urls import reverse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.generic import DetailView, ListView, UpdateView from django.views.generic import DetailView, ListView, UpdateView
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import extend_schema
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, mixins
from rest_framework.decorators import action
from rest_framework.generics import get_object_or_404 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 rest_framework.viewsets import ViewSet, GenericViewSet
from apps.wallet.models import Transaction
from django.contrib.postgres.aggregates import ArrayAgg
from django.db.models import Sum, Count, F
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
from apps.wallet.models import Transaction, Category, Wallet from apps.wallet.models import Transaction, Wallet, Account
from apps.wallet.serializers import TransactionSerializer, DepositSerializer, WithdrawSerializer, \ from apps.wallet.serializers import TransactionSerializer, DepositSerializer, WithdrawSerializer, \
BalanceSerializer AccountSerializer, TransactionGroupedSerializer
from ..constans import StateChoices, TypeChoices from ..constans import StateChoices, TypeChoices
from ..filters import UserTransactionFilter from ..filters import AccountFilter, UserTransactionFilter
from ..forms import TransactionActionForm from ..forms import TransactionActionForm
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -31,12 +39,12 @@ def get_application(request):
return application return application
class MyWalletBalanceView(generics.RetrieveAPIView): class UserAccountBalanceView(generics.RetrieveAPIView):
''' '''
API for getting authenticated user balance. API for getting authenticated user balance.
''' '''
permission_classes = [IsAuthenticatedOrTokenHasScope] permission_classes = [IsAuthenticatedOrTokenHasScope]
serializer_class = BalanceSerializer serializer_class = AccountSerializer
required_scopes = ['wallet.wallet:get_balance'] required_scopes = ['wallet.wallet:get_balance']
def get_object(self): def get_object(self):
@ -57,7 +65,7 @@ class TransactionList(generics.ListAPIView):
required_alternate_scopes = { required_alternate_scopes = {
"GET": [['wallet.user:transaction_list']], "GET": [['wallet.user:transaction_list']],
# "POST": [['wallet.invoice:create']], # "POST": [['wallet.invoice:create']],
} }
filter_backends = (DjangoFilterBackend,) filter_backends = (DjangoFilterBackend,)
filterset_class = UserTransactionFilter filterset_class = UserTransactionFilter
@ -66,7 +74,6 @@ class TransactionList(generics.ListAPIView):
user = self.request.user user = self.request.user
return Transaction.objects.filter(Q(payee_id=user.pk, payee_type=TypeChoices.USER.value) | Q(payer_id=user.pk, payer_type=TypeChoices.USER.value)).all() return Transaction.objects.filter(Q(payee_id=user.pk, payee_type=TypeChoices.USER.value) | Q(payer_id=user.pk, payer_type=TypeChoices.USER.value)).all()
# TODO: # TODO:
# def perform_create(self, serializer): # def perform_create(self, serializer):
# payer = self.request.user # payer = self.request.user
@ -133,6 +140,7 @@ class DepositSubmit(generics.CreateAPIView):
serializer.save(payer=payer, application=get_application(self.request)) serializer.save(payer=payer, application=get_application(self.request))
# TODO: refactore
class DepositVerify(generics.RetrieveAPIView): class DepositVerify(generics.RetrieveAPIView):
permission_classes = [TokenHasScope] permission_classes = [TokenHasScope]
serializer_class = DepositSerializer serializer_class = DepositSerializer
@ -166,6 +174,7 @@ class WithdrawSubmit(generics.CreateAPIView):
serializer.save(payee=payee, application=get_application(self.request)) serializer.save(payee=payee, application=get_application(self.request))
# TODO: refactor
class WithdrawVerify(generics.RetrieveAPIView): class WithdrawVerify(generics.RetrieveAPIView):
permission_classes = [TokenHasScope] permission_classes = [TokenHasScope]
serializer_class = WithdrawSerializer serializer_class = WithdrawSerializer
@ -225,30 +234,34 @@ class TransactionDetailView(UpdateView):
return super().form_valid(form) return super().form_valid(form)
class WalletAccountListAPIView(generics.ListAPIView):
class CategoryMyWalletBalanceView(generics.RetrieveAPIView):
''' '''
API for getting authenticated user balance. API for getting authenticated user balance.
''' '''
permission_classes = [IsAuthenticatedOrTokenHasScope] permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
serializer_class = BalanceSerializer serializer_class = AccountSerializer
required_scopes = ['wallet.wallet:get_balance'] filter_backends = (DjangoFilterBackend,)
filterset_class = AccountFilter
required_alternate_scopes = {
"GET": [['wallet.user:transaction_list']],
# "POST": [['wallet.invoice:create']],
}
queryset = Account.objects.all()
def get_queryset(self):
user = self.request.user
return Account.objects.filter(owner_uuid=user.uuid).all()
def get_object(self): def get_object(self):
user = self.request.user user = self.request.user
category_uuid = self.kwargs.get('category_uuid') wallet_uuid = self.kwargs.get('wallet')
if category_uuid: wallet = get_object_or_404(Wallet.objects.all(), uuid=wallet_uuid)
category = get_object_or_404(Category.objects.all(), uuid=category_uuid) wallet, created = Account.objects.get_or_create(owner_uuid=user.uuid, owner_type=TypeChoices.USER, wallet=wallet)
else:
category = None
wallet, created = Wallet.objects.get_or_create(owner_uuid=user.uuid, owner_type=TypeChoices.USER,
category=category)
return wallet return wallet
# this is application create invoice view # this is application create invoice view
class CategoryTransactionListView(generics.ListAPIView): class TransactionListAPIView(generics.ListAPIView):
''' '''
GET: API for getting list of transactions for authenticated user 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) POST: API for creating transaction for transferring funds from authenticated user to application owner. (deprecated)
@ -261,22 +274,99 @@ class CategoryTransactionListView(generics.ListAPIView):
required_alternate_scopes = { required_alternate_scopes = {
"GET": [['wallet.user:transaction_list']], "GET": [['wallet.user:transaction_list']],
# "POST": [['wallet.invoice:create']], # "POST": [['wallet.invoice:create']],
} }
filter_backends = (DjangoFilterBackend,)
filterset_class = UserTransactionFilter
queryset = Transaction.objects.all()
# TODO: redundant
def get_queryset(self):
user = self.request.user
return Transaction.objects.filter(Q(payee_account__owner_uuid=user.uuid) |
Q(payer_account__owner_uuid=user.uuid)).all()
class WalletTransactionListView(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 = TransactionSerializer
required_alternate_scopes = {
"GET": [['wallet.user:transaction_list']],
# "POST": [['wallet.invoice:create']],
}
filter_backends = (DjangoFilterBackend,) filter_backends = (DjangoFilterBackend,)
filterset_class = UserTransactionFilter filterset_class = UserTransactionFilter
def get_queryset(self): def get_queryset(self):
user = self.request.user user = self.request.user
category_uuid = self.kwargs.get('category_uuid') wallet_uuid = self.kwargs.get('wallet')
if category_uuid: wallet = get_object_or_404(Wallet.objects.all(), uuid=wallet_uuid)
category = get_object_or_404(Category.objects.all(), uuid=category_uuid) account, created = Account.objects.get_or_create(owner_uuid=user.uuid, owner_type=TypeChoices.USER, wallet=wallet)
else: return Transaction.objects.filter(Q(payee_account=account) | Q(payer_account=account)).all()
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) | class UserTransactionViewSet(mixins.ListModelMixin,
Q(payer_wallet=wallet)).all() # mixins.CreateModelMixin,
# mixins.RetrieveModelMixin,
# mixins.UpdateModelMixin,
# mixins.DestroyModelMixin,
GenericViewSet):
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
serializer_class = TransactionSerializer
required_alternate_scopes = {
"GET": [['wallet.user:transaction_list']],
# "POST": [['wallet.invoice:create']],
}
filter_backends = (DjangoFilterBackend,)
filterset_class = UserTransactionFilter
queryset = Transaction.objects.all()
# TODO: redundant
def get_queryset(self):
user = self.request.user
return Transaction.objects.filter(Q(payee_account__owner_uuid=user.uuid) |
Q(payer_account__owner_uuid=user.uuid)).all()
@extend_schema(responses=TransactionGroupedSerializer(many=True))
@action(detail=False, methods=["GET"], serializer_class=TransactionGroupedSerializer)
def grouped(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
grouped_transaction_list = queryset.values('details__reference_id', 'application').annotate(
total_amount=Sum('amount'),
total_count=Count('uuid'),
transactions=ArrayAgg('uuid'),
)
page = list(self.paginate_queryset(grouped_transaction_list))
transaction_uuid_list = []
for grouped_transaction in page:
transaction_uuid_list += grouped_transaction['transactions']
transaction_list = Transaction.objects.filter(uuid__in=transaction_uuid_list)
transaction_dict = {t.pk: t for t in transaction_list}
for grouped_transaction in page:
grouped_transaction: dict
for i, transaction in enumerate(grouped_transaction['transactions']):
grouped_transaction.setdefault('description', transaction_dict[transaction].details.get('description', 'no-description'))
grouped_transaction.setdefault('state', transaction_dict[transaction].state)
grouped_transaction.setdefault('created_at', transaction_dict[transaction].created_at)
grouped_transaction.setdefault('role', transaction_dict[transaction].get_role(self.request.user))
grouped_transaction['transactions'][i] = transaction_dict[transaction]
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)

View file

@ -0,0 +1 @@
"""Contains endpoint functions for accessing the API"""

View file

@ -0,0 +1 @@
"""Contains endpoint functions for accessing the API"""

View file

@ -0,0 +1,151 @@
from http import HTTPStatus
from typing import Any, Optional, Union
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.user_inquiry import UserInquiry
from ...types import Response
def _get_kwargs(
*,
body: UserInquiry,
) -> dict[str, Any]:
headers: dict[str, Any] = {}
_kwargs: dict[str, Any] = {
"method": "post",
"url": "/users/api/inquiry/",
}
_body = body.to_dict()
_kwargs["json"] = _body
headers["Content-Type"] = "application/json"
_kwargs["headers"] = headers
return _kwargs
def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[UserInquiry]:
if response.status_code == 201:
response_201 = UserInquiry.from_dict(response.json())
return response_201
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[UserInquiry]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
*,
client: AuthenticatedClient,
body: UserInquiry,
) -> Response[UserInquiry]:
"""
Args:
body (UserInquiry):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[UserInquiry]
"""
kwargs = _get_kwargs(
body=body,
)
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
*,
client: AuthenticatedClient,
body: UserInquiry,
) -> Optional[UserInquiry]:
"""
Args:
body (UserInquiry):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
UserInquiry
"""
return sync_detailed(
client=client,
body=body,
).parsed
async def asyncio_detailed(
*,
client: AuthenticatedClient,
body: UserInquiry,
) -> Response[UserInquiry]:
"""
Args:
body (UserInquiry):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[UserInquiry]
"""
kwargs = _get_kwargs(
body=body,
)
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
*,
client: AuthenticatedClient,
body: UserInquiry,
) -> Optional[UserInquiry]:
"""
Args:
body (UserInquiry):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
UserInquiry
"""
return (
await asyncio_detailed(
client=client,
body=body,
)
).parsed

View file

@ -0,0 +1,143 @@
from http import HTTPStatus
from typing import Any, Optional, Union
from uuid import UUID
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.account import Account
from ...types import Response
def _get_kwargs(
uuid: UUID,
) -> dict[str, Any]:
_kwargs: dict[str, Any] = {
"method": "get",
"url": f"/users/api/users/{uuid}/details",
}
return _kwargs
def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Account]:
if response.status_code == 200:
response_200 = Account.from_dict(response.json())
return response_200
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Account]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
uuid: UUID,
*,
client: AuthenticatedClient,
) -> Response[Account]:
"""
Args:
uuid (UUID):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Account]
"""
kwargs = _get_kwargs(
uuid=uuid,
)
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
uuid: UUID,
*,
client: AuthenticatedClient,
) -> Optional[Account]:
"""
Args:
uuid (UUID):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Account
"""
return sync_detailed(
uuid=uuid,
client=client,
).parsed
async def asyncio_detailed(
uuid: UUID,
*,
client: AuthenticatedClient,
) -> Response[Account]:
"""
Args:
uuid (UUID):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Account]
"""
kwargs = _get_kwargs(
uuid=uuid,
)
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
uuid: UUID,
*,
client: AuthenticatedClient,
) -> Optional[Account]:
"""
Args:
uuid (UUID):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Account
"""
return (
await asyncio_detailed(
uuid=uuid,
client=client,
)
).parsed

View file

@ -2,7 +2,10 @@
from .account import Account from .account import Account
from .application import Application from .application import Application
from .blank_enum import BlankEnum
from .change_password import ChangePassword from .change_password import ChangePassword
from .education_degree_enum import EducationDegreeEnum
from .gender_enum import GenderEnum
from .paginated_application_list import PaginatedApplicationList from .paginated_application_list import PaginatedApplicationList
from .paginated_public_user_list import PaginatedPublicUserList from .paginated_public_user_list import PaginatedPublicUserList
from .patched_account import PatchedAccount from .patched_account import PatchedAccount
@ -10,11 +13,15 @@ from .patched_change_password import PatchedChangePassword
from .public_user import PublicUser from .public_user import PublicUser
from .request_otp import RequestOTP from .request_otp import RequestOTP
from .request_ott import RequestOTT from .request_ott import RequestOTT
from .user_inquiry import UserInquiry
__all__ = ( __all__ = (
"Account", "Account",
"Application", "Application",
"BlankEnum",
"ChangePassword", "ChangePassword",
"EducationDegreeEnum",
"GenderEnum",
"PaginatedApplicationList", "PaginatedApplicationList",
"PaginatedPublicUserList", "PaginatedPublicUserList",
"PatchedAccount", "PatchedAccount",
@ -22,4 +29,5 @@ __all__ = (
"PublicUser", "PublicUser",
"RequestOTP", "RequestOTP",
"RequestOTT", "RequestOTT",
"UserInquiry",
) )

View file

@ -1,9 +1,15 @@
import datetime
from collections.abc import Mapping
from typing import Any, TypeVar, Union, cast from typing import Any, TypeVar, Union, cast
from uuid import UUID from uuid import UUID
from attrs import define as _attrs_define from attrs import define as _attrs_define
from attrs import field as _attrs_field from attrs import field as _attrs_field
from dateutil.parser import isoparse
from ..models.blank_enum import BlankEnum
from ..models.education_degree_enum import EducationDegreeEnum
from ..models.gender_enum import GenderEnum
from ..types import UNSET, Unset from ..types import UNSET, Unset
T = TypeVar("T", bound="Account") T = TypeVar("T", bound="Account")
@ -24,6 +30,10 @@ class Account:
last_name (Union[Unset, str]): last_name (Union[Unset, str]):
name (Union[Unset, str]): name (Union[Unset, str]):
avatar (Union[None, Unset, str]): avatar (Union[None, Unset, str]):
gender (Union[BlankEnum, GenderEnum, None, Unset]):
education_degree (Union[BlankEnum, EducationDegreeEnum, None, Unset]):
education_field (Union[None, Unset, str]):
birthdate (Union[None, Unset, datetime.date]):
iban (Union[None, Unset, str]): iban (Union[None, Unset, str]):
province (Union[None, Unset, int]): province (Union[None, Unset, int]):
city (Union[None, Unset, int]): city (Union[None, Unset, int]):
@ -42,6 +52,10 @@ class Account:
last_name: Union[Unset, str] = UNSET last_name: Union[Unset, str] = UNSET
name: Union[Unset, str] = UNSET name: Union[Unset, str] = UNSET
avatar: Union[None, Unset, str] = UNSET avatar: Union[None, Unset, str] = UNSET
gender: Union[BlankEnum, GenderEnum, None, Unset] = UNSET
education_degree: Union[BlankEnum, EducationDegreeEnum, None, Unset] = UNSET
education_field: Union[None, Unset, str] = UNSET
birthdate: Union[None, Unset, datetime.date] = UNSET
iban: Union[None, Unset, str] = UNSET iban: Union[None, Unset, str] = UNSET
province: Union[None, Unset, int] = UNSET province: Union[None, Unset, int] = UNSET
city: Union[None, Unset, int] = UNSET city: Union[None, Unset, int] = UNSET
@ -81,6 +95,40 @@ class Account:
else: else:
avatar = self.avatar avatar = self.avatar
gender: Union[None, Unset, str]
if isinstance(self.gender, Unset):
gender = UNSET
elif isinstance(self.gender, GenderEnum):
gender = self.gender.value
elif isinstance(self.gender, BlankEnum):
gender = self.gender.value
else:
gender = self.gender
education_degree: Union[None, Unset, str]
if isinstance(self.education_degree, Unset):
education_degree = UNSET
elif isinstance(self.education_degree, EducationDegreeEnum):
education_degree = self.education_degree.value
elif isinstance(self.education_degree, BlankEnum):
education_degree = self.education_degree.value
else:
education_degree = self.education_degree
education_field: Union[None, Unset, str]
if isinstance(self.education_field, Unset):
education_field = UNSET
else:
education_field = self.education_field
birthdate: Union[None, Unset, str]
if isinstance(self.birthdate, Unset):
birthdate = UNSET
elif isinstance(self.birthdate, datetime.date):
birthdate = self.birthdate.isoformat()
else:
birthdate = self.birthdate
iban: Union[None, Unset, str] iban: Union[None, Unset, str]
if isinstance(self.iban, Unset): if isinstance(self.iban, Unset):
iban = UNSET iban = UNSET
@ -133,6 +181,14 @@ class Account:
field_dict["name"] = name field_dict["name"] = name
if avatar is not UNSET: if avatar is not UNSET:
field_dict["avatar"] = avatar field_dict["avatar"] = avatar
if gender is not UNSET:
field_dict["gender"] = gender
if education_degree is not UNSET:
field_dict["education_degree"] = education_degree
if education_field is not UNSET:
field_dict["education_field"] = education_field
if birthdate is not UNSET:
field_dict["birthdate"] = birthdate
if iban is not UNSET: if iban is not UNSET:
field_dict["iban"] = iban field_dict["iban"] = iban
if province is not UNSET: if province is not UNSET:
@ -147,8 +203,8 @@ class Account:
return field_dict return field_dict
@classmethod @classmethod
def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = src_dict.copy() d = dict(src_dict)
uuid = UUID(d.pop("uuid")) uuid = UUID(d.pop("uuid"))
email = d.pop("email") email = d.pop("email")
@ -190,6 +246,82 @@ class Account:
avatar = _parse_avatar(d.pop("avatar", UNSET)) avatar = _parse_avatar(d.pop("avatar", UNSET))
def _parse_gender(data: object) -> Union[BlankEnum, GenderEnum, None, Unset]:
if data is None:
return data
if isinstance(data, Unset):
return data
try:
if not isinstance(data, str):
raise TypeError()
gender_type_0 = GenderEnum(data)
return gender_type_0
except: # noqa: E722
pass
try:
if not isinstance(data, str):
raise TypeError()
gender_type_1 = BlankEnum(data)
return gender_type_1
except: # noqa: E722
pass
return cast(Union[BlankEnum, GenderEnum, None, Unset], data)
gender = _parse_gender(d.pop("gender", UNSET))
def _parse_education_degree(data: object) -> Union[BlankEnum, EducationDegreeEnum, None, Unset]:
if data is None:
return data
if isinstance(data, Unset):
return data
try:
if not isinstance(data, str):
raise TypeError()
education_degree_type_0 = EducationDegreeEnum(data)
return education_degree_type_0
except: # noqa: E722
pass
try:
if not isinstance(data, str):
raise TypeError()
education_degree_type_1 = BlankEnum(data)
return education_degree_type_1
except: # noqa: E722
pass
return cast(Union[BlankEnum, EducationDegreeEnum, None, Unset], data)
education_degree = _parse_education_degree(d.pop("education_degree", UNSET))
def _parse_education_field(data: object) -> Union[None, Unset, str]:
if data is None:
return data
if isinstance(data, Unset):
return data
return cast(Union[None, Unset, str], data)
education_field = _parse_education_field(d.pop("education_field", UNSET))
def _parse_birthdate(data: object) -> Union[None, Unset, datetime.date]:
if data is None:
return data
if isinstance(data, Unset):
return data
try:
if not isinstance(data, str):
raise TypeError()
birthdate_type_0 = isoparse(data).date()
return birthdate_type_0
except: # noqa: E722
pass
return cast(Union[None, Unset, datetime.date], data)
birthdate = _parse_birthdate(d.pop("birthdate", UNSET))
def _parse_iban(data: object) -> Union[None, Unset, str]: def _parse_iban(data: object) -> Union[None, Unset, str]:
if data is None: if data is None:
return data return data
@ -247,6 +379,10 @@ class Account:
last_name=last_name, last_name=last_name,
name=name, name=name,
avatar=avatar, avatar=avatar,
gender=gender,
education_degree=education_degree,
education_field=education_field,
birthdate=birthdate,
iban=iban, iban=iban,
province=province, province=province,
city=city, city=city,

View file

@ -1,4 +1,5 @@
from typing import Any, TypeVar from collections.abc import Mapping
from typing import Any, TypeVar, Union, cast
from uuid import UUID from uuid import UUID
from attrs import define as _attrs_define from attrs import define as _attrs_define
@ -13,6 +14,7 @@ class Application:
Attributes: Attributes:
uuid (UUID): uuid (UUID):
name (str): name (str):
avatar (Union[None, str]):
client_id (str): client_id (str):
user (UUID): user (UUID):
allowed_scope (str): allowed_scope (str):
@ -20,6 +22,7 @@ class Application:
uuid: UUID uuid: UUID
name: str name: str
avatar: Union[None, str]
client_id: str client_id: str
user: UUID user: UUID
allowed_scope: str allowed_scope: str
@ -30,6 +33,9 @@ class Application:
name = self.name name = self.name
avatar: Union[None, str]
avatar = self.avatar
client_id = self.client_id client_id = self.client_id
user = str(self.user) user = str(self.user)
@ -42,6 +48,7 @@ class Application:
{ {
"uuid": uuid, "uuid": uuid,
"name": name, "name": name,
"avatar": avatar,
"client_id": client_id, "client_id": client_id,
"user": user, "user": user,
"allowed_scope": allowed_scope, "allowed_scope": allowed_scope,
@ -51,12 +58,19 @@ class Application:
return field_dict return field_dict
@classmethod @classmethod
def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = src_dict.copy() d = dict(src_dict)
uuid = UUID(d.pop("uuid")) uuid = UUID(d.pop("uuid"))
name = d.pop("name") name = d.pop("name")
def _parse_avatar(data: object) -> Union[None, str]:
if data is None:
return data
return cast(Union[None, str], data)
avatar = _parse_avatar(d.pop("avatar"))
client_id = d.pop("client_id") client_id = d.pop("client_id")
user = UUID(d.pop("user")) user = UUID(d.pop("user"))
@ -66,6 +80,7 @@ class Application:
application = cls( application = cls(
uuid=uuid, uuid=uuid,
name=name, name=name,
avatar=avatar,
client_id=client_id, client_id=client_id,
user=user, user=user,
allowed_scope=allowed_scope, allowed_scope=allowed_scope,

View file

@ -0,0 +1,8 @@
from enum import Enum
class BlankEnum(str, Enum):
VALUE_0 = ""
def __str__(self) -> str:
return str(self.value)

View file

@ -1,3 +1,4 @@
from collections.abc import Mapping
from typing import Any, TypeVar, Union from typing import Any, TypeVar, Union
from attrs import define as _attrs_define from attrs import define as _attrs_define
@ -43,8 +44,8 @@ class ChangePassword:
return field_dict return field_dict
@classmethod @classmethod
def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = src_dict.copy() d = dict(src_dict)
old_password = d.pop("old_password") old_password = d.pop("old_password")
new_password = d.pop("new_password") new_password = d.pop("new_password")

View file

@ -0,0 +1,14 @@
from enum import Enum
class EducationDegreeEnum(str, Enum):
ASSOCIATE = "associate"
BACHELORS = "bachelors"
DOCTORATE = "doctorate"
HIGH = "high"
MASTERS = "masters"
MIDDLE = "middle"
PRIMARY = "primary"
def __str__(self) -> str:
return str(self.value)

View file

@ -0,0 +1,9 @@
from enum import Enum
class GenderEnum(str, Enum):
F = "f"
M = "m"
def __str__(self) -> str:
return str(self.value)

View file

@ -1,3 +1,4 @@
from collections.abc import Mapping
from typing import TYPE_CHECKING, Any, TypeVar, Union, cast from typing import TYPE_CHECKING, Any, TypeVar, Union, cast
from attrs import define as _attrs_define from attrs import define as _attrs_define
@ -64,10 +65,10 @@ class PaginatedApplicationList:
return field_dict return field_dict
@classmethod @classmethod
def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
from ..models.application import Application from ..models.application import Application
d = src_dict.copy() d = dict(src_dict)
count = d.pop("count") count = d.pop("count")
results = [] results = []

View file

@ -1,3 +1,4 @@
from collections.abc import Mapping
from typing import TYPE_CHECKING, Any, TypeVar, Union, cast from typing import TYPE_CHECKING, Any, TypeVar, Union, cast
from attrs import define as _attrs_define from attrs import define as _attrs_define
@ -64,10 +65,10 @@ class PaginatedPublicUserList:
return field_dict return field_dict
@classmethod @classmethod
def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
from ..models.public_user import PublicUser from ..models.public_user import PublicUser
d = src_dict.copy() d = dict(src_dict)
count = d.pop("count") count = d.pop("count")
results = [] results = []

View file

@ -1,9 +1,15 @@
import datetime
from collections.abc import Mapping
from typing import Any, TypeVar, Union, cast from typing import Any, TypeVar, Union, cast
from uuid import UUID from uuid import UUID
from attrs import define as _attrs_define from attrs import define as _attrs_define
from attrs import field as _attrs_field from attrs import field as _attrs_field
from dateutil.parser import isoparse
from ..models.blank_enum import BlankEnum
from ..models.education_degree_enum import EducationDegreeEnum
from ..models.gender_enum import GenderEnum
from ..types import UNSET, Unset from ..types import UNSET, Unset
T = TypeVar("T", bound="PatchedAccount") T = TypeVar("T", bound="PatchedAccount")
@ -21,6 +27,10 @@ class PatchedAccount:
last_name (Union[Unset, str]): last_name (Union[Unset, str]):
name (Union[Unset, str]): name (Union[Unset, str]):
avatar (Union[None, Unset, str]): avatar (Union[None, Unset, str]):
gender (Union[BlankEnum, GenderEnum, None, Unset]):
education_degree (Union[BlankEnum, EducationDegreeEnum, None, Unset]):
education_field (Union[None, Unset, str]):
birthdate (Union[None, Unset, datetime.date]):
iban (Union[None, Unset, str]): iban (Union[None, Unset, str]):
iban_verified (Union[None, Unset, bool]): iban_verified (Union[None, Unset, bool]):
province (Union[None, Unset, int]): province (Union[None, Unset, int]):
@ -39,6 +49,10 @@ class PatchedAccount:
last_name: Union[Unset, str] = UNSET last_name: Union[Unset, str] = UNSET
name: Union[Unset, str] = UNSET name: Union[Unset, str] = UNSET
avatar: Union[None, Unset, str] = UNSET avatar: Union[None, Unset, str] = UNSET
gender: Union[BlankEnum, GenderEnum, None, Unset] = UNSET
education_degree: Union[BlankEnum, EducationDegreeEnum, None, Unset] = UNSET
education_field: Union[None, Unset, str] = UNSET
birthdate: Union[None, Unset, datetime.date] = UNSET
iban: Union[None, Unset, str] = UNSET iban: Union[None, Unset, str] = UNSET
iban_verified: Union[None, Unset, bool] = UNSET iban_verified: Union[None, Unset, bool] = UNSET
province: Union[None, Unset, int] = UNSET province: Union[None, Unset, int] = UNSET
@ -76,6 +90,40 @@ class PatchedAccount:
else: else:
avatar = self.avatar avatar = self.avatar
gender: Union[None, Unset, str]
if isinstance(self.gender, Unset):
gender = UNSET
elif isinstance(self.gender, GenderEnum):
gender = self.gender.value
elif isinstance(self.gender, BlankEnum):
gender = self.gender.value
else:
gender = self.gender
education_degree: Union[None, Unset, str]
if isinstance(self.education_degree, Unset):
education_degree = UNSET
elif isinstance(self.education_degree, EducationDegreeEnum):
education_degree = self.education_degree.value
elif isinstance(self.education_degree, BlankEnum):
education_degree = self.education_degree.value
else:
education_degree = self.education_degree
education_field: Union[None, Unset, str]
if isinstance(self.education_field, Unset):
education_field = UNSET
else:
education_field = self.education_field
birthdate: Union[None, Unset, str]
if isinstance(self.birthdate, Unset):
birthdate = UNSET
elif isinstance(self.birthdate, datetime.date):
birthdate = self.birthdate.isoformat()
else:
birthdate = self.birthdate
iban: Union[None, Unset, str] iban: Union[None, Unset, str]
if isinstance(self.iban, Unset): if isinstance(self.iban, Unset):
iban = UNSET iban = UNSET
@ -135,6 +183,14 @@ class PatchedAccount:
field_dict["name"] = name field_dict["name"] = name
if avatar is not UNSET: if avatar is not UNSET:
field_dict["avatar"] = avatar field_dict["avatar"] = avatar
if gender is not UNSET:
field_dict["gender"] = gender
if education_degree is not UNSET:
field_dict["education_degree"] = education_degree
if education_field is not UNSET:
field_dict["education_field"] = education_field
if birthdate is not UNSET:
field_dict["birthdate"] = birthdate
if iban is not UNSET: if iban is not UNSET:
field_dict["iban"] = iban field_dict["iban"] = iban
if iban_verified is not UNSET: if iban_verified is not UNSET:
@ -155,8 +211,8 @@ class PatchedAccount:
return field_dict return field_dict
@classmethod @classmethod
def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = src_dict.copy() d = dict(src_dict)
_uuid = d.pop("uuid", UNSET) _uuid = d.pop("uuid", UNSET)
uuid: Union[Unset, UUID] uuid: Union[Unset, UUID]
if isinstance(_uuid, Unset): if isinstance(_uuid, Unset):
@ -192,6 +248,82 @@ class PatchedAccount:
avatar = _parse_avatar(d.pop("avatar", UNSET)) avatar = _parse_avatar(d.pop("avatar", UNSET))
def _parse_gender(data: object) -> Union[BlankEnum, GenderEnum, None, Unset]:
if data is None:
return data
if isinstance(data, Unset):
return data
try:
if not isinstance(data, str):
raise TypeError()
gender_type_0 = GenderEnum(data)
return gender_type_0
except: # noqa: E722
pass
try:
if not isinstance(data, str):
raise TypeError()
gender_type_1 = BlankEnum(data)
return gender_type_1
except: # noqa: E722
pass
return cast(Union[BlankEnum, GenderEnum, None, Unset], data)
gender = _parse_gender(d.pop("gender", UNSET))
def _parse_education_degree(data: object) -> Union[BlankEnum, EducationDegreeEnum, None, Unset]:
if data is None:
return data
if isinstance(data, Unset):
return data
try:
if not isinstance(data, str):
raise TypeError()
education_degree_type_0 = EducationDegreeEnum(data)
return education_degree_type_0
except: # noqa: E722
pass
try:
if not isinstance(data, str):
raise TypeError()
education_degree_type_1 = BlankEnum(data)
return education_degree_type_1
except: # noqa: E722
pass
return cast(Union[BlankEnum, EducationDegreeEnum, None, Unset], data)
education_degree = _parse_education_degree(d.pop("education_degree", UNSET))
def _parse_education_field(data: object) -> Union[None, Unset, str]:
if data is None:
return data
if isinstance(data, Unset):
return data
return cast(Union[None, Unset, str], data)
education_field = _parse_education_field(d.pop("education_field", UNSET))
def _parse_birthdate(data: object) -> Union[None, Unset, datetime.date]:
if data is None:
return data
if isinstance(data, Unset):
return data
try:
if not isinstance(data, str):
raise TypeError()
birthdate_type_0 = isoparse(data).date()
return birthdate_type_0
except: # noqa: E722
pass
return cast(Union[None, Unset, datetime.date], data)
birthdate = _parse_birthdate(d.pop("birthdate", UNSET))
def _parse_iban(data: object) -> Union[None, Unset, str]: def _parse_iban(data: object) -> Union[None, Unset, str]:
if data is None: if data is None:
return data return data
@ -259,6 +391,10 @@ class PatchedAccount:
last_name=last_name, last_name=last_name,
name=name, name=name,
avatar=avatar, avatar=avatar,
gender=gender,
education_degree=education_degree,
education_field=education_field,
birthdate=birthdate,
iban=iban, iban=iban,
iban_verified=iban_verified, iban_verified=iban_verified,
province=province, province=province,

View file

@ -1,3 +1,4 @@
from collections.abc import Mapping
from typing import Any, TypeVar, Union from typing import Any, TypeVar, Union
from attrs import define as _attrs_define from attrs import define as _attrs_define
@ -42,8 +43,8 @@ class PatchedChangePassword:
return field_dict return field_dict
@classmethod @classmethod
def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = src_dict.copy() d = dict(src_dict)
old_password = d.pop("old_password", UNSET) old_password = d.pop("old_password", UNSET)
old_password_field = d.pop("old_password_field", UNSET) old_password_field = d.pop("old_password_field", UNSET)

View file

@ -1,3 +1,4 @@
from collections.abc import Mapping
from typing import Any, TypeVar, Union, cast from typing import Any, TypeVar, Union, cast
from uuid import UUID from uuid import UUID
@ -58,8 +59,8 @@ class PublicUser:
return field_dict return field_dict
@classmethod @classmethod
def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = src_dict.copy() d = dict(src_dict)
def _parse_avatar(data: object) -> Union[None, str]: def _parse_avatar(data: object) -> Union[None, str]:
if data is None: if data is None:

View file

@ -1,4 +1,5 @@
import datetime import datetime
from collections.abc import Mapping
from typing import Any, TypeVar, Union, cast from typing import Any, TypeVar, Union, cast
from attrs import define as _attrs_define from attrs import define as _attrs_define
@ -46,8 +47,8 @@ class RequestOTP:
return field_dict return field_dict
@classmethod @classmethod
def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = src_dict.copy() d = dict(src_dict)
phone_number = d.pop("phone_number") phone_number = d.pop("phone_number")
def _parse_otp_expire(data: object) -> Union[None, datetime.datetime]: def _parse_otp_expire(data: object) -> Union[None, datetime.datetime]:

View file

@ -1,4 +1,5 @@
import datetime import datetime
from collections.abc import Mapping
from typing import Any, TypeVar, Union, cast from typing import Any, TypeVar, Union, cast
from attrs import define as _attrs_define from attrs import define as _attrs_define
@ -47,8 +48,8 @@ class RequestOTT:
return field_dict return field_dict
@classmethod @classmethod
def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = src_dict.copy() d = dict(src_dict)
application = d.pop("application") application = d.pop("application")
def _parse_ott(data: object) -> Union[None, str]: def _parse_ott(data: object) -> Union[None, str]:

View file

@ -0,0 +1,68 @@
from collections.abc import Mapping
from typing import Any, TypeVar
from uuid import UUID
from attrs import define as _attrs_define
from attrs import field as _attrs_field
T = TypeVar("T", bound="UserInquiry")
@_attrs_define
class UserInquiry:
"""
Attributes:
uuid (UUID):
phone_number (str):
"""
uuid: UUID
phone_number: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
uuid = str(self.uuid)
phone_number = self.phone_number
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"uuid": uuid,
"phone_number": phone_number,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
uuid = UUID(d.pop("uuid"))
phone_number = d.pop("phone_number")
user_inquiry = cls(
uuid=uuid,
phone_number=phone_number,
)
user_inquiry.additional_properties = d
return user_inquiry
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties

50
log/accounts.log Normal file
View file

@ -0,0 +1,50 @@
2026-08-01 15:27:52,276 [INFO] django.server log 98318 6145355776 "GET /admin/login/ HTTP/1.1" 200 4068
2026-08-01 15:28:16,343 [INFO] django.server log 98318 6145355776 "GET /admin/login/ HTTP/1.1" 200 4068
2026-08-01 15:28:16,620 [INFO] django.server log 98318 6145355776 "POST /admin/login/ HTTP/1.1" 302 0
2026-08-01 15:28:16,695 [INFO] django.server log 98318 6145355776 "GET /admin/wallet/transaction/ HTTP/1.1" 200 30332
2026-08-01 15:29:03,893 [INFO] django.server log 98318 6162182144 "GET /admin/login/ HTTP/1.1" 200 4068
2026-08-01 15:29:03,994 [INFO] django.server log 98318 6145355776 "GET /static/admin/css/login.css HTTP/1.1" 200 958
2026-08-01 15:29:03,995 [INFO] django.server log 98318 6179008512 "GET /static/admin/css/dark_mode.css HTTP/1.1" 200 2682
2026-08-01 15:29:03,995 [INFO] django.server log 98318 6195834880 "GET /static/admin/css/nav_sidebar.css HTTP/1.1" 200 2810
2026-08-01 15:29:03,997 [INFO] django.server log 98318 6229487616 "GET /static/admin/js/theme.js HTTP/1.1" 200 1943
2026-08-01 15:29:03,997 [INFO] django.server log 98318 6212661248 "GET /static/admin/css/responsive.css HTTP/1.1" 200 17905
2026-08-01 15:29:03,997 [INFO] django.server log 98318 6162182144 "GET /static/admin/css/base.css HTTP/1.1" 200 21544
2026-08-01 15:29:03,998 [INFO] django.server log 98318 6229487616 "GET /static/admin/js/nav_sidebar.js HTTP/1.1" 200 3063
2026-08-01 15:29:04,565 [WARNING] django.server log 98318 6229487616 "GET /favicon.ico HTTP/1.1" 404 3758
2026-08-01 15:32:02,114 [INFO] django.server log 98318 6145355776 "GET /admin/ HTTP/1.1" 302 0
2026-08-01 15:32:02,125 [INFO] django.server log 98318 6145355776 "GET /admin/login/?next=/admin/ HTTP/1.1" 200 4081
2026-08-01 15:32:02,263 [INFO] django.server log 98318 6162182144 "GET /static/admin/css/dark_mode.css HTTP/1.1" 200 2682
2026-08-01 15:32:02,266 [INFO] django.server log 98318 6179008512 "GET /static/admin/css/nav_sidebar.css HTTP/1.1" 200 2810
2026-08-01 15:32:02,266 [INFO] django.server log 98318 6145355776 "GET /static/admin/css/base.css HTTP/1.1" 200 21544
2026-08-01 15:32:02,267 [INFO] django.server log 98318 6212661248 "GET /static/admin/js/theme.js HTTP/1.1" 200 1943
2026-08-01 15:32:02,267 [INFO] django.server log 98318 6195834880 "GET /static/admin/css/login.css HTTP/1.1" 200 958
2026-08-01 15:32:02,268 [INFO] django.server log 98318 6229487616 "GET /static/admin/css/responsive.css HTTP/1.1" 200 17905
2026-08-01 15:32:02,276 [INFO] django.server log 98318 6229487616 "GET /static/admin/js/nav_sidebar.js HTTP/1.1" 200 3063
2026-08-01 15:32:02,403 [WARNING] django.server log 98318 6229487616 "GET /favicon.ico HTTP/1.1" 404 3758
2026-08-01 15:32:17,288 [INFO] django.server log 98318 6229487616 "POST /admin/login/?next=/admin/ HTTP/1.1" 302 0
2026-08-01 15:32:17,351 [INFO] django.server log 98318 6229487616 "GET /admin/ HTTP/1.1" 200 13562
2026-08-01 15:32:17,384 [INFO] django.server log 98318 6229487616 "GET /static/admin/css/dashboard.css HTTP/1.1" 200 441
2026-08-01 15:32:17,389 [INFO] django.server log 98318 6195834880 "GET /static/admin/img/icon-changelink.svg HTTP/1.1" 200 380
2026-08-01 15:32:17,389 [INFO] django.server log 98318 6229487616 "GET /static/admin/img/icon-addlink.svg HTTP/1.1" 200 331
2026-08-01 15:32:22,629 [INFO] django.server log 98318 6229487616 "GET /admin/wallet/transaction/ HTTP/1.1" 200 30332
2026-08-01 15:32:22,649 [INFO] django.server log 98318 6162182144 "GET /static/admin/js/jquery.init.js HTTP/1.1" 200 347
2026-08-01 15:32:22,649 [INFO] django.server log 98318 6229487616 "GET /static/admin/css/changelists.css HTTP/1.1" 200 6811
2026-08-01 15:32:22,649 [INFO] django.server log 98318 6179008512 "GET /static/admin/css/django_jalali.min.css HTTP/1.1" 200 8077
2026-08-01 15:32:22,650 [INFO] django.server log 98318 6212661248 "GET /static/admin/js/django_jalali.min.js HTTP/1.1" 200 19826
2026-08-01 15:32:22,657 [INFO] django.server log 98318 6212661248 "GET /static/admin/js/actions.js HTTP/1.1" 200 8067
2026-08-01 15:32:22,658 [INFO] django.server log 98318 6229487616 "GET /static/admin/css/widgets.css HTTP/1.1" 200 11800
2026-08-01 15:32:22,658 [INFO] django.server log 98318 6162182144 "GET /static/admin/js/core.js HTTP/1.1" 200 6208
2026-08-01 15:32:22,660 [INFO] django.server log 98318 6179008512 "GET /static/admin/js/admin/RelatedObjectLookups.js HTTP/1.1" 200 9042
2026-08-01 15:32:22,660 [INFO] django.server log 98318 6145355776 "GET /static/admin/js/vendor/jquery/jquery.js HTTP/1.1" 200 285312
2026-08-01 15:32:22,666 [INFO] django.server log 98318 6145355776 "GET /static/admin/js/urlify.js HTTP/1.1" 200 7887
2026-08-01 15:32:22,666 [INFO] django.server log 98318 6212661248 "GET /static/admin/js/prepopulate.js HTTP/1.1" 200 1531
2026-08-01 15:32:22,667 [INFO] django.server log 98318 6195834880 "GET /admin/jsi18n/ HTTP/1.1" 200 3342
2026-08-01 15:32:22,667 [INFO] django.server log 98318 6229487616 "GET /static/admin/img/search.svg HTTP/1.1" 200 458
2026-08-01 15:32:22,669 [INFO] django.server log 98318 6179008512 "GET /static/admin/js/vendor/xregexp/xregexp.js HTTP/1.1" 200 325171
2026-08-01 15:32:22,679 [INFO] django.server log 98318 6179008512 "GET /static/admin/js/filters.js HTTP/1.1" 200 978
2026-08-01 15:32:22,696 [INFO] django.server log 98318 6229487616 "GET /static/admin/img/icon-viewlink.svg HTTP/1.1" 200 581
2026-08-01 15:32:22,696 [INFO] django.server log 98318 6179008512 "GET /static/admin/img/tooltag-add.svg HTTP/1.1" 200 331
2026-08-01 15:32:22,698 [INFO] django.server log 98318 6179008512 "GET /static/admin/img/sorting-icons.svg HTTP/1.1" 200 1097
2026-08-01 15:32:22,719 [INFO] django.server log 98318 6179008512 "GET /static/admin/js/calendar.js HTTP/1.1" 200 9141
2026-08-01 15:32:22,720 [INFO] django.server log 98318 6229487616 "GET /static/admin/js/admin/DateTimeShortcuts.js HTTP/1.1" 200 19319
2026-08-01 15:32:22,736 [INFO] django.server log 98318 6229487616 "GET /static/admin/img/icon-clock.svg HTTP/1.1" 200 677

0
log/errors.log Normal file
View file

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
import os import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'wallet.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'main.settings')
from utils.broker import get_rpc_broker_consumer from utils.broker import get_rpc_broker_consumer

View file

View file

@ -0,0 +1,141 @@
import os
def get_settings(key):
from django.conf import settings
return getattr(settings, 'BASE_DIR')
BASE_DIR = get_settings('BASE_DIR')
LOG_DIR = BASE_DIR / 'log'
if not os.path.exists(LOG_DIR):
os.makedirs(LOG_DIR)
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{asctime} [{levelname}] {name} {module} {process:d} {thread:d} {message}',
'style': '{',
},
'standard': {
'format': '{asctime} [{levelname}] {name}: {message}',
'style': '{',
},
'simple': {
'format': '{levelname} {message}',
'style': '{',
},
},
'filters': {
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse',
},
},
'handlers': {
# هندلر برای نوشتن در فایل
'file': {
'level': 'INFO',
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': os.path.join(LOG_DIR, 'accounts.log'),
# 'maxBytes': 1024 * 1024 * 10, # 10 MB
'when': 'midnight',
'interval': 5,
'backupCount': 30,
'formatter': 'verbose',
'encoding': 'utf-8',
},
# هندلر برای خطاها در فایل جداگانه
'error_file': {
'level': 'ERROR',
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': os.path.join(LOG_DIR, 'errors.log'),
# 'maxBytes': 1024 * 1024 * 10,
'when': 'midnight',
'interval': 5,
'backupCount': 30,
'formatter': 'verbose',
'encoding': 'utf-8',
},
# هندلر برای کنسول (فقط در حالت DEBUG)
'console': {
'level': 'DEBUG',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'simple',
},
# هندلر برای کنسول همیشه فعال (حتی در production)
'console_always': {
'level': 'INFO',
'class': 'logging.StreamHandler',
'formatter': 'standard',
},
},
'loggers': {
# لاگر ریشه
'': { # empty string = root logger
'handlers': ['console', 'file', 'error_file'],
'level': 'INFO',
'propagate': True,
},
# لاگر اختصاصی برای Django
'django': {
'handlers': ['console', 'file'],
'level': 'INFO',
'propagate': False,
},
# لاگر اختصاصی برای درخواست‌ها
'django.request': {
'handlers': ['file', 'error_file', 'console'],
'level': 'ERROR',
'propagate': False,
},
# # لاگر اختصاصی برای برنامه خودتان
# 'root': {
# 'handlers': ['console', 'file', 'error_file'],
# 'level': 'DEBUG',
# 'propagate': False,
# },
},
}
# LOKI_BASE_PUBLIC_URL = config('LOKI_BASE_PUBLIC_URL', default=None, cast=str)
# {
# 'version': 1,
# 'disable_existing_loggers': False,
# 'formatters': {
# 'loki': {
# 'class': 'utils.logs.LokiFormatter', # required
# },
# },
#
# 'handlers': {
# 'loki': {
# 'level': 'DEBUG', # Log level. Required
# 'class': 'utils.logs.LokiHandler', # Required
# 'formatter': 'loki', # Loki formatter. Required
# 'timeout': 2, # Post request timeout, default is 0.5. Optional
# 'url': f'{LOKI_BASE_PUBLIC_URL}/loki/api/v1/push', # Loki url. Defaults to localhost. Optional.
# # 'auth': ("user", "password"), # Basic auth to authenticate with loki. Default is None (i.e. no auth). Optional
# 'tags': {"app": "accounts"}, # Tags / Labels to attach to the log. Optional, but strongly encoraged to use.
# 'mode': 'thread',
# # Push mode. Can be 'sync' or 'thread'. Sync is blocking, thread is non-blocking. Defaults to sync. Optional.
# },
# 'console': {
# 'level': 'DEBUG',
# 'class': 'logging.StreamHandler',
# # 'formatter': 'verbose',
# },
# },
# 'loggers': {
# '': {
# 'handlers': ['console', 'loki'],
# 'level': 'INFO',
# 'propagate': True,
# },
# },
# }

View file

@ -16,7 +16,6 @@ from decouple import config
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
@ -38,6 +37,7 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django.contrib.postgres',
# pip installed apps # pip installed apps
'drf_spectacular', 'drf_spectacular',
@ -47,7 +47,8 @@ INSTALLED_APPS = [
'crispy_forms', 'crispy_forms',
'crispy_bootstrap5', 'crispy_bootstrap5',
'django_filters', 'django_filters',
'jalali_date', 'rangefilter',
# 'jalali_date',
# local apps # local apps
'apps.core', 'apps.core',
@ -70,7 +71,6 @@ MIDDLEWARE = [
'utils.exceptions.ErrorMiddleware', 'utils.exceptions.ErrorMiddleware',
] ]
OAUTH2_PROVIDER_APPLICATION_MODEL = "gooyal_oauth2.Application" OAUTH2_PROVIDER_APPLICATION_MODEL = "gooyal_oauth2.Application"
OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = "gooyal_oauth2.AccessToken" OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = "gooyal_oauth2.AccessToken"
OAUTH2_PROVIDER_ID_TOKEN_MODEL = "gooyal_oauth2.IDToken" OAUTH2_PROVIDER_ID_TOKEN_MODEL = "gooyal_oauth2.IDToken"
@ -126,8 +126,7 @@ SPECTACULAR_SETTINGS = {
# https://spec.openapis.org/oas/v3.0.3#oauth-flows-object # https://spec.openapis.org/oas/v3.0.3#oauth-flows-object
} }
ROOT_URLCONF = 'main.urls'
ROOT_URLCONF = 'wallet.urls'
TEMPLATES = [ TEMPLATES = [
{ {
@ -150,7 +149,7 @@ AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend',
) )
WSGI_APPLICATION = 'wallet.wsgi.application' WSGI_APPLICATION = 'main.wsgi.application'
# Database # Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases # https://docs.djangoproject.com/en/5.0/ref/settings/#databases
@ -191,7 +190,6 @@ AUTH_PASSWORD_VALIDATORS = [
}, },
] ]
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/ # https://docs.djangoproject.com/en/5.0/topics/i18n/
@ -209,7 +207,6 @@ LOCALE_PATHS = [
BASE_DIR / 'locale', BASE_DIR / 'locale',
] ]
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/ # https://docs.djangoproject.com/en/5.0/howto/static-files/
@ -224,17 +221,13 @@ if DEBUG == True:
BASE_DIR / 'static', BASE_DIR / 'static',
) )
else: else:
STATIC_ROOT = BASE_DIR / 'static' STATIC_ROOT = BASE_DIR / 'static'
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
LOGIN_URL = '/users/login/request/' LOGIN_URL = '/users/login/request/'
LOGIN_REDIRECT_URL = '/' LOGIN_REDIRECT_URL = '/'
@ -243,7 +236,7 @@ CRISPY_TEMPLATE_PACK = "bootstrap5"
AUTH_USER_MODEL = 'users.User' AUTH_USER_MODEL = 'users.User'
CORS_ORIGIN_ALLOW_ALL = True CORS_ORIGIN_ALLOW_ALL = True
CSRF_TRUSTED_ORIGINS = ['http://*.gooyal.com', 'https://*.gooyal.com'] CSRF_TRUSTED_ORIGINS = ['http://*.gooyal.com', 'https://*.gooyal.com', 'https://*.gooyal.ir']
JALALI_DATE_DEFAULTS = { JALALI_DATE_DEFAULTS = {
'Strftime': { 'Strftime': {
@ -262,4 +255,8 @@ CACHES = {
} }
} }
ALLOWED_NEGATIVE_BALANCE_APPLICATIONS = config('ALLOWED_NEGATIVE_BALANCE_APPLICATIONS', cast=lambda v: [s.strip() for s in v.split(',')]) LOKI_BASE_PUBLIC_URL = config('LOKI_BASE_PUBLIC_URL', default=None, cast=str)
from main.other_settings.logging import LOGGING
ALLOWED_NEGATIVE_BALANCE_APPLICATIONS = config('ALLOWED_NEGATIVE_BALANCE_APPLICATIONS', cast=lambda v: [s.strip() for s in v.split(',')])

View file

@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'wallet.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'main.settings')
application = get_wsgi_application() application = get_wsgi_application()

View file

@ -5,7 +5,7 @@ import sys
def main(): def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'wallet.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'main.settings')
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError as exc: except ImportError as exc:

View file

@ -18,6 +18,9 @@ django-jalali-date
django-betterforms django-betterforms
django-crispy-forms django-crispy-forms
crispy_bootstrap5 crispy_bootstrap5
psycopg[binary] psycopg[binary,pool]
django-redis django-redis
httpx httpx
packaging==25.0
xlsxwriter
django-admin-rangefilter

View file

@ -1,37 +1,35 @@
# #
# This file is autogenerated by pip-compile with Python 3.11 # This file is autogenerated by pip-compile with Python 3.13
# by the following command: # by the following command:
# #
# pip-compile # pip-compile --cert=None --client-cert=None --index-url=None --pip-args=None
# #
amqp==5.3.1 amqp==5.3.1
# via kombu # via kombu
anyio==4.8.0 anyio==4.12.1
# via httpx # via httpx
asgiref==3.8.1 asgiref==3.11.1
# via # via
# django # django
# django-cors-headers # django-cors-headers
async-timeout==5.0.1 attrs==25.4.0
# via redis
attrs==25.2.0
# via # via
# jsonschema # jsonschema
# referencing # referencing
billiard==4.2.1 billiard==4.2.4
# via celery # via celery
celery==5.4.0 celery==5.6.2
# via -r requirements.in # via -r requirements.in
certifi==2025.1.31 certifi==2026.1.4
# via # via
# httpcore # httpcore
# httpx # httpx
# requests # requests
cffi==1.17.1 cffi==2.0.0
# via cryptography # via cryptography
charset-normalizer==3.4.1 charset-normalizer==3.4.4
# via requests # via requests
click==8.1.8 click==8.3.1
# via # via
# celery # celery
# click-didyoumean # click-didyoumean
@ -39,15 +37,15 @@ click==8.1.8
# click-repl # click-repl
click-didyoumean==0.3.1 click-didyoumean==0.3.1
# via celery # via celery
click-plugins==1.1.1 click-plugins==1.1.1.2
# via celery # via celery
click-repl==0.3.0 click-repl==0.3.0
# via celery # via celery
crispy-bootstrap5==2024.10 crispy-bootstrap5==2025.6
# via -r requirements.in # via -r requirements.in
cryptography==44.0.2 cryptography==46.0.4
# via jwcrypto # via jwcrypto
django==5.1.7 django==6.0.2
# via # via
# -r requirements.in # -r requirements.in
# crispy-bootstrap5 # crispy-bootstrap5
@ -62,39 +60,39 @@ django==5.1.7
# drf-spectacular # drf-spectacular
django-betterforms==2.0.0 django-betterforms==2.0.0
# via -r requirements.in # via -r requirements.in
django-cors-headers==4.7.0 django-cors-headers==4.9.0
# via -r requirements.in # via -r requirements.in
django-crispy-forms==2.3 django-crispy-forms==2.5
# via # via
# -r requirements.in # -r requirements.in
# crispy-bootstrap5 # crispy-bootstrap5
django-filter==25.1 django-filter==25.2
# via -r requirements.in # via -r requirements.in
django-jalali-date==1.1.3 django-jalali-date==2.0.0
# via -r requirements.in # via -r requirements.in
django-oauth-toolkit==3.0.1 django-oauth-toolkit==3.2.0
# via -r requirements.in # via -r requirements.in
django-redis==5.4.0 django-redis==6.0.0
# via -r requirements.in # via -r requirements.in
djangorestframework==3.15.2 djangorestframework==3.16.1
# via # via
# -r requirements.in # -r requirements.in
# drf-spectacular # drf-spectacular
drf-spectacular==0.28.0 drf-spectacular==0.29.0
# via -r requirements.in # via -r requirements.in
gevent==24.11.1 gevent==25.9.1
# via -r requirements.in # via -r requirements.in
greenlet==3.1.1 greenlet==3.3.1
# via gevent # via gevent
gunicorn==23.0.0 gunicorn==25.0.2
# via -r requirements.in # via -r requirements.in
h11==0.14.0 h11==0.16.0
# via httpcore # via httpcore
httpcore==1.0.7 httpcore==1.0.9
# via httpx # via httpx
httpx==0.28.1 httpx==0.28.1
# via -r requirements.in # via -r requirements.in
idna==3.10 idna==3.11
# via # via
# anyio # anyio
# httpx # httpx
@ -105,81 +103,82 @@ jalali-core==1.0.0
# via jdatetime # via jdatetime
jdatetime==5.2.0 jdatetime==5.2.0
# via django-jalali-date # via django-jalali-date
jsonschema==4.23.0 jsonschema==4.26.0
# via drf-spectacular # via drf-spectacular
jsonschema-specifications==2024.10.1 jsonschema-specifications==2025.9.1
# via jsonschema # via jsonschema
jwcrypto==1.5.6 jwcrypto==1.5.6
# via django-oauth-toolkit # via django-oauth-toolkit
kombu==5.4.2 kombu==5.6.2
# via celery # via celery
markdown==3.7 markdown==3.10.1
# via -r requirements.in # via -r requirements.in
oauthlib==3.2.2 oauthlib==3.3.1
# via django-oauth-toolkit # via django-oauth-toolkit
packaging==24.2 packaging==25.0
# via gunicorn # via
pillow==11.1.0 # -r requirements.in
# gunicorn
# kombu
pillow==12.1.0
# via -r requirements.in # via -r requirements.in
prompt-toolkit==3.0.50 prompt-toolkit==3.0.52
# via click-repl # via click-repl
psycopg[binary]==3.2.6 psycopg[binary,pool]==3.3.2
# via -r requirements.in # via -r requirements.in
psycopg-binary==3.2.6 psycopg-binary==3.3.2
# via psycopg # via psycopg
pycparser==2.22 psycopg-pool==3.3.0
# via psycopg
pycparser==3.0
# via cffi # via cffi
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
# via celery # via celery
python-decouple==3.8 python-decouple==3.8
# via -r requirements.in # via -r requirements.in
pyyaml==6.0.2 pyyaml==6.0.3
# via drf-spectacular # via drf-spectacular
redis==5.2.1 redis==7.1.0
# via # via
# -r requirements.in # -r requirements.in
# django-redis # django-redis
referencing==0.36.2 referencing==0.37.0
# via # via
# jsonschema # jsonschema
# jsonschema-specifications # jsonschema-specifications
requests==2.32.3 requests==2.32.5
# via django-oauth-toolkit # via django-oauth-toolkit
rpds-py==0.23.1 rpds-py==0.30.0
# via # via
# jsonschema # jsonschema
# referencing # referencing
six==1.17.0 six==1.17.0
# via python-dateutil # via python-dateutil
sniffio==1.3.1 sqlparse==0.5.5
# via anyio
sqlparse==0.5.3
# via django # via django
typing-extensions==4.12.2 typing-extensions==4.15.0
# via # via
# anyio
# jwcrypto # jwcrypto
# psycopg # psycopg-pool
# referencing tzdata==2025.3
tzdata==2025.1 # via kombu
# via tzlocal==5.3.1
# celery # via celery
# kombu uritemplate==4.2.0
uritemplate==4.1.1
# via drf-spectacular # via drf-spectacular
urllib3==2.3.0 urllib3==2.6.3
# via requests # via requests
vine==5.1.0 vine==5.1.0
# via # via
# amqp # amqp
# celery # celery
# kombu # kombu
wcwidth==0.2.13 wcwidth==0.6.0
# via prompt-toolkit # via prompt-toolkit
zope-event==5.0 zope-event==6.1
# via gevent # via gevent
zope-interface==7.2 zope-interface==8.2
# via gevent # via gevent
xlsxwriter==3.2.3
# The following packages are considered to be unsafe in a requirements file: # via -r requirements.in
# setuptools django-admin-rangefilter==0.13.5

2
run.sh
View file

@ -5,4 +5,4 @@ while ! nc -z $DB_HOST 5432 ; do
done done
#python3 manage.py collectstatic --noinput #python3 manage.py collectstatic --noinput
python3 manage.py migrate python3 manage.py migrate
gunicorn wallet.wsgi:application --bind 0.0.0.0:8000 -w 4 gunicorn main.wsgi:application --bind 0.0.0.0:8000 -w 4

2
static/admin/css/django_jalali.min.css vendored Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1031,7 +1031,7 @@ function createDisabledPseudo( disabled ) {
// Check for inherited disabledness on relevant non-disabled elements: // Check for inherited disabledness on relevant non-disabled elements:
// * listed form-associated elements in a disabled fieldset // * listed form-associated elements in a disabled fieldset
// https://html.spec.whatwg.org/multipage/forms.html#category-listed // https://html.spec.whatwg.org/multipage/forms.html#wallet-listed
// https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled // https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled
// * option elements in a disabled optgroup // * option elements in a disabled optgroup
// https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled // https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled

View file

@ -1,7 +1,7 @@
{% extends "base_charge.html" %} {% extends "base_charge.html" %}
{% load static %} {% load static %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% load jalali_tags %} {#{% load jalali_tags %}#}
{% block title %}افزایش موجودی{% endblock %} {% block title %}افزایش موجودی{% endblock %}
{% block page_title %}افزایش موجودی{% endblock %} {% block page_title %}افزایش موجودی{% endblock %}

View file

@ -1,7 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %} {% load static %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% load jalali_tags %} {#{% load jalali_tags %}#}
{% block title %}wallet: Home{% endblock %} {% block title %}wallet: Home{% endblock %}
{% block page_title %}wallet: Home{% endblock %} {% block page_title %}wallet: Home{% endblock %}

View file

@ -1,4 +1,4 @@
{% load jalali_tags %} {#{% load jalali_tags %}#}
<div> <div>
<div class="card card-body mb-3"> <div class="card card-body mb-3">
<div class="row"> <div class="row">

View file

@ -1,4 +1,4 @@
{% load jalali_tags %} {#{% load jalali_tags %}#}
<div> <div>
<p class="fw-bold mb-1">اطلاعات آقا</p> <p class="fw-bold mb-1">اطلاعات آقا</p>
<div class="card card-body mb-3"> <div class="card card-body mb-3">

View file

@ -1,7 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %} {% load static %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% load jalali_tags %} {#{% load jalali_tags %}#}
{% block title %}user account{% endblock %} {% block title %}user account{% endblock %}
{% block page_title %}user account{% endblock %} {% block page_title %}user account{% endblock %}

View file

@ -8,7 +8,8 @@ from gooyal_accounts_client import AuthenticatedClient
from gooyal_accounts_client.models import PublicUser, Application from gooyal_accounts_client.models import PublicUser, Application
from gooyal_accounts_client.api.users import users_api_users_retrieve from gooyal_accounts_client.api.users import users_api_users_retrieve
from gooyal_accounts_client.api.oauth2 import oauth2_apis_applications_retrieve from gooyal_accounts_client.api.oauth2 import oauth2_apis_applications_retrieve
from gooyal_accounts_client.types import Response from gooyal_accounts_client.models import Account
from gooyal_accounts_client.api.users import (users_api_users_details_retrieve)
def login_as_client_credentials(): def login_as_client_credentials():
@ -60,6 +61,13 @@ def get_user_details(uuid):
return data return data
def get_user_detailed_info(uuid):
client = get_client()
response: Account = users_api_users_details_retrieve.sync(uuid=uuid, client=client)
print(response)
return response
def get_application_details(uuid): def get_application_details(uuid):
client = get_client() client = get_client()
data: Application = oauth2_apis_applications_retrieve.sync(client=client, id=uuid) data: Application = oauth2_apis_applications_retrieve.sync(client=client, id=uuid)

View file

@ -27,7 +27,7 @@ class LokiHandler(logging.Handler):
try: try:
payload = self.formatter.format(record) payload = self.formatter.format(record)
_push_message( self._push_message(
self._url, json=payload, timeout=self._timeout, auth=self._auth self._url, json=payload, timeout=self._timeout, auth=self._auth
) )
except requests.exceptions.ReadTimeout: except requests.exceptions.ReadTimeout:
@ -41,7 +41,6 @@ class LokiHandler(logging.Handler):
self.formatter = fmt self.formatter = fmt
def _push_message(self, *args, **kwargs): def _push_message(self, *args, **kwargs):
print('hehe')
if self._mode == "sync": if self._mode == "sync":
return _push_message(*args, **kwargs) return _push_message(*args, **kwargs)