import pytest import json from datetime import timedelta from django.utils import timezone from core.authentication import create_jwt_token from apps.events.models import Event, Registration from apps.payments.models import Payment, DiscountCode from apps.users.models import User @pytest.mark.django_db @pytest.mark.integration class TestPaymentsAPIIntegrationTests: password = 'PaymentPass!123' @classmethod def setUpTestData(cls): cls.user = User.objects.create_user(username='pay_user', email='pay.user@example.com', mobile='09123456789', password=cls.password) cls.user.is_email_verified = True cls.user.save(update_fields=['is_email_verified']) def setup_method(self): type(self).setUpTestData() self.event = Event.objects.create(title='Pay Event', description='Payment event', start_time=timezone.now(), end_time=timezone.now() + timedelta(hours=2), registration_start_date=timezone.now() - timedelta(days=1), registration_end_date=timezone.now() + timedelta(days=1), slug='pay-event', price=50000, capacity=10, status=Event.StatusChoices.PUBLISHED) self.token = create_jwt_token(self.user) def _headers(self): return {'HTTP_AUTHORIZATION': f'Bearer {self.token}'} def _create_paid_event(self): return Event.objects.create(title='Paid Event', description='Paid', start_time=timezone.now(), end_time=timezone.now() + timedelta(hours=1), registration_start_date=timezone.now() - timedelta(days=1), registration_end_date=timezone.now() + timedelta(days=2), slug=f'paid-{timezone.now().timestamp()}', price=20000, capacity=5, status=Event.StatusChoices.PUBLISHED) def _create_discount_code(self, event): code = DiscountCode.objects.create(code='DISC50', value=50, is_active=True) code.applicable_events.add(event) return code def test_create_payment_for_free_event(self): free = Event.objects.create(title='Free', description='Zero', start_time=timezone.now(), end_time=timezone.now() + timedelta(hours=1), registration_start_date=timezone.now() - timedelta(days=1), registration_end_date=timezone.now() + timedelta(days=1), slug='free-event', price=0, capacity=10, status=Event.StatusChoices.PUBLISHED) response = self.client.post('/api/payments/create', data=json.dumps({'event_id': free.id, 'description': 'Free registration'}), content_type='application/json', **self._headers()) assert response.status_code == 200 data = response.json() assert data['amount'] == 0 assert data['start_pay_url'] is None def test_create_payment_with_discount(self, mocker): mock_post = mocker.patch('apps.payments.api.views.requests.post') mock_response = mocker.Mock() mock_response.json.return_value = {'data': {'code': 100, 'authority': 'AUTH'}} mock_post.return_value = mock_response code = self._create_discount_code(self.event) response = self.client.post('/api/payments/create', data=json.dumps({'event_id': self.event.id, 'description': 'Pay with discount', 'discount_code': code.code}), content_type='application/json', **self._headers()) assert response.status_code == 200 payload = response.json() assert payload['discount_amount'] == self.event.price // 2 assert payload['amount'] == self.event.price // 2 assert 'start_pay_url' in payload payment = Payment.objects.get(user=self.user, event=self.event) assert payment.discount_code == code def test_callback_success_marks_paid(self, mocker): mock_post = mocker.patch('apps.payments.api.views.requests.post') payment = Payment.objects.create(user=self.user, event=self.event, base_amount=self.event.price, amount=self.event.price, status=Payment.OrderStatusChoices.PENDING, authority='AUTH123') mock_resp = mocker.Mock() mock_resp.json.return_value = {'data': {'code': 100, 'ref_id': 'REF', 'card_pan': '123', 'card_hash': 'ABC'}} mock_post.return_value = mock_resp response = self.client.get('/api/payments/callback', {'Authority': 'AUTH123', 'Status': 'OK'}) payment.refresh_from_db() assert payment.status == Payment.OrderStatusChoices.PAID assert 'status=success' in response.url def test_callback_failure_redirects_failed(self, mocker): mock_post = mocker.patch('apps.payments.api.views.requests.post') payment = Payment.objects.create(user=self.user, event=self.event, base_amount=self.event.price, amount=self.event.price, status=Payment.OrderStatusChoices.PENDING, authority='AUTH456') mock_resp = mocker.Mock() mock_resp.json.return_value = {'data': {'code': 101, 'ref_id': 'REF'}} mock_post.return_value = mock_resp response = self.client.get('/api/payments/callback', {'Authority': 'AUTH456', 'Status': 'OK'}) payment.refresh_from_db() assert payment.status == Payment.OrderStatusChoices.PAID assert 'status=success' in response.url def test_callback_missing_authority_returns_error(self): response = self.client.get('/api/payments/callback', {'Status': 'OK'}) assert response.status_code == 400 def test_callback_not_ok_cancels(self): payment = Payment.objects.create(user=self.user, event=self.event, base_amount=self.event.price, amount=self.event.price, status=Payment.OrderStatusChoices.PENDING, authority='AUTH789') response = self.client.get('/api/payments/callback', {'Authority': 'AUTH789', 'Status': 'NOK'}) payment.refresh_from_db() assert payment.status == Payment.OrderStatusChoices.CANCELED assert 'status=failed' in response.url def test_create_payment_gateway_failure(self, mocker): mock_post = mocker.patch('apps.payments.api.views.requests.post', side_effect=RuntimeError('down')) response = self.client.post('/api/payments/create', data=json.dumps({'event_id': self.event.id, 'description': 'Gateway fail'}), content_type='application/json', **self._headers()) assert response.status_code == 502 assert not Payment.objects.filter(user=self.user).exists() def test_create_payment_when_already_paid(self): Payment.objects.create(user=self.user, event=self.event, base_amount=self.event.price, amount=self.event.price, status=Payment.OrderStatusChoices.PAID) response = self.client.post('/api/payments/create', data=json.dumps({'event_id': self.event.id, 'description': 'Duplicate'}), content_type='application/json', **self._headers()) assert response.status_code == 400 def test_registration_final_price_none_updates(self, mocker): mock_post = mocker.patch('apps.payments.api.views.requests.post') registration = Registration.objects.create(event=self.event, user=self.user, status=Registration.StatusChoices.PENDING, final_price=None) mock_response = mocker.Mock() mock_response.json.return_value = {'data': {'code': 100, 'authority': 'AUTH'}} mock_post.return_value = mock_response response = self.client.post('/api/payments/create', data=json.dumps({'event_id': self.event.id, 'description': 'Update'}), content_type='application/json', **self._headers()) assert response.status_code == 200 registration.refresh_from_db() if registration.final_price is None: pytest.fail('final_price should be populated') def test_coupon_check_success_and_errors(self): code = DiscountCode.objects.create(code='PAYCO', value=20, is_active=True, type=DiscountCode.Type.PERCENT) code.applicable_events.add(self.event) missing = self.client.post('/api/payments/coupon/check', data=json.dumps({'event_id': self.event.id}), content_type='application/json', **self._headers()) assert missing.status_code == 422 invalid = self.client.post('/api/payments/coupon/check', data=json.dumps({'event_id': self.event.id, 'code': 'INVALID'}), content_type='application/json', **self._headers()) assert invalid.status_code == 404 success = self.client.post('/api/payments/coupon/check', data=json.dumps({'event_id': self.event.id, 'code': code.code}), content_type='application/json', **self._headers()) assert success.status_code == 200 assert 'final_price' in success.json() @pytest.fixture(autouse=True) def _pytest_client(self, client): self.client = client @pytest.fixture(autouse=True) def _pytest_settings(self, settings): settings.ZARINPAL_MERCHANT_ID = 'MID' settings.ZARINPAL_REQUEST_URL = 'https://zarinpal/request' settings.ZARINPAL_STARTPAY = 'https://zarinpal/start/' settings.ZARINPAL_VERIFY_URL = 'https://zarinpal/verify' settings.ZARINPAL_CALLBACK_URL = 'https://frontend/callback' @pytest.mark.django_db @pytest.mark.integration def test_my_payments_are_user_scoped_and_omit_gateway_identifiers(client, user_factory, auth_headers, event_factory): current_user = user_factory(username="payment_history_owner") other_user = user_factory(username="payment_history_other") own_event = event_factory(slug="payment-history-own", title="My workshop") other_event = event_factory(slug="payment-history-other", title="Someone else's workshop") Payment.objects.create( user=current_user, event=own_event, base_amount=120000, discount_amount=20000, amount=100000, status=Payment.OrderStatusChoices.PAID, authority="internal-authority-owner", ref_id="internal-reference-owner", card_pan="6037-****-****-1234", card_hash="internal-card-hash", ) Payment.objects.create( user=other_user, event=other_event, base_amount=50000, amount=50000, status=Payment.OrderStatusChoices.PENDING, authority="internal-authority-other", ) response = client.get("/api/payments/me", **auth_headers(current_user)) assert response.status_code == 200, response.content payload = response.json() assert len(payload) == 1 assert payload[0]["event"]["title"] == own_event.title assert payload[0]["amount"] == 100000 assert payload[0]["discount_amount"] == 20000 assert payload[0]["status"] == "paid" assert set(payload[0]) == {"event", "base_amount", "discount_amount", "amount", "status", "created_at", "paid_at"} assert "internal-authority-owner" not in response.content.decode() assert "internal-reference-owner" not in response.content.decode() assert "internal-card-hash" not in response.content.decode() assert client.get("/api/payments/me").status_code == 401 @pytest.mark.django_db @pytest.mark.integration def test_my_payments_pagination_is_user_scoped(client, user_factory, auth_headers, event_factory): current_user = user_factory(username="payment_paging_owner") other_user = user_factory(username="payment_paging_other") event = event_factory(slug="payment-paging-event") for index in range(12): Payment.objects.create( user=current_user, event=event, base_amount=1000 + index, amount=1000 + index, status=Payment.OrderStatusChoices.PAID, ) Payment.objects.create( user=other_user, event=event, base_amount=9000, amount=9000, status=Payment.OrderStatusChoices.PAID, ) first_page = client.get("/api/payments/me/paginated", {"page": 1}, **auth_headers(current_user)) second_page = client.get("/api/payments/me/paginated", {"page": 2}, **auth_headers(current_user)) assert first_page.status_code == 200, first_page.content assert first_page.json()["count"] == 12 assert first_page.json()["page_size"] == 10 assert first_page.json()["total_pages"] == 2 assert len(first_page.json()["results"]) == 10 assert len(second_page.json()["results"]) == 2 assert all(item["event"]["slug"] == event.slug for item in first_page.json()["results"]) assert "authority" not in first_page.json()["results"][0]