Ilo/apps/payments/tests_phase6c.py
2026-05-02 20:01:30 +03:30

393 lines
16 KiB
Python

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()