Files
MathApp/payment/tests_invoices.py
Zareian 39247f6d53 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>
2026-10-02 02:38:43 +03:30

93 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
GET /api/payment/invoices — «پرداخت‌ها و فاکتورها»ی پروفایل (۲۰۲۶-۱۰-۰۱).
راه‌های شکستن که این‌جا امتحان می‌شوند: دیدنِ فاکتورِ دیگری، عوض شدنِ فاکتورِ قدیمی
با عوض شدنِ قیمتِ پلن، و نشان دادنِ refِ ساختگیِ کدِ ۱۰۰٪ به‌جای کدِ پیگیریِ بانکی.
"""
from django.core.cache import cache
from django.test import TestCase
from rest_framework.test import APIClient
from accounts.authentication import issue_access_token
from accounts.models import User
from discount.models import DiscountCode
from payment.models import Order
from payment.services import apply_order_payment_result
from subscription.models import Plan
def _client(user):
client = APIClient()
client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(user.id)}')
return client
class InvoiceTests(TestCase):
def setUp(self):
cache.clear()
self.user = User.objects.create(phone='09120007001', is_guest=False)
self.client = _client(self.user)
self.plan = Plan.objects.create(title='سالانه', price=299000, duration_days=365)
def tearDown(self):
cache.clear()
def buy(self):
response = self.client.post('/api/subscription/purchase', {'planId': str(self.plan.id)}, format='json')
self.assertEqual(response.status_code, 200, response.content)
return Order.objects.get(pk=response.json()['orderId'])
def invoices(self, client=None):
response = (client or self.client).get('/api/payment/invoices')
self.assertEqual(response.status_code, 200, response.content)
return response.json()['invoices']
def test_paid_order_with_discount_reads_like_an_invoice(self):
order = self.buy()
DiscountCode.objects.create(code='MEHR20', type='percent', value=20, max_uses=5)
redeem = self.client.post('/api/discount/redeem', {'code': 'MEHR20', 'orderId': str(order.id)}, format='json')
self.assertEqual(redeem.status_code, 200, redeem.content)
apply_order_payment_result(order=order, gateway='zarinpal', gateway_ref_id='A00000123456',
new_status='paid', event='test')
[invoice] = self.invoices()
self.assertEqual(invoice['invoiceNumber'], f'TD-{order.id:06d}')
self.assertEqual(invoice['status'], 'paid')
self.assertEqual(invoice['planTitle'], 'سالانه')
self.assertEqual((invoice['listPrice'], invoice['discountAmount'], invoice['amount']),
(299000, 59800, 239200))
self.assertEqual(invoice['discountCode'], 'MEHR20')
self.assertEqual(invoice['refId'], 'A00000123456')
self.assertIsNotNone(invoice['paidAt'])
def test_changing_the_plan_later_does_not_rewrite_an_old_invoice(self):
self.buy()
Plan.objects.filter(pk=self.plan.pk).update(title='سالانه‌ی ویژه', price=399000)
[invoice] = self.invoices()
self.assertEqual((invoice['planTitle'], invoice['listPrice']), ('سالانه', 299000))
def test_full_discount_shows_no_fake_bank_reference(self):
order = self.buy()
DiscountCode.objects.create(code='FREE100', type='percent', value=100, max_uses=5)
self.client.post('/api/discount/redeem', {'code': 'FREE100', 'orderId': str(order.id)}, format='json')
[invoice] = self.invoices()
self.assertEqual((invoice['status'], invoice['amount'], invoice['refId']), ('paid', 0, ''))
def test_unpaid_orders_are_listed_too(self):
self.buy()
[invoice] = self.invoices()
self.assertEqual((invoice['status'], invoice['paidAt'], invoice['refId']), ('pending', None, ''))
def test_someone_else_sees_nothing(self):
self.buy()
stranger = User.objects.create(phone='09120007002', is_guest=False)
self.assertEqual(self.invoices(_client(stranger)), [])
def test_orders_from_before_the_snapshot_have_no_list_price(self):
Order.objects.create(user_id=self.user.id, amount=99000, idempotency_key='old-order')
[invoice] = self.invoices()
self.assertEqual((invoice['listPrice'], invoice['discountAmount'], invoice['planTitle']), (None, None, ''))
def test_requires_a_token(self):
self.assertIn(APIClient().get('/api/payment/invoices').status_code, (401, 403))