Compare commits
50 commits
FEATURE/dr
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| fba0cf3b64 | |||
| 2fae79eb04 | |||
| 11327db537 | |||
| 1d555cd031 | |||
| fb4c79c5c6 | |||
| 5b278bddd2 | |||
| 8328814dca | |||
| 5d897b0917 | |||
| 5eff037d10 | |||
|
|
d9b7dbe1b2 | ||
|
|
e6eafbd6dc | ||
| 2d8e0da6fa | |||
| 7b1e9376b4 | |||
| 615a568294 | |||
| a8b32a16f4 | |||
|
|
0a179bf350 | ||
| e27fe27bf0 | |||
| e3413e33ab | |||
| 7e378e5b54 | |||
|
|
e5f22d1659 | ||
| 8acf7bc6c1 | |||
| fd244950e1 | |||
|
|
cc5b0568a9 | ||
|
|
a5639b5426 | ||
|
|
2caae02a3c | ||
| 53cc3f67ee | |||
| fb00595d23 | |||
|
|
1d961efb69 | ||
|
|
15fe0d4531 | ||
|
|
e045ffd9b1 | ||
|
|
27db599e8c | ||
|
|
95df80f593 | ||
|
|
1e3a69ece1 | ||
|
|
ff3cbcf162 | ||
|
|
7e8e35c746 | ||
|
|
b8e0cd09fc | ||
|
|
c24b52e341 | ||
| 132a3afcbf | |||
|
|
4393e3878a | ||
|
|
bd3438528d | ||
|
|
dae2a2e518 | ||
|
|
9183b1aa78 | ||
|
|
a35bd6b202 | ||
|
|
587a0d106b | ||
|
|
a9935fe72c | ||
|
|
ce1b8ccd46 | ||
|
|
d76d23ae7d | ||
|
|
62fd479e60 | ||
| 29e89fe6cc | |||
|
|
b281bcd914 |
61 changed files with 1633 additions and 177 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -5,3 +5,4 @@ utils/delme.py
|
|||
*.pyc
|
||||
.env
|
||||
/venv/
|
||||
/.venv/
|
||||
|
|
|
|||
15
Dockerfile
15
Dockerfile
|
|
@ -1,9 +1,12 @@
|
|||
FROM debian:12
|
||||
FROM base:debian13
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
WORKDIR /app
|
||||
RUN date
|
||||
RUN apt update
|
||||
RUN apt install gnupg2 netcat-traditional libssl-dev libcrypto++-dev lsb-release python3-pip -y
|
||||
COPY requirements.txt /app/requirements.txt
|
||||
RUN pip3 install --break-system-packages -r requirements.txt
|
||||
#RUN date
|
||||
#RUN apt update
|
||||
#RUN apt install gnupg2 netcat-traditional libssl-dev libcrypto++-dev lsb-release python3-pip -y
|
||||
#RUN apt install binutils libproj-dev gdal-bin -y
|
||||
#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"]
|
||||
|
|
|
|||
8
Dockerfile.build
Normal file
8
Dockerfile.build
Normal 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
8
Dockerfile.run
Normal 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"]
|
||||
18
apps/core/migrations/0002_alter_charge_state.py
Normal file
18
apps/core/migrations/0002_alter_charge_state.py
Normal 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),
|
||||
),
|
||||
]
|
||||
|
|
@ -5,8 +5,8 @@ from django.contrib.admin.widgets import AdminDateWidget
|
|||
from django.forms.fields import DateField, SplitDateTimeField, TimeField, DateTimeField
|
||||
from django.utils.encoding import force_str
|
||||
from datetime import date as datetime_date, datetime as datetime_datetime
|
||||
from jdatetime import GregorianToJalali, datetime as jalali_datetime
|
||||
import jdatetime
|
||||
# from jdatetime import GregorianToJalali, datetime as jalali_datetime
|
||||
# import jdatetime
|
||||
|
||||
class JalaliDateField(DateField):
|
||||
def __init__(self, *, input_formats=None, **kwargs):
|
||||
|
|
|
|||
|
|
@ -1,15 +1,111 @@
|
|||
from django.contrib import admin
|
||||
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):
|
||||
list_display = ['uuid', 'amount', 'created_at', 'updated_at', 'payer', 'payer_type', 'payee', 'payee_type', 'state', 'application']
|
||||
search_fields = ['uuid', 'created_at', 'updated_at', 'payer', 'payee', 'application__uuid']
|
||||
list_filter = ['state']
|
||||
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_account__owner_uuid', 'payee_account__owner_uuid', 'application__uuid']
|
||||
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',]
|
||||
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(Account, AccountAdmin)
|
||||
|
|
|
|||
46
apps/wallet/admin_filters.py
Normal file
46
apps/wallet/admin_filters.py
Normal 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,
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
|
@ -10,6 +10,8 @@ class StateChoices(IntegerChoices):
|
|||
SUCCESS = 5 , _('success')
|
||||
FAILED = 6 , _('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):
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class UserTransactionFilter(django_filters.FilterSet):
|
|||
|
||||
|
||||
class AccountFilter(django_filters.FilterSet):
|
||||
wallet = WalletUUIDFilter(field_name='wallet', required=True, lookup_expr='in')
|
||||
wallets = WalletUUIDFilter(field_name='wallet', required=True, lookup_expr='in')
|
||||
|
||||
class Meta:
|
||||
model = Account
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
18
apps/wallet/migrations/0018_alter_transaction_state.py
Normal file
18
apps/wallet/migrations/0018_alter_transaction_state.py
Normal 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),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,11 +1,16 @@
|
|||
import logging
|
||||
from functools import cached_property
|
||||
|
||||
from django.contrib.postgres.indexes import GinIndex
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.db import models
|
||||
from django.db import transaction
|
||||
from django.db.models import F
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import jdatetime
|
||||
|
||||
from rest_framework.exceptions import APIException
|
||||
from django.conf import settings
|
||||
from oauth2_provider import settings as oauth2_settings
|
||||
|
|
@ -20,6 +25,7 @@ from ..gooyal_oauth2.models import Application
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CurrencyChoices(models.TextChoices):
|
||||
IRANIAN_RIAL = '166ab7ce33b34799950d45035a9997d6', _('IRANIAN RIAL') # uuid without dash
|
||||
|
||||
|
|
@ -27,10 +33,11 @@ class CurrencyChoices(models.TextChoices):
|
|||
class AccountQuerySet(models.QuerySet):
|
||||
pass
|
||||
|
||||
|
||||
class Account(BaseModel):
|
||||
owner_uuid = models.UUIDField(db_index=True, null=False)
|
||||
wallet = models.ForeignKey('Wallet', on_delete=models.PROTECT, null=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)
|
||||
|
||||
# TODO: unique together wallet and owner_uuid
|
||||
|
|
@ -46,10 +53,16 @@ class Account(BaseModel):
|
|||
|
||||
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 WalletQuerySet(models.QuerySet):
|
||||
pass
|
||||
|
||||
|
||||
class Wallet(BaseModel):
|
||||
application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, null=True)
|
||||
label = models.CharField(_('label'), max_length=255)
|
||||
|
|
@ -57,6 +70,9 @@ class Wallet(BaseModel):
|
|||
|
||||
objects = WalletQuerySet.as_manager()
|
||||
|
||||
def __str__(self):
|
||||
return self.label
|
||||
|
||||
|
||||
# TODO: move DRF functionalities to view or serializers
|
||||
# class TransactionManager(models.Manager):
|
||||
|
|
@ -92,6 +108,39 @@ class TransactionQuerySet(models.QuerySet):
|
|||
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):
|
||||
amount = models.PositiveBigIntegerField(_('amount'), blank=False, validators=[MinValueValidator(1)])
|
||||
|
|
@ -107,6 +156,8 @@ class Transaction(BaseModel):
|
|||
db_index=True)
|
||||
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',
|
||||
# 'reference_id', 'application_details_url'}
|
||||
|
||||
|
|
@ -114,6 +165,13 @@ class Transaction(BaseModel):
|
|||
|
||||
class Meta:
|
||||
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):
|
||||
try:
|
||||
|
|
@ -123,6 +181,27 @@ class Transaction(BaseModel):
|
|||
|
||||
# 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):
|
||||
return self.payer_account.get_owner()
|
||||
|
||||
|
|
@ -153,18 +232,26 @@ class Transaction(BaseModel):
|
|||
def payee_type(self):
|
||||
return self.payee_account.owner_type
|
||||
|
||||
def change_state(self, from_state, to_state, error_message=None, **data):
|
||||
if self.state == from_state:
|
||||
updated = Transaction.objects.filter(pk=self.pk, state=from_state).update(
|
||||
def change_state(self, from_states: list | tuple, to_state, same_ok=False, raise_exception=True,
|
||||
error_message=None,
|
||||
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,
|
||||
**data
|
||||
)
|
||||
if updated: # double check in db level
|
||||
self.state = to_state
|
||||
else:
|
||||
raise APIException(error_message)
|
||||
else:
|
||||
raise APIException(error_message)
|
||||
return self
|
||||
|
||||
if raise_exception:
|
||||
raise Exception(error_message)
|
||||
|
||||
def withdraw_from_payer_balance(self):
|
||||
logger.debug('start withdraw_from_payer_balance')
|
||||
|
|
@ -173,8 +260,23 @@ class Transaction(BaseModel):
|
|||
if self.amount > self.payer_account.balance and not payer_is_allowed_negative_balance:
|
||||
logger.error('not enough balance')
|
||||
raise UnprocessableEntity('not enough balance')
|
||||
|
||||
if not payer_is_allowed_negative_balance:
|
||||
updated = Account.objects.filter(pk=self.payer_account.pk, balance__gte=self.amount).update(balance=F('balance') - self.amount)
|
||||
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}')
|
||||
self.payer_account.balance = F('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):
|
||||
|
|
@ -183,6 +285,12 @@ class Transaction(BaseModel):
|
|||
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):
|
||||
if not self.payer_id and not (payer_id and payer_type):
|
||||
|
|
@ -206,20 +314,24 @@ class Transaction(BaseModel):
|
|||
with transaction.atomic():
|
||||
if self.delay:
|
||||
self.change_state(
|
||||
StateChoices.CREATED,
|
||||
[StateChoices.CREATED],
|
||||
StateChoices.DELAYED,
|
||||
error_message='this transaction is not ready to pay',
|
||||
payer_id=payer_id,
|
||||
payer_type=payer_type
|
||||
data={
|
||||
"payer_id": payer_id,
|
||||
"payer_type": payer_type
|
||||
}
|
||||
)
|
||||
|
||||
else:
|
||||
self.change_state(
|
||||
StateChoices.CREATED,
|
||||
[StateChoices.CREATED],
|
||||
StateChoices.SUCCESS,
|
||||
error_message='this transaction is not ready to pay',
|
||||
payer_id=payer_id,
|
||||
payer_type=payer_type
|
||||
data={
|
||||
"payer_id": payer_id,
|
||||
"payer_type": payer_type
|
||||
}
|
||||
)
|
||||
self.deposit_to_payee_balance()
|
||||
|
||||
|
|
@ -233,7 +345,7 @@ class Transaction(BaseModel):
|
|||
|
||||
with transaction.atomic():
|
||||
self.change_state(
|
||||
StateChoices.DELAYED,
|
||||
[StateChoices.DELAYED],
|
||||
StateChoices.SUCCESS,
|
||||
error_message='this transaction is not delayed'
|
||||
)
|
||||
|
|
@ -248,18 +360,18 @@ class Transaction(BaseModel):
|
|||
raise APIException('payee identification error')
|
||||
|
||||
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)
|
||||
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:
|
||||
logger.debug( f'this transaction is not ready to pay(UUID:{self.uuid}, state:{self.state})')
|
||||
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')
|
||||
|
||||
with transaction.atomic():
|
||||
self.withdraw_from_payer_balance()
|
||||
self.change_state(
|
||||
StateChoices.CREATED,
|
||||
[StateChoices.CREATED],
|
||||
StateChoices.PENDING,
|
||||
error_message=_('this transaction is not ready to pay')
|
||||
)
|
||||
|
|
@ -272,12 +384,46 @@ class Transaction(BaseModel):
|
|||
with transaction.atomic():
|
||||
self.deposit_to_payee_balance()
|
||||
self.change_state(
|
||||
StateChoices.PENDING,
|
||||
[StateChoices.PENDING],
|
||||
StateChoices.SUCCESS,
|
||||
error_message=_('this transaction is not pending')
|
||||
)
|
||||
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):
|
||||
if self.payer_account.owner_uuid == entity.uuid:
|
||||
return 1
|
||||
|
|
@ -293,21 +439,21 @@ class Transaction(BaseModel):
|
|||
# detail = {'payer_name', 'payee_name', 'application_name', 'payer_avatar_url', 'payee_avatar_url', 'application_avatar_url'}
|
||||
is_updated = False
|
||||
if not self.details.get('payer_name'):
|
||||
print('update payer')
|
||||
logger.info('update payer')
|
||||
if self.payer_account.owner_type == TypeChoices.USER:
|
||||
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_avatar_url'] = payer_details.avatar
|
||||
else:
|
||||
payer_details = get_application_details(self.payer_id)
|
||||
payer_details = get_application_details(self.payer)
|
||||
self.details['payer_name'] = payer_details.name
|
||||
# TODO: get avatar from details
|
||||
is_updated = True
|
||||
print("payer_details:", payer_details)
|
||||
print("is_updated:", is_updated)
|
||||
logger.debug("payer_details:", payer_details)
|
||||
logger.info("is_updated:", is_updated)
|
||||
|
||||
if not self.details.get('payee_name'):
|
||||
print('update payee')
|
||||
logger.info('update payee')
|
||||
if self.payee_account.owner_type == TypeChoices.USER:
|
||||
payee_details: PublicUser = get_user_details(self.payee_account.owner_uuid)
|
||||
self.details['payee_name'] = payee_details.first_name + payee_details.last_name
|
||||
|
|
@ -337,3 +483,29 @@ class Transaction(BaseModel):
|
|||
if is_updated:
|
||||
print('update details')
|
||||
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
|
||||
|
|
|
|||
|
|
@ -223,3 +223,20 @@ class ApplicationWithdrawSerializer(serializers.ModelSerializer):
|
|||
)
|
||||
|
||||
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']
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ class ApplicationApiFlowsTests(APITestCase):
|
|||
'wallet.deposit:submit',
|
||||
'wallet.wallet:get_balance',
|
||||
'wallet.application:get_user_balance',
|
||||
'wallet.user:transaction_list',
|
||||
|
||||
'wallet.application.withdraw:submit',
|
||||
'wallet.application.withdraw:verify',
|
||||
|
|
@ -93,9 +94,10 @@ class ApplicationApiFlowsTests(APITestCase):
|
|||
return "Bearer {0}".format(token)
|
||||
|
||||
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)
|
||||
wallets = [self.wallet_rial_uuid.hex, self.wallet_reward_uuid.hex]
|
||||
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)
|
||||
|
|
@ -239,6 +241,7 @@ class ApplicationApiFlowsTests(APITestCase):
|
|||
}
|
||||
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)
|
||||
|
||||
def test_deposit_flow_zero_amount(self):
|
||||
|
|
@ -303,6 +306,7 @@ class ApplicationApiFlowsTests(APITestCase):
|
|||
'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)
|
||||
print(response.json())
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
response = self.client.post(reverse('wallet:application_deposit_submit_api', kwargs=kwargs_payer_wallet), data=data, HTTP_AUTHORIZATION=auth)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
from django.urls import path
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from .views import application as application_views
|
||||
from .views import user as user_views
|
||||
from .views.user import UserTransactionViewSet
|
||||
|
||||
app_name = "wallet"
|
||||
router = DefaultRouter()
|
||||
router.register('api/transactions', UserTransactionViewSet, basename='transactions')
|
||||
|
||||
urlpatterns = router.urls
|
||||
|
||||
user_urlpatterns = [
|
||||
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('api/wallets/', user_views.WalletAccountListAPIView.as_view(), name='user_wallet_account_api'),
|
||||
path('api/transactions/', user_views.TransactionListAPIView.as_view(), name='user_transaction_list_api'),
|
||||
|
||||
# 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'),
|
||||
|
|
@ -24,13 +29,15 @@ application_urlpatterns = [
|
|||
|
||||
path('api/application/<payer_wallet>/deposit/', application_views.DepositSubmitAPIView.as_view(), name='application_deposit_submit_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/<payee_wallet>/withdraw/', application_views.WithdrawSubmitAPIView.as_view(), name='application_withdraw_submit_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/wallets/<wallet>/', application_views.WalletTransactionListAPIView.as_view(), name='application_transaction_list_api'),
|
||||
# path('api/application/wallets/<wallet>/<uuid>/', application_views.WalletTransactionDetailAPIView.as_view(), name='application_transaction_detail'),
|
||||
|
||||
]
|
||||
|
||||
urlpatterns = user_urlpatterns + application_urlpatterns
|
||||
urlpatterns += user_urlpatterns + application_urlpatterns
|
||||
|
|
@ -259,7 +259,33 @@ class DepositVerifyAPIView(generics.RetrieveAPIView):
|
|||
instance = self.get_object()
|
||||
if instance.state == StateChoices.PENDING:
|
||||
instance.verify()
|
||||
logger.warning(f"Transaction was retrieved for {instance.uuid}: {instance.state}")
|
||||
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)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
|
@ -359,3 +385,25 @@ class WithdrawVerifyAPIView(generics.RetrieveAPIView):
|
|||
|
||||
serializer = self.get_serializer(instance)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class WithdrawRollbackAPIView(generics.RetrieveAPIView):
|
||||
permission_classes = [TokenHasScope]
|
||||
serializer_class = ApplicationWithdrawSerializer
|
||||
lookup_field = 'uuid'
|
||||
required_scopes = ['wallet.application.withdraw:rollback']
|
||||
|
||||
def get_queryset(self):
|
||||
application = get_application(self.request)
|
||||
payee_wallet_uuid = self.kwargs.get('payee_wallet')
|
||||
wallet = get_object_or_404(Wallet.objects.all(), uuid=payee_wallet_uuid)
|
||||
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):
|
||||
instance = self.get_object()
|
||||
|
||||
instance.rollback()
|
||||
logger.warning(f"Transaction was rolled back for {instance.uuid}: {instance.state}")
|
||||
serializer = self.get_serializer(instance)
|
||||
return Response(serializer.data)
|
||||
|
|
|
|||
|
|
@ -1,21 +1,29 @@
|
|||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.generic import DetailView, ListView, UpdateView
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from drf_spectacular.utils import extend_schema
|
||||
|
||||
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.permissions import AllowAny
|
||||
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.wallet.models import Transaction, Wallet, Account
|
||||
from apps.wallet.serializers import TransactionSerializer, DepositSerializer, WithdrawSerializer, \
|
||||
AccountSerializer
|
||||
AccountSerializer, TransactionGroupedSerializer
|
||||
from ..constans import StateChoices, TypeChoices
|
||||
from ..filters import AccountFilter, UserTransactionFilter
|
||||
from ..forms import TransactionActionForm
|
||||
|
|
@ -66,7 +74,6 @@ class TransactionList(generics.ListAPIView):
|
|||
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()
|
||||
|
||||
|
||||
# TODO:
|
||||
# def perform_create(self, serializer):
|
||||
# payer = self.request.user
|
||||
|
|
@ -166,6 +173,7 @@ class WithdrawSubmit(generics.CreateAPIView):
|
|||
payee = self.request.auth.application.user
|
||||
serializer.save(payee=payee, application=get_application(self.request))
|
||||
|
||||
|
||||
# TODO: refactor
|
||||
class WithdrawVerify(generics.RetrieveAPIView):
|
||||
permission_classes = [TokenHasScope]
|
||||
|
|
@ -226,18 +234,24 @@ class TransactionDetailView(UpdateView):
|
|||
return super().form_valid(form)
|
||||
|
||||
|
||||
|
||||
class WalletAccountListAPIView(generics.ListAPIView):
|
||||
'''
|
||||
API for getting authenticated user balance.
|
||||
'''
|
||||
permission_classes = [IsAuthenticatedOrTokenHasScope]
|
||||
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
|
||||
serializer_class = AccountSerializer
|
||||
filter_backends = (DjangoFilterBackend,)
|
||||
filterset_class = AccountFilter
|
||||
required_scopes = ['wallet.wallet:get_balance']
|
||||
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):
|
||||
user = self.request.user
|
||||
wallet_uuid = self.kwargs.get('wallet')
|
||||
|
|
@ -298,3 +312,61 @@ class WalletTransactionListView(generics.ListAPIView):
|
|||
account, created = Account.objects.get_or_create(owner_uuid=user.uuid, owner_type=TypeChoices.USER, wallet=wallet)
|
||||
return Transaction.objects.filter(Q(payee_account=account) | Q(payer_account=account)).all()
|
||||
|
||||
|
||||
class UserTransactionViewSet(mixins.ListModelMixin,
|
||||
# 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)
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
"""Contains endpoint functions for accessing the API"""
|
||||
|
|
@ -0,0 +1 @@
|
|||
"""Contains endpoint functions for accessing the API"""
|
||||
151
gooyal_accounts_client/api/users/users_api_inquiry_create.py
Normal file
151
gooyal_accounts_client/api/users/users_api_inquiry_create.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -2,7 +2,10 @@
|
|||
|
||||
from .account import Account
|
||||
from .application import Application
|
||||
from .blank_enum import BlankEnum
|
||||
from .change_password import ChangePassword
|
||||
from .education_degree_enum import EducationDegreeEnum
|
||||
from .gender_enum import GenderEnum
|
||||
from .paginated_application_list import PaginatedApplicationList
|
||||
from .paginated_public_user_list import PaginatedPublicUserList
|
||||
from .patched_account import PatchedAccount
|
||||
|
|
@ -10,11 +13,15 @@ from .patched_change_password import PatchedChangePassword
|
|||
from .public_user import PublicUser
|
||||
from .request_otp import RequestOTP
|
||||
from .request_ott import RequestOTT
|
||||
from .user_inquiry import UserInquiry
|
||||
|
||||
__all__ = (
|
||||
"Account",
|
||||
"Application",
|
||||
"BlankEnum",
|
||||
"ChangePassword",
|
||||
"EducationDegreeEnum",
|
||||
"GenderEnum",
|
||||
"PaginatedApplicationList",
|
||||
"PaginatedPublicUserList",
|
||||
"PatchedAccount",
|
||||
|
|
@ -22,4 +29,5 @@ __all__ = (
|
|||
"PublicUser",
|
||||
"RequestOTP",
|
||||
"RequestOTT",
|
||||
"UserInquiry",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
import datetime
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, TypeVar, Union, cast
|
||||
from uuid import UUID
|
||||
|
||||
from attrs import define as _attrs_define
|
||||
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
|
||||
|
||||
T = TypeVar("T", bound="Account")
|
||||
|
|
@ -24,6 +30,10 @@ class Account:
|
|||
last_name (Union[Unset, str]):
|
||||
name (Union[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]):
|
||||
province (Union[None, Unset, int]):
|
||||
city (Union[None, Unset, int]):
|
||||
|
|
@ -42,6 +52,10 @@ class Account:
|
|||
last_name: Union[Unset, str] = UNSET
|
||||
name: Union[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
|
||||
province: Union[None, Unset, int] = UNSET
|
||||
city: Union[None, Unset, int] = UNSET
|
||||
|
|
@ -81,6 +95,40 @@ class Account:
|
|||
else:
|
||||
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]
|
||||
if isinstance(self.iban, Unset):
|
||||
iban = UNSET
|
||||
|
|
@ -133,6 +181,14 @@ class Account:
|
|||
field_dict["name"] = name
|
||||
if avatar is not UNSET:
|
||||
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:
|
||||
field_dict["iban"] = iban
|
||||
if province is not UNSET:
|
||||
|
|
@ -147,8 +203,8 @@ class Account:
|
|||
return field_dict
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T:
|
||||
d = src_dict.copy()
|
||||
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
|
||||
d = dict(src_dict)
|
||||
uuid = UUID(d.pop("uuid"))
|
||||
|
||||
email = d.pop("email")
|
||||
|
|
@ -190,6 +246,82 @@ class Account:
|
|||
|
||||
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]:
|
||||
if data is None:
|
||||
return data
|
||||
|
|
@ -247,6 +379,10 @@ class Account:
|
|||
last_name=last_name,
|
||||
name=name,
|
||||
avatar=avatar,
|
||||
gender=gender,
|
||||
education_degree=education_degree,
|
||||
education_field=education_field,
|
||||
birthdate=birthdate,
|
||||
iban=iban,
|
||||
province=province,
|
||||
city=city,
|
||||
|
|
|
|||
|
|
@ -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 attrs import define as _attrs_define
|
||||
|
|
@ -13,6 +14,7 @@ class Application:
|
|||
Attributes:
|
||||
uuid (UUID):
|
||||
name (str):
|
||||
avatar (Union[None, str]):
|
||||
client_id (str):
|
||||
user (UUID):
|
||||
allowed_scope (str):
|
||||
|
|
@ -20,6 +22,7 @@ class Application:
|
|||
|
||||
uuid: UUID
|
||||
name: str
|
||||
avatar: Union[None, str]
|
||||
client_id: str
|
||||
user: UUID
|
||||
allowed_scope: str
|
||||
|
|
@ -30,6 +33,9 @@ class Application:
|
|||
|
||||
name = self.name
|
||||
|
||||
avatar: Union[None, str]
|
||||
avatar = self.avatar
|
||||
|
||||
client_id = self.client_id
|
||||
|
||||
user = str(self.user)
|
||||
|
|
@ -42,6 +48,7 @@ class Application:
|
|||
{
|
||||
"uuid": uuid,
|
||||
"name": name,
|
||||
"avatar": avatar,
|
||||
"client_id": client_id,
|
||||
"user": user,
|
||||
"allowed_scope": allowed_scope,
|
||||
|
|
@ -51,12 +58,19 @@ class Application:
|
|||
return field_dict
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T:
|
||||
d = src_dict.copy()
|
||||
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
|
||||
d = dict(src_dict)
|
||||
uuid = UUID(d.pop("uuid"))
|
||||
|
||||
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")
|
||||
|
||||
user = UUID(d.pop("user"))
|
||||
|
|
@ -66,6 +80,7 @@ class Application:
|
|||
application = cls(
|
||||
uuid=uuid,
|
||||
name=name,
|
||||
avatar=avatar,
|
||||
client_id=client_id,
|
||||
user=user,
|
||||
allowed_scope=allowed_scope,
|
||||
|
|
|
|||
8
gooyal_accounts_client/models/blank_enum.py
Normal file
8
gooyal_accounts_client/models/blank_enum.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from enum import Enum
|
||||
|
||||
|
||||
class BlankEnum(str, Enum):
|
||||
VALUE_0 = ""
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.value)
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
from collections.abc import Mapping
|
||||
from typing import Any, TypeVar, Union
|
||||
|
||||
from attrs import define as _attrs_define
|
||||
|
|
@ -43,8 +44,8 @@ class ChangePassword:
|
|||
return field_dict
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T:
|
||||
d = src_dict.copy()
|
||||
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
|
||||
d = dict(src_dict)
|
||||
old_password = d.pop("old_password")
|
||||
|
||||
new_password = d.pop("new_password")
|
||||
|
|
|
|||
14
gooyal_accounts_client/models/education_degree_enum.py
Normal file
14
gooyal_accounts_client/models/education_degree_enum.py
Normal 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)
|
||||
9
gooyal_accounts_client/models/gender_enum.py
Normal file
9
gooyal_accounts_client/models/gender_enum.py
Normal 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)
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING, Any, TypeVar, Union, cast
|
||||
|
||||
from attrs import define as _attrs_define
|
||||
|
|
@ -64,10 +65,10 @@ class PaginatedApplicationList:
|
|||
return field_dict
|
||||
|
||||
@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
|
||||
|
||||
d = src_dict.copy()
|
||||
d = dict(src_dict)
|
||||
count = d.pop("count")
|
||||
|
||||
results = []
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING, Any, TypeVar, Union, cast
|
||||
|
||||
from attrs import define as _attrs_define
|
||||
|
|
@ -64,10 +65,10 @@ class PaginatedPublicUserList:
|
|||
return field_dict
|
||||
|
||||
@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
|
||||
|
||||
d = src_dict.copy()
|
||||
d = dict(src_dict)
|
||||
count = d.pop("count")
|
||||
|
||||
results = []
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
import datetime
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, TypeVar, Union, cast
|
||||
from uuid import UUID
|
||||
|
||||
from attrs import define as _attrs_define
|
||||
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
|
||||
|
||||
T = TypeVar("T", bound="PatchedAccount")
|
||||
|
|
@ -21,6 +27,10 @@ class PatchedAccount:
|
|||
last_name (Union[Unset, str]):
|
||||
name (Union[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_verified (Union[None, Unset, bool]):
|
||||
province (Union[None, Unset, int]):
|
||||
|
|
@ -39,6 +49,10 @@ class PatchedAccount:
|
|||
last_name: Union[Unset, str] = UNSET
|
||||
name: Union[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_verified: Union[None, Unset, bool] = UNSET
|
||||
province: Union[None, Unset, int] = UNSET
|
||||
|
|
@ -76,6 +90,40 @@ class PatchedAccount:
|
|||
else:
|
||||
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]
|
||||
if isinstance(self.iban, Unset):
|
||||
iban = UNSET
|
||||
|
|
@ -135,6 +183,14 @@ class PatchedAccount:
|
|||
field_dict["name"] = name
|
||||
if avatar is not UNSET:
|
||||
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:
|
||||
field_dict["iban"] = iban
|
||||
if iban_verified is not UNSET:
|
||||
|
|
@ -155,8 +211,8 @@ class PatchedAccount:
|
|||
return field_dict
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T:
|
||||
d = src_dict.copy()
|
||||
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
|
||||
d = dict(src_dict)
|
||||
_uuid = d.pop("uuid", UNSET)
|
||||
uuid: Union[Unset, UUID]
|
||||
if isinstance(_uuid, Unset):
|
||||
|
|
@ -192,6 +248,82 @@ class PatchedAccount:
|
|||
|
||||
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]:
|
||||
if data is None:
|
||||
return data
|
||||
|
|
@ -259,6 +391,10 @@ class PatchedAccount:
|
|||
last_name=last_name,
|
||||
name=name,
|
||||
avatar=avatar,
|
||||
gender=gender,
|
||||
education_degree=education_degree,
|
||||
education_field=education_field,
|
||||
birthdate=birthdate,
|
||||
iban=iban,
|
||||
iban_verified=iban_verified,
|
||||
province=province,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from collections.abc import Mapping
|
||||
from typing import Any, TypeVar, Union
|
||||
|
||||
from attrs import define as _attrs_define
|
||||
|
|
@ -42,8 +43,8 @@ class PatchedChangePassword:
|
|||
return field_dict
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T:
|
||||
d = src_dict.copy()
|
||||
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
|
||||
d = dict(src_dict)
|
||||
old_password = d.pop("old_password", UNSET)
|
||||
|
||||
old_password_field = d.pop("old_password_field", UNSET)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from collections.abc import Mapping
|
||||
from typing import Any, TypeVar, Union, cast
|
||||
from uuid import UUID
|
||||
|
||||
|
|
@ -58,8 +59,8 @@ class PublicUser:
|
|||
return field_dict
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T:
|
||||
d = src_dict.copy()
|
||||
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
|
||||
d = dict(src_dict)
|
||||
|
||||
def _parse_avatar(data: object) -> Union[None, str]:
|
||||
if data is None:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import datetime
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, TypeVar, Union, cast
|
||||
|
||||
from attrs import define as _attrs_define
|
||||
|
|
@ -46,8 +47,8 @@ class RequestOTP:
|
|||
return field_dict
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T:
|
||||
d = src_dict.copy()
|
||||
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
|
||||
d = dict(src_dict)
|
||||
phone_number = d.pop("phone_number")
|
||||
|
||||
def _parse_otp_expire(data: object) -> Union[None, datetime.datetime]:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import datetime
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, TypeVar, Union, cast
|
||||
|
||||
from attrs import define as _attrs_define
|
||||
|
|
@ -47,8 +48,8 @@ class RequestOTT:
|
|||
return field_dict
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T:
|
||||
d = src_dict.copy()
|
||||
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
|
||||
d = dict(src_dict)
|
||||
application = d.pop("application")
|
||||
|
||||
def _parse_ott(data: object) -> Union[None, str]:
|
||||
|
|
|
|||
68
gooyal_accounts_client/models/user_inquiry.py
Normal file
68
gooyal_accounts_client/models/user_inquiry.py
Normal 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
50
log/accounts.log
Normal 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
0
log/errors.log
Normal file
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python
|
||||
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
|
||||
|
||||
0
main/other_settings/__init__.py
Normal file
0
main/other_settings/__init__.py
Normal file
141
main/other_settings/logging.py
Normal file
141
main/other_settings/logging.py
Normal 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,
|
||||
# },
|
||||
# },
|
||||
# }
|
||||
|
|
@ -16,7 +16,6 @@ from decouple import config
|
|||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
||||
|
||||
|
|
@ -38,6 +37,7 @@ INSTALLED_APPS = [
|
|||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.postgres',
|
||||
|
||||
# pip installed apps
|
||||
'drf_spectacular',
|
||||
|
|
@ -47,7 +47,8 @@ INSTALLED_APPS = [
|
|||
'crispy_forms',
|
||||
'crispy_bootstrap5',
|
||||
'django_filters',
|
||||
'jalali_date',
|
||||
'rangefilter',
|
||||
# 'jalali_date',
|
||||
|
||||
# local apps
|
||||
'apps.core',
|
||||
|
|
@ -70,7 +71,6 @@ MIDDLEWARE = [
|
|||
'utils.exceptions.ErrorMiddleware',
|
||||
]
|
||||
|
||||
|
||||
OAUTH2_PROVIDER_APPLICATION_MODEL = "gooyal_oauth2.Application"
|
||||
OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = "gooyal_oauth2.AccessToken"
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
ROOT_URLCONF = 'wallet.urls'
|
||||
ROOT_URLCONF = 'main.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
|
|
@ -150,7 +149,7 @@ AUTHENTICATION_BACKENDS = (
|
|||
'django.contrib.auth.backends.ModelBackend',
|
||||
)
|
||||
|
||||
WSGI_APPLICATION = 'wallet.wsgi.application'
|
||||
WSGI_APPLICATION = 'main.wsgi.application'
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
|
||||
|
|
@ -191,7 +190,6 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/5.0/topics/i18n/
|
||||
|
||||
|
|
@ -209,7 +207,6 @@ LOCALE_PATHS = [
|
|||
BASE_DIR / 'locale',
|
||||
]
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/5.0/howto/static-files/
|
||||
|
||||
|
|
@ -226,15 +223,11 @@ if DEBUG == True:
|
|||
else:
|
||||
STATIC_ROOT = BASE_DIR / 'static'
|
||||
|
||||
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
|
||||
|
||||
LOGIN_URL = '/users/login/request/'
|
||||
LOGIN_REDIRECT_URL = '/'
|
||||
|
||||
|
|
@ -243,7 +236,7 @@ CRISPY_TEMPLATE_PACK = "bootstrap5"
|
|||
|
||||
AUTH_USER_MODEL = 'users.User'
|
||||
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 = {
|
||||
'Strftime': {
|
||||
|
|
@ -262,4 +255,8 @@ CACHES = {
|
|||
}
|
||||
}
|
||||
|
||||
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(',')])
|
||||
|
|
@ -11,6 +11,6 @@ import os
|
|||
|
||||
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()
|
||||
|
|
@ -5,7 +5,7 @@ import sys
|
|||
|
||||
|
||||
def main():
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'wallet.settings')
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'main.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ django-jalali-date
|
|||
django-betterforms
|
||||
django-crispy-forms
|
||||
crispy_bootstrap5
|
||||
psycopg[binary]
|
||||
psycopg[binary,pool]
|
||||
django-redis
|
||||
httpx
|
||||
packaging==25.0
|
||||
xlsxwriter
|
||||
django-admin-rangefilter
|
||||
133
requirements.txt
133
requirements.txt
|
|
@ -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:
|
||||
#
|
||||
# pip-compile
|
||||
# pip-compile --cert=None --client-cert=None --index-url=None --pip-args=None
|
||||
#
|
||||
amqp==5.3.1
|
||||
# via kombu
|
||||
anyio==4.8.0
|
||||
anyio==4.12.1
|
||||
# via httpx
|
||||
asgiref==3.8.1
|
||||
asgiref==3.11.1
|
||||
# via
|
||||
# django
|
||||
# django-cors-headers
|
||||
async-timeout==5.0.1
|
||||
# via redis
|
||||
attrs==25.2.0
|
||||
attrs==25.4.0
|
||||
# via
|
||||
# jsonschema
|
||||
# referencing
|
||||
billiard==4.2.1
|
||||
billiard==4.2.4
|
||||
# via celery
|
||||
celery==5.4.0
|
||||
celery==5.6.2
|
||||
# via -r requirements.in
|
||||
certifi==2025.1.31
|
||||
certifi==2026.1.4
|
||||
# via
|
||||
# httpcore
|
||||
# httpx
|
||||
# requests
|
||||
cffi==1.17.1
|
||||
cffi==2.0.0
|
||||
# via cryptography
|
||||
charset-normalizer==3.4.1
|
||||
charset-normalizer==3.4.4
|
||||
# via requests
|
||||
click==8.1.8
|
||||
click==8.3.1
|
||||
# via
|
||||
# celery
|
||||
# click-didyoumean
|
||||
|
|
@ -39,15 +37,15 @@ click==8.1.8
|
|||
# click-repl
|
||||
click-didyoumean==0.3.1
|
||||
# via celery
|
||||
click-plugins==1.1.1
|
||||
click-plugins==1.1.1.2
|
||||
# via celery
|
||||
click-repl==0.3.0
|
||||
# via celery
|
||||
crispy-bootstrap5==2024.10
|
||||
crispy-bootstrap5==2025.6
|
||||
# via -r requirements.in
|
||||
cryptography==44.0.2
|
||||
cryptography==46.0.4
|
||||
# via jwcrypto
|
||||
django==5.1.7
|
||||
django==6.0.2
|
||||
# via
|
||||
# -r requirements.in
|
||||
# crispy-bootstrap5
|
||||
|
|
@ -62,39 +60,39 @@ django==5.1.7
|
|||
# drf-spectacular
|
||||
django-betterforms==2.0.0
|
||||
# via -r requirements.in
|
||||
django-cors-headers==4.7.0
|
||||
django-cors-headers==4.9.0
|
||||
# via -r requirements.in
|
||||
django-crispy-forms==2.3
|
||||
django-crispy-forms==2.5
|
||||
# via
|
||||
# -r requirements.in
|
||||
# crispy-bootstrap5
|
||||
django-filter==25.1
|
||||
django-filter==25.2
|
||||
# via -r requirements.in
|
||||
django-jalali-date==1.1.3
|
||||
django-jalali-date==2.0.0
|
||||
# via -r requirements.in
|
||||
django-oauth-toolkit==3.0.1
|
||||
django-oauth-toolkit==3.2.0
|
||||
# via -r requirements.in
|
||||
django-redis==5.4.0
|
||||
django-redis==6.0.0
|
||||
# via -r requirements.in
|
||||
djangorestframework==3.15.2
|
||||
djangorestframework==3.16.1
|
||||
# via
|
||||
# -r requirements.in
|
||||
# drf-spectacular
|
||||
drf-spectacular==0.28.0
|
||||
drf-spectacular==0.29.0
|
||||
# via -r requirements.in
|
||||
gevent==24.11.1
|
||||
gevent==25.9.1
|
||||
# via -r requirements.in
|
||||
greenlet==3.1.1
|
||||
greenlet==3.3.1
|
||||
# via gevent
|
||||
gunicorn==23.0.0
|
||||
gunicorn==25.0.2
|
||||
# via -r requirements.in
|
||||
h11==0.14.0
|
||||
h11==0.16.0
|
||||
# via httpcore
|
||||
httpcore==1.0.7
|
||||
httpcore==1.0.9
|
||||
# via httpx
|
||||
httpx==0.28.1
|
||||
# via -r requirements.in
|
||||
idna==3.10
|
||||
idna==3.11
|
||||
# via
|
||||
# anyio
|
||||
# httpx
|
||||
|
|
@ -105,81 +103,82 @@ jalali-core==1.0.0
|
|||
# via jdatetime
|
||||
jdatetime==5.2.0
|
||||
# via django-jalali-date
|
||||
jsonschema==4.23.0
|
||||
jsonschema==4.26.0
|
||||
# via drf-spectacular
|
||||
jsonschema-specifications==2024.10.1
|
||||
jsonschema-specifications==2025.9.1
|
||||
# via jsonschema
|
||||
jwcrypto==1.5.6
|
||||
# via django-oauth-toolkit
|
||||
kombu==5.4.2
|
||||
kombu==5.6.2
|
||||
# via celery
|
||||
markdown==3.7
|
||||
markdown==3.10.1
|
||||
# via -r requirements.in
|
||||
oauthlib==3.2.2
|
||||
oauthlib==3.3.1
|
||||
# via django-oauth-toolkit
|
||||
packaging==24.2
|
||||
# via gunicorn
|
||||
pillow==11.1.0
|
||||
packaging==25.0
|
||||
# via
|
||||
# -r requirements.in
|
||||
# gunicorn
|
||||
# kombu
|
||||
pillow==12.1.0
|
||||
# via -r requirements.in
|
||||
prompt-toolkit==3.0.50
|
||||
prompt-toolkit==3.0.52
|
||||
# via click-repl
|
||||
psycopg[binary]==3.2.6
|
||||
psycopg[binary,pool]==3.3.2
|
||||
# via -r requirements.in
|
||||
psycopg-binary==3.2.6
|
||||
psycopg-binary==3.3.2
|
||||
# via psycopg
|
||||
pycparser==2.22
|
||||
psycopg-pool==3.3.0
|
||||
# via psycopg
|
||||
pycparser==3.0
|
||||
# via cffi
|
||||
python-dateutil==2.9.0.post0
|
||||
# via celery
|
||||
python-decouple==3.8
|
||||
# via -r requirements.in
|
||||
pyyaml==6.0.2
|
||||
pyyaml==6.0.3
|
||||
# via drf-spectacular
|
||||
redis==5.2.1
|
||||
redis==7.1.0
|
||||
# via
|
||||
# -r requirements.in
|
||||
# django-redis
|
||||
referencing==0.36.2
|
||||
referencing==0.37.0
|
||||
# via
|
||||
# jsonschema
|
||||
# jsonschema-specifications
|
||||
requests==2.32.3
|
||||
requests==2.32.5
|
||||
# via django-oauth-toolkit
|
||||
rpds-py==0.23.1
|
||||
rpds-py==0.30.0
|
||||
# via
|
||||
# jsonschema
|
||||
# referencing
|
||||
six==1.17.0
|
||||
# via python-dateutil
|
||||
sniffio==1.3.1
|
||||
# via anyio
|
||||
sqlparse==0.5.3
|
||||
sqlparse==0.5.5
|
||||
# via django
|
||||
typing-extensions==4.12.2
|
||||
typing-extensions==4.15.0
|
||||
# via
|
||||
# anyio
|
||||
# jwcrypto
|
||||
# psycopg
|
||||
# referencing
|
||||
tzdata==2025.1
|
||||
# via
|
||||
# celery
|
||||
# kombu
|
||||
uritemplate==4.1.1
|
||||
# psycopg-pool
|
||||
tzdata==2025.3
|
||||
# via kombu
|
||||
tzlocal==5.3.1
|
||||
# via celery
|
||||
uritemplate==4.2.0
|
||||
# via drf-spectacular
|
||||
urllib3==2.3.0
|
||||
urllib3==2.6.3
|
||||
# via requests
|
||||
vine==5.1.0
|
||||
# via
|
||||
# amqp
|
||||
# celery
|
||||
# kombu
|
||||
wcwidth==0.2.13
|
||||
wcwidth==0.6.0
|
||||
# via prompt-toolkit
|
||||
zope-event==5.0
|
||||
zope-event==6.1
|
||||
# via gevent
|
||||
zope-interface==7.2
|
||||
zope-interface==8.2
|
||||
# via gevent
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# setuptools
|
||||
xlsxwriter==3.2.3
|
||||
# via -r requirements.in
|
||||
django-admin-rangefilter==0.13.5
|
||||
|
|
|
|||
2
run.sh
2
run.sh
|
|
@ -5,4 +5,4 @@ while ! nc -z $DB_HOST 5432 ; do
|
|||
done
|
||||
#python3 manage.py collectstatic --noinput
|
||||
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
2
static/admin/css/django_jalali.min.css
vendored
Executable file
File diff suppressed because one or more lines are too long
20
static/admin/js/django_jalali.min.js
vendored
20
static/admin/js/django_jalali.min.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "base_charge.html" %}
|
||||
{% load static %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load jalali_tags %}
|
||||
{#{% load jalali_tags %}#}
|
||||
|
||||
{% block title %}افزایش موجودی{% endblock %}
|
||||
{% block page_title %}افزایش موجودی{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load jalali_tags %}
|
||||
{#{% load jalali_tags %}#}
|
||||
|
||||
{% block title %}wallet: Home{% endblock %}
|
||||
{% block page_title %}wallet: Home{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% load jalali_tags %}
|
||||
{#{% load jalali_tags %}#}
|
||||
<div>
|
||||
<div class="card card-body mb-3">
|
||||
<div class="row">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% load jalali_tags %}
|
||||
{#{% load jalali_tags %}#}
|
||||
<div>
|
||||
<p class="fw-bold mb-1">اطلاعات آقا</p>
|
||||
<div class="card card-body mb-3">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load jalali_tags %}
|
||||
{#{% load jalali_tags %}#}
|
||||
|
||||
{% block title %}user account{% endblock %}
|
||||
{% block page_title %}user account{% endblock %}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ from gooyal_accounts_client import AuthenticatedClient
|
|||
from gooyal_accounts_client.models import PublicUser, Application
|
||||
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.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():
|
||||
|
|
@ -60,6 +61,13 @@ def get_user_details(uuid):
|
|||
|
||||
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):
|
||||
client = get_client()
|
||||
data: Application = oauth2_apis_applications_retrieve.sync(client=client, id=uuid)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class LokiHandler(logging.Handler):
|
|||
try:
|
||||
payload = self.formatter.format(record)
|
||||
|
||||
_push_message(
|
||||
self._push_message(
|
||||
self._url, json=payload, timeout=self._timeout, auth=self._auth
|
||||
)
|
||||
except requests.exceptions.ReadTimeout:
|
||||
|
|
@ -41,7 +41,6 @@ class LokiHandler(logging.Handler):
|
|||
self.formatter = fmt
|
||||
|
||||
def _push_message(self, *args, **kwargs):
|
||||
print('hehe')
|
||||
if self._mode == "sync":
|
||||
return _push_message(*args, **kwargs)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue