from datetime import timedelta from django.contrib.auth import get_user_model from django.test import TestCase, override_settings from django.utils import timezone 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 Order, OrderStatus, PromoCode, Referral User = get_user_model() CONSOLE = "apps.payments.services.gateway.ConsolePaymentBackend" def _teacher(phone="09120000001"): u = User.objects.create_user(phone_number=phone, full_name="Teacher", role=Role.TEACHER) TeacherProfile.objects.create(user=u, is_approved=True) 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): c = Course.objects.create( instructor=instructor, title="C", tagline="T", description="D", level="beginner", language="fa", price_toman=price, ) c.publish() sec = Section.objects.create(course=c, title="S") Lesson.objects.create(section=sec, title="L", kind=LessonKind.VIDEO, video_url="https://e.com/x", status=LessonStatus.PUBLISHED) return c # ==================================================================== referral code class ReferralCodeAutoTests(TestCase): def test_referral_code_auto_generated_on_save(self): u = User.objects.create_user(phone_number="09120000010", full_name="X", role=Role.STUDENT) self.assertEqual(len(u.referral_code), 6) u2 = User.objects.create_user(phone_number="09120000011", full_name="Y", role=Role.STUDENT) self.assertNotEqual(u.referral_code, u2.referral_code) # ==================================================================== promo codes @override_settings(PAYMENT_BACKEND=CONSOLE) class PromoCodeCheckoutTests(TestCase): def setUp(self): self.client = APIClient() self.t = _teacher() self.s = _student() self.course = _paid_course(self.t, price=1_000_000) def test_percent_promo_applied(self): PromoCode.objects.create(code="WELCOME10", discount_percent=10, is_active=True) self.client.force_authenticate(self.s) r = self.client.post( f"/api/courses/{self.course.slug}/checkout/", {"promo_code": "welcome10"}, format="json", ) self.assertEqual(r.status_code, 201, r.content) order = Order.objects.get(pk=r.json()["order_id"]) self.assertEqual(order.base_amount_toman, 1_000_000) self.assertEqual(order.discount_amount_toman, 100_000) self.assertEqual(order.amount_toman, 900_000) self.assertEqual(order.promo_code.code, "WELCOME10") def test_fixed_promo_applied(self): PromoCode.objects.create(code="MINUS200K", discount_amount_toman=200_000, is_active=True) self.client.force_authenticate(self.s) r = self.client.post( f"/api/courses/{self.course.slug}/checkout/", {"promo_code": "MINUS200K"}, format="json", ) self.assertEqual(r.status_code, 201) order = Order.objects.get(pk=r.json()["order_id"]) self.assertEqual(order.amount_toman, 800_000) def test_invalid_promo_400(self): self.client.force_authenticate(self.s) r = self.client.post( f"/api/courses/{self.course.slug}/checkout/", {"promo_code": "NOTREAL"}, format="json", ) self.assertEqual(r.status_code, 400) def test_expired_promo_400(self): PromoCode.objects.create( code="OLD", discount_percent=10, valid_until=timezone.now() - timedelta(days=1), ) self.client.force_authenticate(self.s) r = self.client.post( f"/api/courses/{self.course.slug}/checkout/", {"promo_code": "OLD"}, format="json", ) self.assertEqual(r.status_code, 400) def test_max_uses_exhausted(self): PromoCode.objects.create(code="ONCE", discount_percent=10, max_uses=1, used_count=1) self.client.force_authenticate(self.s) r = self.client.post( f"/api/courses/{self.course.slug}/checkout/", {"promo_code": "ONCE"}, format="json", ) self.assertEqual(r.status_code, 400) def test_promo_scoped_to_other_course_400(self): other = _paid_course(self.t, price=500_000) other.slug = "other"; other.save() PromoCode.objects.create(code="OC10", discount_percent=10, course=other) self.client.force_authenticate(self.s) r = self.client.post( f"/api/courses/{self.course.slug}/checkout/", {"promo_code": "OC10"}, format="json", ) self.assertEqual(r.status_code, 400) def test_promo_used_count_bumped_on_paid(self): pc = PromoCode.objects.create(code="W10", discount_percent=10) self.client.force_authenticate(self.s) co = self.client.post(f"/api/courses/{self.course.slug}/checkout/", {"promo_code": "W10"}, format="json").json() self.client.get( f"/api/payments/callback/console/?order_id={co['order_id']}&Status=OK&Authority=CONSOLE-{co['order_id']}" ) pc.refresh_from_db() self.assertEqual(pc.used_count, 1) def test_full_discount_skips_gateway(self): PromoCode.objects.create(code="FREE100", discount_percent=100) self.client.force_authenticate(self.s) r = self.client.post( f"/api/courses/{self.course.slug}/checkout/", {"promo_code": "FREE100"}, format="json", ) self.assertEqual(r.status_code, 201) body = r.json() self.assertEqual(body["amount_toman"], 0) self.assertEqual(body["gateway"], "free") order = Order.objects.get(pk=body["order_id"]) self.assertEqual(order.status, OrderStatus.PAID) self.assertTrue( Enrollment.objects.filter(student=self.s, course=self.course).exists() ) # ==================================================================== referral codes @override_settings(PAYMENT_BACKEND=CONSOLE, REFERRAL_BUYER_DISCOUNT_PERCENT=10, REFERRAL_REFERRER_CREDIT_PERCENT=10) class ReferralCheckoutTests(TestCase): def setUp(self): self.client = APIClient() self.t = _teacher() self.s = _student() self.referrer = _student(phone="09120000050") self.course = _paid_course(self.t, price=1_000_000) def test_referral_applies_buyer_discount(self): self.client.force_authenticate(self.s) r = self.client.post( f"/api/courses/{self.course.slug}/checkout/", {"referral_code": self.referrer.referral_code}, format="json", ) self.assertEqual(r.status_code, 201, r.content) order = Order.objects.get(pk=r.json()["order_id"]) self.assertEqual(order.discount_amount_toman, 100_000) self.assertEqual(order.amount_toman, 900_000) self.assertEqual(order.referrer_id, self.referrer.id) def test_self_referral_rejected(self): self.client.force_authenticate(self.s) r = self.client.post( f"/api/courses/{self.course.slug}/checkout/", {"referral_code": self.s.referral_code}, format="json", ) self.assertEqual(r.status_code, 400) def test_promo_and_referral_together_rejected(self): PromoCode.objects.create(code="X", discount_percent=10) self.client.force_authenticate(self.s) r = self.client.post( f"/api/courses/{self.course.slug}/checkout/", {"promo_code": "X", "referral_code": self.referrer.referral_code}, format="json", ) self.assertEqual(r.status_code, 400) def test_referral_row_created_on_paid(self): self.client.force_authenticate(self.s) co = self.client.post( f"/api/courses/{self.course.slug}/checkout/", {"referral_code": self.referrer.referral_code}, format="json", ).json() self.client.get( f"/api/payments/callback/console/?order_id={co['order_id']}&Status=OK&Authority=CONSOLE-{co['order_id']}" ) ref = Referral.objects.get(order_id=co["order_id"]) self.assertEqual(ref.referrer_id, self.referrer.id) self.assertEqual(ref.buyer_id, self.s.id) self.assertEqual(ref.buyer_discount_toman, 100_000) # 10% of 900_000 = 90_000 self.assertEqual(ref.referrer_credit_toman, 90_000) # ==================================================================== preview @override_settings(PAYMENT_BACKEND=CONSOLE) class CheckoutPreviewTests(TestCase): def setUp(self): self.client = APIClient() self.t = _teacher() self.s = _student() self.course = _paid_course(self.t, price=1_000_000) def test_preview_with_promo_returns_discount(self): PromoCode.objects.create(code="W10", discount_percent=10) self.client.force_authenticate(self.s) r = self.client.post( f"/api/courses/{self.course.slug}/checkout/preview/", {"promo_code": "W10"}, format="json", ) self.assertEqual(r.status_code, 200, r.content) body = r.json() self.assertEqual(body["base_amount_toman"], 1_000_000) self.assertEqual(body["discount_amount_toman"], 100_000) self.assertEqual(body["final_amount_toman"], 900_000) self.assertEqual(body["promo_code_applied"], "W10") # Preview must NOT have created an Order self.assertFalse(Order.objects.exists()) def test_preview_with_invalid_promo_400(self): self.client.force_authenticate(self.s) r = self.client.post( f"/api/courses/{self.course.slug}/checkout/preview/", {"promo_code": "BAD"}, format="json", ) self.assertEqual(r.status_code, 400) # ==================================================================== discount countdown class DiscountCountdownTests(TestCase): def setUp(self): self.client = APIClient() self.t = _teacher() def test_discount_active_when_in_window(self): c = Course.objects.create( instructor=self.t, title="C", tagline="", description="", level="beginner", language="fa", price_toman=80, original_price_toman=100, discount_starts_at=timezone.now() - timedelta(hours=1), discount_ends_at=timezone.now() + timedelta(hours=1), ) c.publish() r = self.client.get(f"/api/courses/{c.slug}/") body = r.json() self.assertTrue(body["discount_active"]) self.assertIsNotNone(body["discount_ends_at"]) def test_discount_inactive_after_window(self): c = Course.objects.create( instructor=self.t, title="C2", tagline="", description="", level="beginner", language="fa", price_toman=80, original_price_toman=100, discount_ends_at=timezone.now() - timedelta(hours=1), ) c.publish() r = self.client.get(f"/api/courses/{c.slug}/") self.assertFalse(r.json()["discount_active"]) def test_discount_inactive_when_no_original_price(self): c = Course.objects.create( instructor=self.t, title="C3", tagline="", description="", level="beginner", language="fa", price_toman=100, ) c.publish() r = self.client.get(f"/api/courses/{c.slug}/") self.assertFalse(r.json()["discount_active"]) # ==================================================================== featured instructors filter class FeaturedInstructorsTests(TestCase): def test_is_featured_filter(self): client = APIClient() a = _teacher(phone="09120000001") b = _teacher(phone="09120000002") a.teacher_profile.is_featured = True a.teacher_profile.save() r = client.get("/api/instructors/?is_featured=true") ids = [row["id"] for row in r.json()] self.assertIn(a.id, ids) self.assertNotIn(b.id, ids) # ==================================================================== global search class GlobalSearchTests(TestCase): def setUp(self): self.client = APIClient() self.t = _teacher() Course.objects.create( instructor=self.t, title="Python Basics", tagline="", description="", level="beginner", language="fa", price_toman=None, ).publish() Course.objects.create( instructor=self.t, title="Persian Typography", tagline="", description="", level="beginner", language="fa", ).publish() def test_missing_q_400(self): r = self.client.get("/api/search/") self.assertEqual(r.status_code, 400) def test_search_courses_default(self): r = self.client.get("/api/search/?q=Python") self.assertEqual(r.status_code, 200) body = r.json() self.assertIn("courses", body) titles = [c["title"] for c in body["courses"]] self.assertIn("Python Basics", titles) def test_search_instructors_only(self): r = self.client.get("/api/search/?q=Teacher&types=instructors") body = r.json() self.assertNotIn("courses", body) self.assertIn("instructors", body) # ==================================================================== IDPay backend class IDPayBackendUnitTests(TestCase): """Unit-test IDPay's start/verify with mocked HTTP — we can't reach api.idpay.ir from dev.""" @override_settings(IDPAY_API_KEY="fake-key", IDPAY_SANDBOX=True) def test_start_stores_authority_and_returns_link(self): from unittest.mock import patch from apps.payments.services.gateway import IDPayBackend t = _teacher() s = _student() course = _paid_course(t, price=1000) order = Order.objects.create( student=s, course=course, base_amount_toman=1000, amount_toman=1000, gateway="idpay", status=OrderStatus.PENDING, ) with patch("apps.payments.services.gateway.requests.post") as mock_post: mock_post.return_value.status_code = 200 mock_post.return_value.json.return_value = {"id": "FAKE-AUTH", "link": "https://idpay.ir/p/ws/FAKE-AUTH"} backend = IDPayBackend() redirect = backend.start(order, "https://example/cb") order.refresh_from_db() self.assertEqual(order.authority, "FAKE-AUTH") self.assertEqual(redirect, "https://idpay.ir/p/ws/FAKE-AUTH") @override_settings(IDPAY_API_KEY="fake-key", IDPAY_SANDBOX=True) def test_verify_success(self): from unittest.mock import patch from apps.payments.services.gateway import IDPayBackend t = _teacher() s = _student() course = _paid_course(t, price=1000) order = Order.objects.create( student=s, course=course, base_amount_toman=1000, amount_toman=1000, gateway="idpay", status=OrderStatus.PENDING, authority="AUTH-1", ) with patch("apps.payments.services.gateway.requests.post") as mock_post: mock_post.return_value.status_code = 200 mock_post.return_value.json.return_value = {"status": 100, "track_id": "TRACK-1"} backend = IDPayBackend() ok, ref_id, err = backend.verify(order) self.assertTrue(ok) self.assertEqual(ref_id, "TRACK-1") self.assertIsNone(err) @override_settings(IDPAY_API_KEY="") def test_init_without_key_fails(self): from apps.payments.services.gateway import IDPayBackend, PaymentGatewayError with self.assertRaises(PaymentGatewayError): IDPayBackend()