accounts.0008 drops the columns and refuses to run while guest rows exist. Docs describe the current state; tests that only proved guest behaviour are gone. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
199 lines
9.8 KiB
Python
199 lines
9.8 KiB
Python
"""
|
||
لغو و بازپرداختِ سفارش از پنلِ ادمین (۲۰۲۶-۱۰-۰۲).
|
||
|
||
راههای شکستن که امتحان میشوند: بازپرداختِ دوبارهی یک سفارش، منفی شدنِ موجودیِ کسی که بخشی از
|
||
بسته را خرج کرده، بازپرداختِ سفارشی که پرداخت نشده، کلیکِ اشتباه در ادمین (بیصفحهی تأیید)، و
|
||
سفارشِ لغوشدهای که کاربرش واقعاً پول داده بود. خودِ برگرداندنِ پول در درگاه آزموده نمیشود چون این
|
||
کد آن را انجام نمیدهد.
|
||
"""
|
||
from datetime import timedelta
|
||
|
||
from django.contrib.auth import get_user_model
|
||
from django.core.cache import cache
|
||
from django.test import TestCase
|
||
from django.utils import timezone
|
||
from rest_framework.test import APIClient
|
||
|
||
from accounts.authentication import issue_access_token
|
||
from accounts.models import User
|
||
from affiliate.models import AffiliateAccount, Conversion
|
||
from subscription.models import HeartsPass, Plan, QuestionCredit, Subscription
|
||
from subscription.services import credit_balance
|
||
|
||
from .models import Order, Transaction
|
||
from .services import OrderStateError, apply_order_payment_result, cancel_pending_order, refund_order
|
||
|
||
|
||
class _Base(TestCase):
|
||
def setUp(self):
|
||
cache.clear()
|
||
self.user = User.objects.create(phone='09120007401')
|
||
self.client_app = APIClient()
|
||
self.client_app.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(self.user.id)}')
|
||
self.pack = Plan.objects.get(kind='pack', question_credits=20)
|
||
|
||
def tearDown(self):
|
||
cache.clear()
|
||
|
||
def paid_pack(self, ref='R1'):
|
||
response = self.client_app.post('/api/subscription/purchase', {'planId': str(self.pack.id)}, format='json')
|
||
order = Order.objects.get(pk=response.json()['orderId'])
|
||
apply_order_payment_result(order=order, gateway='zarinpal', gateway_ref_id=ref, new_status='paid', event='t')
|
||
order.refresh_from_db()
|
||
return order
|
||
|
||
def spend(self, n):
|
||
for i in range(n):
|
||
QuestionCredit.objects.create(user_id=self.user.id, delta=-1, reason='question', message_id=10_000 + i)
|
||
|
||
|
||
class RefundServiceTests(_Base):
|
||
def test_an_unused_pack_is_taken_back_whole_with_its_heart_days(self):
|
||
order = self.paid_pack()
|
||
summary = refund_order(order)
|
||
self.assertEqual((summary['credits_granted'], summary['credits_taken']), (20, 20))
|
||
self.assertEqual(credit_balance(self.user.id), 0)
|
||
# ۷ روزِ همین بسته از کارتِ ۷روزه کم شد، پس قلبِ نامحدود همین الان تمام است.
|
||
self.assertLessEqual(HeartsPass.objects.get(user_id=self.user.id).expires_at, timezone.now() + timedelta(seconds=5))
|
||
order.refresh_from_db()
|
||
self.assertEqual(order.status, 'refunded')
|
||
self.assertTrue(Transaction.objects.filter(order=order, status='refunded', gateway='admin').exists())
|
||
|
||
def test_what_was_already_spent_is_not_taken_twice_and_balance_never_goes_negative(self):
|
||
order = self.paid_pack()
|
||
self.spend(15)
|
||
summary = refund_order(order)
|
||
self.assertEqual(summary['credits_taken'], 5)
|
||
self.assertEqual(credit_balance(self.user.id), 0)
|
||
|
||
def test_only_this_orders_credits_are_taken_not_a_second_packs(self):
|
||
first = self.paid_pack(ref='R1')
|
||
self.paid_pack(ref='R2')
|
||
refund_order(first)
|
||
self.assertEqual(credit_balance(self.user.id), 20)
|
||
|
||
def test_a_second_refund_of_the_same_order_is_refused(self):
|
||
order = self.paid_pack()
|
||
refund_order(order)
|
||
with self.assertRaises(OrderStateError):
|
||
refund_order(order)
|
||
self.assertEqual(credit_balance(self.user.id), 0)
|
||
self.assertEqual(QuestionCredit.objects.filter(reason='refund').count(), 1)
|
||
|
||
def test_an_unpaid_order_cannot_be_refunded(self):
|
||
order = Order.objects.create(user_id=self.user.id, amount=1000, idempotency_key='u1')
|
||
with self.assertRaises(OrderStateError):
|
||
refund_order(order)
|
||
|
||
def test_a_subscription_refund_takes_its_days_off(self):
|
||
plan = Plan.objects.filter(kind='subscription').first()
|
||
sub = Subscription.objects.create(user_id=self.user.id, plan=plan, status='active',
|
||
expires_at=timezone.now() + timedelta(days=plan.duration_days + 10))
|
||
order = Order.objects.create(user_id=self.user.id, amount=plan.price, subscription_id=sub.id,
|
||
plan_id=plan.id, status='paid', idempotency_key='s1')
|
||
refund_order(order)
|
||
sub.refresh_from_db()
|
||
self.assertAlmostEqual((sub.expires_at - timezone.now()).days, 9, delta=1)
|
||
self.assertEqual(sub.status, 'active')
|
||
|
||
def test_a_subscription_with_nothing_left_is_canceled(self):
|
||
plan = Plan.objects.filter(kind='subscription').first()
|
||
sub = Subscription.objects.create(user_id=self.user.id, plan=plan, status='active',
|
||
expires_at=timezone.now() + timedelta(days=3))
|
||
order = Order.objects.create(user_id=self.user.id, amount=plan.price, subscription_id=sub.id,
|
||
plan_id=plan.id, status='paid', idempotency_key='s2')
|
||
refund_order(order)
|
||
sub.refresh_from_db()
|
||
self.assertEqual(sub.status, 'canceled')
|
||
|
||
def test_the_referrers_commission_for_this_order_is_undone(self):
|
||
referrer = User.objects.create(phone='09120007402')
|
||
account = AffiliateAccount.objects.create(user_id=referrer.id, referral_code='REF1', commission_rate=0.1)
|
||
Conversion.objects.create(affiliate_account=account, referred_user_id=self.user.id)
|
||
order = self.paid_pack()
|
||
self.assertEqual(Conversion.objects.get().status, 'paid')
|
||
refund_order(order)
|
||
conversion = Conversion.objects.get()
|
||
self.assertEqual((conversion.status, conversion.commission_amount, conversion.order_id), ('pending', 0, None))
|
||
|
||
def test_the_invoice_shows_the_refund(self):
|
||
refund_order(self.paid_pack())
|
||
invoice = self.client_app.get('/api/payment/invoices').json()['invoices'][0]
|
||
self.assertEqual(invoice['status'], 'refunded')
|
||
|
||
|
||
class CancelServiceTests(_Base):
|
||
def test_a_pending_order_is_canceled_and_a_paid_one_is_not(self):
|
||
order = Order.objects.create(user_id=self.user.id, amount=1000, idempotency_key='c1')
|
||
cancel_pending_order(order)
|
||
order.refresh_from_db()
|
||
self.assertEqual(order.status, 'canceled')
|
||
with self.assertRaises(OrderStateError):
|
||
cancel_pending_order(self.paid_pack())
|
||
|
||
def test_if_the_user_did_pay_a_canceled_order_the_payment_still_counts(self):
|
||
response = self.client_app.post('/api/subscription/purchase', {'planId': str(self.pack.id)}, format='json')
|
||
order = Order.objects.get(pk=response.json()['orderId'])
|
||
cancel_pending_order(order)
|
||
apply_order_payment_result(order=order, gateway='zarinpal', gateway_ref_id='LATE', new_status='paid', event='t')
|
||
self.assertEqual(credit_balance(self.user.id), 20)
|
||
|
||
|
||
class OrderAdminTests(_Base):
|
||
def setUp(self):
|
||
super().setUp()
|
||
staff = get_user_model().objects.create_superuser('admin', 'a@example.com', 'pw-for-tests-only')
|
||
self.admin = self.client
|
||
self.admin.force_login(staff)
|
||
|
||
def action(self, name, order, **extra):
|
||
return self.admin.post('/admin/payment/order/', {'action': name, '_selected_action': [order.pk], **extra})
|
||
|
||
def test_refund_asks_for_confirmation_first(self):
|
||
order = self.paid_pack()
|
||
page = self.action('refund', order)
|
||
self.assertEqual(page.status_code, 200)
|
||
self.assertContains(page, 'پول را برنمیگرداند')
|
||
order.refresh_from_db()
|
||
self.assertEqual(order.status, 'paid')
|
||
self.assertEqual(credit_balance(self.user.id), 20)
|
||
|
||
done = self.action('refund', order, confirm='yes')
|
||
self.assertEqual(done.status_code, 302)
|
||
order.refresh_from_db()
|
||
self.assertEqual(order.status, 'refunded')
|
||
self.assertEqual(credit_balance(self.user.id), 0)
|
||
|
||
def test_cancel_action(self):
|
||
order = Order.objects.create(user_id=self.user.id, amount=1000, idempotency_key='c2')
|
||
self.assertEqual(self.action('cancel_pending', order).status_code, 302)
|
||
order.refresh_from_db()
|
||
self.assertEqual(order.status, 'canceled')
|
||
|
||
def test_status_cannot_be_edited_by_hand(self):
|
||
order = self.paid_pack()
|
||
page = self.admin.get(f'/admin/payment/order/{order.pk}/change/')
|
||
self.assertEqual(page.status_code, 200)
|
||
self.assertNotContains(page, 'name="status"')
|
||
|
||
|
||
class ForgedFailureCallbackTests(TestCase):
|
||
"""
|
||
کالبکِ زرینپال بیاحراز است. Status=NOKِ جعلی نباید سفارشِ بازپرداختشده یا لغوشده را
|
||
«ناموفق» کند (تا ۲۰۲۶-۱۰-۰۳ میکرد و ردِ بازپرداخت از وضعیتِ سفارش میرفت).
|
||
"""
|
||
|
||
def test_nok_does_not_overwrite_refunded_or_canceled(self):
|
||
for status in ('refunded', 'canceled'):
|
||
order = Order.objects.create(user_id=1, amount=1000, status=status,
|
||
gateway_authority=f'A-{status}', idempotency_key=f'k-{status}')
|
||
APIClient().get('/api/payment/zarinpal/callback', {'Authority': f'A-{status}', 'Status': 'NOK'})
|
||
order.refresh_from_db()
|
||
self.assertEqual(order.status, status)
|
||
|
||
def test_nok_still_fails_a_pending_order(self):
|
||
order = Order.objects.create(user_id=1, amount=1000, gateway_authority='A-p', idempotency_key='k-p')
|
||
APIClient().get('/api/payment/zarinpal/callback', {'Authority': 'A-p', 'Status': 'NOK'})
|
||
order.refresh_from_db()
|
||
self.assertEqual(order.status, 'failed')
|