list a user's own support tickets, Persian digits in completion notifications, rewrite .env.example to match the code

GET /api/support/tickets returns the caller's last 20 tickets with their
support-reply count, on the read throttle so listing never spends the
30/hour create quota. Before it the app kept ticket ids only in one
browser's localStorage, so a ticket and its answer vanished with the device.

Completion notifications now use Persian digits and drop the
'with 0 stars ... 0 points' sentence when nothing was earned.

.env.example: NUM_PROXIES=2 (ArvanCloud + nginx), KAVENEGAR_OTP_TEMPLATE,
BAZAAR_PISHKHAN_API_SECRET, sandbox off by default; removed variables the
code no longer reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 21:18:12 +03:30
parent 8321dc22c3
commit 93b451d422
7 changed files with 192 additions and 163 deletions

View File

@@ -23,6 +23,19 @@ class TicketCreateResponseSerializer(serializers.Serializer):
status = serializers.CharField()
class TicketSummarySerializer(serializers.Serializer):
ticketId = serializers.CharField()
subject = serializers.CharField()
category = serializers.CharField()
status = serializers.CharField()
createdAt = serializers.DateTimeField()
supportReplies = serializers.IntegerField()
class TicketListResponseSerializer(serializers.Serializer):
tickets = TicketSummarySerializer(many=True)
class TicketMessageItemSerializer(serializers.Serializer):
id = serializers.CharField()
sender = serializers.CharField()

View File

@@ -354,3 +354,41 @@ class SupportCreateGuestThrottleBypassTests(TestCase):
self.assertEqual(BugReport.objects.count(), bug_count_before)
self.assertEqual(Ticket.objects.count(), ticket_count_before)
class TicketListTests(TestCase):
"""GET /api/support/tickets: فقط تیکت‌های خودِ کاربر، تازه‌ترین اول، با شمارِ پاسخِ پشتیبانی."""
def setUp(self):
cache.clear()
self.client = APIClient()
self.user = User.objects.create(device_id='device-ticket-list-1', is_guest=True, name='')
self.other = User.objects.create(device_id='device-ticket-list-2', is_guest=True, name='')
self.auth = {'HTTP_AUTHORIZATION': f'Bearer {issue_access_token(self.user.id)}'}
def test_lists_only_own_tickets_with_reply_count(self):
old = Ticket.objects.create(user_id=self.user.id, subject='قدیمی', category='learning')
TicketMessage.objects.create(ticket=old, sender='user', text='سلام')
TicketMessage.objects.create(ticket=old, sender='support', text='جواب')
new = Ticket.objects.create(user_id=self.user.id, subject='تازه', category='payment')
Ticket.objects.create(user_id=self.other.id, subject='مالِ دیگری', category='other')
response = self.client.get('/api/support/tickets', **self.auth)
self.assertEqual(response.status_code, 200)
tickets = response.json()['tickets']
self.assertEqual([t['ticketId'] for t in tickets], [str(new.pk), str(old.pk)])
self.assertEqual(tickets[1]['supportReplies'], 1)
self.assertEqual(tickets[0]['supportReplies'], 0)
def test_requires_authentication(self):
self.assertIn(self.client.get('/api/support/tickets').status_code, (401, 403))
def test_listing_does_not_spend_the_create_quota(self):
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'support_create': '1/hour'}):
for _ in range(3):
self.assertEqual(self.client.get('/api/support/tickets', **self.auth).status_code, 200)
response = self.client.post(
'/api/support/tickets', {'subject': 'س', 'category': 'other'}, format='json', **self.auth,
)
self.assertEqual(response.status_code, 200)

View File

@@ -1,3 +1,4 @@
from django.db.models import Count, Q
from django.shortcuts import get_object_or_404
from drf_spectacular.utils import extend_schema
from rest_framework.permissions import IsAuthenticated
@@ -12,6 +13,7 @@ from .serializers import (
TicketCreateRequestSerializer,
TicketCreateResponseSerializer,
TicketDetailResponseSerializer,
TicketListResponseSerializer,
TicketMessageCreateRequestSerializer,
TicketMessageCreateResponseSerializer,
)
@@ -88,7 +90,15 @@ class BugReportCreateView(APIView):
class TicketCreateView(APIView):
"""POST /api/support/tickets — ایجاد تیکت"""
"""
GET /api/support/tickets — تیکت‌های همین کاربر (۲۰ تای آخر)
POST /api/support/tickets — ایجاد تیکت
GET تا ۲۰۲۶-۰۹-۲۹ نبود و اپ شناسه‌ی تیکت‌ها را فقط در حافظه‌ی همان مرورگر
نگه می‌داشت؛ با عوض شدنِ دستگاه، تیکت و پاسخِ پشتیبانی از دید پنهان می‌شد.
"""
LIST_LIMIT = 20
permission_classes = [IsAuthenticated]
# مثلِ BugReportCreateView: هیچ idempotency ای نیست، پس این endpoint هم
@@ -99,6 +109,36 @@ class TicketCreateView(APIView):
throttle_scope = 'support_create'
throttle_classes = [GuestAwareSupportThrottle]
def get_throttles(self):
# GET سهمِ خواندن را می‌خورد، نه سقفِ ۳۰ در ساعتِ ساختِ رکورد.
if self.request.method == 'GET':
self.throttle_scope = 'read'
return super().get_throttles()
@extend_schema(
operation_id='support_tickets_list',
responses=TicketListResponseSerializer,
tags=['Support (support)'],
)
def get(self, request):
tickets = (
Ticket.objects.filter(user_id=request.user.id)
.annotate(support_replies=Count('messages', filter=Q(messages__sender='support')))
.order_by('-created_at')[: self.LIST_LIMIT]
)
data = {'tickets': [
{
'ticketId': str(t.pk),
'subject': t.subject,
'category': t.category,
'status': t.status,
'createdAt': t.created_at,
'supportReplies': t.support_replies,
}
for t in tickets
]}
return Response(TicketListResponseSerializer(data).data)
@extend_schema(
request=TicketCreateRequestSerializer,
responses=TicketCreateResponseSerializer,