add reference field

This commit is contained in:
Sayyid Hamid Mahdavi 2026-02-07 09:28:06 +03:30
parent 4393e3878a
commit c24b52e341
4 changed files with 158 additions and 8 deletions

View file

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

View file

@ -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_details_a7ff79aa'),
),
]

View file

@ -1,6 +1,7 @@
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
@ -32,8 +33,8 @@ class AccountQuerySet(models.QuerySet):
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
@ -111,6 +112,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'}
@ -118,6 +121,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_details_a7ff79aa'
),
]
def __str__(self):
try:
@ -381,3 +391,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

View file

@ -7,10 +7,12 @@ from django.views.generic import DetailView, ListView, UpdateView
from django_filters.rest_framework import DjangoFilterBackend
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.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
from apps.wallet.models import Transaction, Wallet, Account
@ -66,7 +68,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 +167,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,7 +228,6 @@ class TransactionDetailView(UpdateView):
return super().form_valid(form)
class WalletAccountListAPIView(generics.ListAPIView):
'''
API for getting authenticated user balance.
@ -306,3 +307,68 @@ class WalletTransactionListView(generics.ListAPIView):
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()
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
@action(detail=False, methods=["GET"])
def grouped(self):
from collections import defaultdict
from django.db.models import Count, Sum
# فقط ۲ کوئری بهینه
# کوئری ۱: آمار کلی (سبک)
stats = dict(Transaction.objects.values('refrence').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
})