senior's post-demo changes: parent report in the student's own account, Jalali birth date, invoices, math-only tutor, priced quotas
- remove the family app from the root (separate parent accounts, invite codes, parental settings); progress.0006 drops its tables, permissions and migration history, and Profile.parent_phone, the two family notification types and ?userId= on /api/progress/nodes go with it - GET /api/user/weekly-report: last 7 days next to the 7 before (estimated minutes, questions, accuracy, islands, tutor questions) for the "parent report" page - profile birthDate in Jalali (accounts/jalali.py), ages 5-99, birthYear derived from it - GET /api/payment/invoices; orders now snapshot list price and plan title - tutor answers only math: server pre-filter, scope rule in the prompt (tutor-v11), refusal marker replaced with fixed text; self-harm gets a help line instead - streamed answers now record token usage (it was always 0) - daily tutor quota: AI_DAILY_QUOTA_FREE (default 3) vs Plan.ai_daily_quota - delete_users management command for wiping test accounts across every app - docs/ai-cost-and-guardrails.md: measured cost per question and guardrail results Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ from . import bazaar, zarinpal
|
||||
from .models import Order
|
||||
from .permissions import HasInternalServiceKey
|
||||
from .serializers import (
|
||||
InvoiceListResponseSerializer,
|
||||
BazaarVerifyRequestSerializer,
|
||||
BazaarVerifyResponseSerializer,
|
||||
CreateOrderRequestSerializer,
|
||||
@@ -43,7 +44,7 @@ def _redirect_to_configured_url(url: str) -> HttpResponseRedirect:
|
||||
|
||||
|
||||
class GuestAwareBazaarThrottle(ScopedRateThrottle):
|
||||
"""همون الگوی GuestAwareAIThrottle/GuestAwareFamilyThrottle/etc: چرخشِ
|
||||
"""همون الگوی GuestAwareAIThrottle/GuestAwareDiscountThrottle/etc: چرخشِ
|
||||
حساب مهمان (هر deviceId یه pk تازه) نباید bucket تازهی bazaar_verify
|
||||
بده، چون هر درخواستِ پذیرفتهشده یه فراخوانیِ سرور-به-سرور به بازاره.
|
||||
برای مهمانها کلیدِ cache بر اساسِ IP هست، نه pk."""
|
||||
@@ -365,3 +366,53 @@ class OrderDetailView(APIView):
|
||||
],
|
||||
}
|
||||
return Response(OrderDetailResponseSerializer(data).data)
|
||||
|
||||
|
||||
INVOICE_LIMIT = 50
|
||||
|
||||
|
||||
def invoice_payload(order) -> dict:
|
||||
"""یک سفارش به شکلِ فاکتور. کدِ تخفیف و کدِ پیگیری از جدولهای خودشان خوانده میشوند."""
|
||||
from discount.models import Redemption
|
||||
|
||||
redemption = Redemption.objects.filter(order_id=order.id).select_related('discount_code').first()
|
||||
paid = next((t for t in order.transactions.all() if t.status == 'paid'), None)
|
||||
# کدِ تخفیفِ ۱۰۰٪ هم یک Transactionِ «paid» میسازد، با refی ساختگی (discount-…)؛
|
||||
# آن کدِ پیگیریِ بانکی نیست و به کاربر نشان داده نمیشود.
|
||||
ref = paid.gateway_ref_id if paid and paid.gateway != 'discount' else ''
|
||||
return {
|
||||
'orderId': str(order.id),
|
||||
'invoiceNumber': f'TD-{order.id:06d}',
|
||||
'status': order.status,
|
||||
'provider': order.provider,
|
||||
'planTitle': order.plan_title,
|
||||
'listPrice': order.list_price,
|
||||
'discountAmount': (order.list_price - order.amount) if order.list_price is not None else None,
|
||||
'discountCode': redemption.discount_code.code if redemption else '',
|
||||
'amount': order.amount,
|
||||
'currency': order.currency,
|
||||
'createdAt': order.created_at,
|
||||
'paidAt': paid.paid_at if paid else None,
|
||||
'refId': ref,
|
||||
}
|
||||
|
||||
|
||||
class InvoiceListView(APIView):
|
||||
"""
|
||||
GET /api/payment/invoices — پرداختها و فاکتورهای خودِ کاربر، تازهترین اول.
|
||||
|
||||
همهی سفارشها برمیگردند، نه فقط پرداختشدهها، تا کسی که پولش کم شده ولی
|
||||
اشتراکش فعال نشده سفارشِ «در انتظار» یا «ناموفق» خودش را ببیند و کدش را به
|
||||
پشتیبانی بدهد. اینجا از زرینپال بازسنجی نمیشود (برای ۵۰ سفارش ۵۰ فراخوانی
|
||||
میشد)؛ GET /api/payment/orders/{id} و وضعیتِ اشتراک این کار را میکنند.
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
throttle_scope = 'read'
|
||||
throttle_classes = [ScopedRateThrottle]
|
||||
|
||||
@extend_schema(responses=InvoiceListResponseSerializer, tags=['Payment (payment)'])
|
||||
def get(self, request):
|
||||
orders = (Order.objects.filter(user_id=request.user.id)
|
||||
.prefetch_related('transactions').order_by('-created_at', '-id')[:INVOICE_LIMIT])
|
||||
return Response(InvoiceListResponseSerializer({'invoices': [invoice_payload(o) for o in orders]}).data)
|
||||
|
||||
Reference in New Issue
Block a user