from django.contrib.auth import get_user_model from django.test import TestCase, override_settings from django.urls import reverse from rest_framework.test import APIClient from apps.accounts.models import Role, TeacherProfile from apps.courses.models import ( Course, Lesson, LessonKind, LessonStatus, Section, ) from apps.enrollments.models import Enrollment from apps.payments.models import Gateway, Order, OrderStatus User = get_user_model() CONSOLE_PAYMENT = "apps.payments.services.gateway.ConsolePaymentBackend" # --------------------------------------------------------------------------- helpers def _teacher(phone="09120000001", approved=True): u = User.objects.create_user(phone_number=phone, full_name="Teacher", role=Role.TEACHER) TeacherProfile.objects.create(user=u, is_approved=approved) return u def _student(phone="09120000099"): return User.objects.create_user(phone_number=phone, full_name="Student", role=Role.STUDENT) def _paid_course(instructor, *, price=1_000_000, n_lessons=2, published=True): c = Course.objects.create( instructor=instructor, title="Pay Course", tagline="T", description="D", level="beginner", language="fa", price_toman=price, ) if published: c.publish() sec = Section.objects.create(course=c, title="S1", order=0) for i in range(n_lessons): Lesson.objects.create( section=sec, title=f"L{i+1}", kind=LessonKind.VIDEO, video_url=f"https://e.com/{i}", status=LessonStatus.PUBLISHED, order=i, ) return c def _free_course(instructor, *, published=True): c = Course.objects.create( instructor=instructor, title="Free", tagline="T", description="D", level="beginner", language="fa", price_toman=None, ) if published: c.publish() return c # --------------------------------------------------------------------------- order math class OrderMathTests(TestCase): @override_settings(ILO_COMMISSION_PERCENT=15, PAYMENT_BACKEND=CONSOLE_PAYMENT) def test_commission_split_at_15_percent(self): from apps.payments.services.orders import start_checkout t = _teacher() s = _student() course = _paid_course(t, price=1_000_000) order, redirect_url = start_checkout(s, course) self.assertEqual(order.amount_toman, 1_000_000) self.assertEqual(order.ilo_fee_toman, 150_000) self.assertEqual(order.instructor_share_toman, 850_000) self.assertEqual(order.commission_percent_snapshot, 15) self.assertEqual(order.gateway, Gateway.CONSOLE) self.assertEqual(order.status, OrderStatus.PENDING) self.assertTrue(order.authority.startswith("CONSOLE-")) self.assertIn("Authority=CONSOLE-", redirect_url) self.assertIn("Status=OK", redirect_url) @override_settings(ILO_COMMISSION_PERCENT=20, PAYMENT_BACKEND=CONSOLE_PAYMENT) def test_commission_uses_setting(self): from apps.payments.services.orders import start_checkout t = _teacher() s = _student() course = _paid_course(t, price=500_000) order, _ = start_checkout(s, course) self.assertEqual(order.ilo_fee_toman, 100_000) self.assertEqual(order.instructor_share_toman, 400_000) # --------------------------------------------------------------------------- checkout endpoint @override_settings(PAYMENT_BACKEND=CONSOLE_PAYMENT) class CheckoutAPITests(TestCase): def setUp(self): self.client = APIClient() self.t = _teacher() self.s = _student() self.paid = _paid_course(self.t, price=2_000_000) self.free = _free_course(self.t) def test_anon_blocked(self): r = self.client.post(f"/api/courses/{self.paid.slug}/checkout/") self.assertEqual(r.status_code, 401) def test_teacher_blocked(self): self.client.force_authenticate(self.t) r = self.client.post(f"/api/courses/{self.paid.slug}/checkout/") self.assertEqual(r.status_code, 403) def test_student_checkout_returns_redirect(self): self.client.force_authenticate(self.s) r = self.client.post(f"/api/courses/{self.paid.slug}/checkout/") self.assertEqual(r.status_code, 201, r.content) body = r.json() self.assertEqual(body["amount_toman"], 2_000_000) self.assertEqual(body["gateway"], "console") self.assertIn("Authority=CONSOLE-", body["redirect_url"]) self.assertEqual(Order.objects.filter(student=self.s).count(), 1) def test_checkout_for_free_course_400(self): self.client.force_authenticate(self.s) r = self.client.post(f"/api/courses/{self.free.slug}/checkout/") self.assertEqual(r.status_code, 400) def test_checkout_when_already_enrolled_400(self): Enrollment.objects.create(student=self.s, course=self.paid) self.client.force_authenticate(self.s) r = self.client.post(f"/api/courses/{self.paid.slug}/checkout/") self.assertEqual(r.status_code, 400) def test_enroll_endpoint_returns_402_for_paid_course(self): self.client.force_authenticate(self.s) r = self.client.post(f"/api/courses/{self.paid.slug}/enroll/") self.assertEqual(r.status_code, 402) body = r.json() self.assertIn("checkout_url", body) self.assertEqual(body["price_toman"], 2_000_000) # --------------------------------------------------------------------------- callback @override_settings(PAYMENT_BACKEND=CONSOLE_PAYMENT, PAYMENT_SUCCESS_REDIRECT_URL="/p/ok", PAYMENT_FAILURE_REDIRECT_URL="/p/fail") class CallbackAPITests(TestCase): def setUp(self): self.client = APIClient() self.t = _teacher() self.s = _student() self.course = _paid_course(self.t, price=1_000_000) def _open_pending_order(self): self.client.force_authenticate(self.s) r = self.client.post(f"/api/courses/{self.course.slug}/checkout/") order_id = r.json()["order_id"] # The console redirect URL has Authority=CONSOLE-&Status=OK&order_id= return Order.objects.get(pk=order_id) def test_full_flow_success(self): order = self._open_pending_order() r = self.client.get( f"/api/payments/callback/console/?order_id={order.pk}&Status=OK&Authority={order.authority}" ) self.assertEqual(r.status_code, 302) self.assertIn("/p/ok", r["Location"]) order.refresh_from_db() self.assertEqual(order.status, OrderStatus.PAID) self.assertTrue( Enrollment.objects.filter(student=self.s, course=self.course).exists() ) def test_callback_idempotent(self): order = self._open_pending_order() r1 = self.client.get( f"/api/payments/callback/console/?order_id={order.pk}&Status=OK&Authority={order.authority}" ) r2 = self.client.get( f"/api/payments/callback/console/?order_id={order.pk}&Status=OK&Authority={order.authority}" ) self.assertEqual(r1.status_code, 302) self.assertEqual(r2.status_code, 302) # Still exactly one enrollment self.assertEqual( Enrollment.objects.filter(student=self.s, course=self.course).count(), 1 ) def test_callback_user_cancel_marks_failed(self): order = self._open_pending_order() r = self.client.get( f"/api/payments/callback/console/?order_id={order.pk}&Status=NOK&Authority={order.authority}" ) self.assertEqual(r.status_code, 302) self.assertIn("/p/fail", r["Location"]) order.refresh_from_db() self.assertEqual(order.status, OrderStatus.FAILED) self.assertFalse( Enrollment.objects.filter(student=self.s, course=self.course).exists() ) def test_callback_unknown_order(self): r = self.client.get( "/api/payments/callback/console/?order_id=999999&Status=OK&Authority=X" ) self.assertEqual(r.status_code, 302) self.assertIn("/p/fail", r["Location"]) # --------------------------------------------------------------------------- order lists @override_settings(PAYMENT_BACKEND=CONSOLE_PAYMENT) class OrderListAPITests(TestCase): def setUp(self): self.client = APIClient() self.t = _teacher() self.s = _student() self.course = _paid_course(self.t, price=1_500_000) def test_my_orders_lists_own(self): # complete a full flow first self.client.force_authenticate(self.s) co = self.client.post(f"/api/courses/{self.course.slug}/checkout/").json() self.client.get( f"/api/payments/callback/console/?order_id={co['order_id']}&Status=OK&Authority=CONSOLE-{co['order_id']}" ) r = self.client.get("/api/me/orders/") self.assertEqual(r.status_code, 200) body = r.json() self.assertEqual(len(body), 1) self.assertEqual(body[0]["status"], "paid") self.assertEqual(body[0]["amount_toman"], 1_500_000) self.assertEqual(body[0]["course_slug"], self.course.slug) def test_instructor_sales(self): # student buys self.client.force_authenticate(self.s) co = self.client.post(f"/api/courses/{self.course.slug}/checkout/").json() self.client.get( f"/api/payments/callback/console/?order_id={co['order_id']}&Status=OK&Authority=CONSOLE-{co['order_id']}" ) # teacher sees the sale self.client.force_authenticate(self.t) r = self.client.get("/api/instructor/orders/?status=paid") self.assertEqual(r.status_code, 200) body = r.json() self.assertEqual(len(body), 1) self.assertEqual(body[0]["amount_toman"], 1_500_000) self.assertEqual(body[0]["ilo_fee_toman"], 225_000) # 15% self.assertEqual(body[0]["instructor_share_toman"], 1_275_000) self.assertEqual(body[0]["student_id"], self.s.id) def test_student_blocked_from_instructor_sales(self): self.client.force_authenticate(self.s) r = self.client.get("/api/instructor/orders/") self.assertEqual(r.status_code, 403) @override_settings(PAYMENT_BACKEND=CONSOLE_PAYMENT) class OrderDetailAPITests(TestCase): def setUp(self): self.client = APIClient() self.t = _teacher() self.s = _student() self.course = _paid_course(self.t, price=500_000) self.client.force_authenticate(self.s) co = self.client.post(f"/api/courses/{self.course.slug}/checkout/").json() self.order_id = co["order_id"] self.client.get( f"/api/payments/callback/console/?order_id={self.order_id}&Status=OK&Authority=CONSOLE-{self.order_id}" ) def test_get_my_order_detail(self): r = self.client.get(f"/api/me/orders/{self.order_id}/") self.assertEqual(r.status_code, 200, r.content) body = r.json() self.assertEqual(body["id"], self.order_id) self.assertEqual(body["status"], "paid") self.assertEqual(body["amount_toman"], 500_000) self.assertTrue(body["ref_id"]) def test_cannot_get_others_order(self): other = _student(phone="09120000022") self.client.force_authenticate(other) r = self.client.get(f"/api/me/orders/{self.order_id}/") self.assertEqual(r.status_code, 404)