470 lines
24 KiB
Python
470 lines
24 KiB
Python
import pytest
|
|
import json
|
|
import redis
|
|
from datetime import timedelta
|
|
from kombu.exceptions import OperationalError
|
|
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_create_payment_reuses_an_active_attempt(self, mocker):
|
|
gateway = mocker.patch('apps.payments.api.views.requests.post')
|
|
gateway.return_value.json.return_value = {'data': {'code': 100, 'authority': 'AUTH-REUSED'}}
|
|
first = self.client.post(
|
|
'/api/payments/create',
|
|
data=json.dumps({
|
|
'event_id': self.event.id,
|
|
'description': 'First tap',
|
|
'idempotency_key': 'b0b849d1-8d8f-4f0d-94b6-7b60876f1501',
|
|
}),
|
|
content_type='application/json',
|
|
**self._headers(),
|
|
)
|
|
second = self.client.post(
|
|
'/api/payments/create',
|
|
data=json.dumps({
|
|
'event_id': self.event.id,
|
|
'description': 'Repeated tap',
|
|
'idempotency_key': 'b5f06335-1301-48c2-b504-f009c8e087d3',
|
|
}),
|
|
content_type='application/json',
|
|
**self._headers(),
|
|
)
|
|
|
|
assert first.status_code == second.status_code == 200
|
|
assert first.json()['payment_id'] == second.json()['payment_id']
|
|
assert first.json()['start_pay_url'] == second.json()['start_pay_url']
|
|
assert Payment.objects.filter(user=self.user, event=self.event).count() == 1
|
|
gateway.assert_called_once()
|
|
|
|
def test_create_payment_replays_same_key_after_gateway_timeout(self, mocker):
|
|
gateway = mocker.patch('apps.payments.api.views.requests.post', side_effect=RuntimeError('timeout'))
|
|
payload = {
|
|
'event_id': self.event.id,
|
|
'description': 'Checkout retry',
|
|
'idempotency_key': 'ed7e1426-320d-4faf-b808-7ad63e73477c',
|
|
}
|
|
|
|
first = self.client.post('/api/payments/create', data=json.dumps(payload), content_type='application/json', **self._headers())
|
|
second = self.client.post('/api/payments/create', data=json.dumps(payload), content_type='application/json', **self._headers())
|
|
|
|
assert first.status_code == second.status_code == 200
|
|
assert first.json()['status'] == second.json()['status'] == 'pending'
|
|
assert first.json()['payment_id'] == second.json()['payment_id']
|
|
assert Payment.objects.filter(user=self.user, event=self.event).count() == 1
|
|
gateway.assert_called_once()
|
|
|
|
def test_gateway_server_error_is_not_treated_as_a_definitive_failure(self, mocker):
|
|
gateway_response = mocker.Mock(status_code=503)
|
|
gateway_response.json.return_value = {"errors": {"code": -9}}
|
|
mocker.patch("apps.payments.api.views.requests.post", return_value=gateway_response)
|
|
|
|
response = self.client.post(
|
|
"/api/payments/create",
|
|
data=json.dumps({"event_id": self.event.id, "description": "Gateway server error"}),
|
|
content_type="application/json",
|
|
**self._headers(),
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["status"] == "pending"
|
|
assert Payment.objects.filter(
|
|
user=self.user,
|
|
event=self.event,
|
|
status=Payment.OrderStatusChoices.PENDING,
|
|
authority__isnull=True,
|
|
).exists()
|
|
|
|
def test_definitive_gateway_rejection_allows_a_new_attempt(self, mocker):
|
|
gateway = mocker.patch('apps.payments.api.views.requests.post')
|
|
gateway.side_effect = [
|
|
mocker.Mock(**{'json.return_value': {'errors': {'code': -9}}}),
|
|
mocker.Mock(**{'json.return_value': {'data': {'code': 100, 'authority': 'AUTH-RETRY'}}}),
|
|
]
|
|
rejected = self.client.post(
|
|
'/api/payments/create',
|
|
data=json.dumps({
|
|
'event_id': self.event.id,
|
|
'description': 'Rejected attempt',
|
|
'idempotency_key': '060826cf-b8c0-4828-b0f2-d37c0f2678dd',
|
|
}),
|
|
content_type='application/json',
|
|
**self._headers(),
|
|
)
|
|
retried = self.client.post(
|
|
'/api/payments/create',
|
|
data=json.dumps({
|
|
'event_id': self.event.id,
|
|
'description': 'New attempt',
|
|
'idempotency_key': '865f3c43-2888-4fd0-8706-1c2ae60cb5f6',
|
|
}),
|
|
content_type='application/json',
|
|
**self._headers(),
|
|
)
|
|
|
|
assert rejected.status_code == retried.status_code == 200
|
|
assert rejected.json()['status'] == 'failed'
|
|
assert retried.json()['status'] == 'pending'
|
|
assert Payment.objects.filter(user=self.user, event=self.event).count() == 2
|
|
assert Payment.objects.filter(user=self.user, event=self.event, status=Payment.OrderStatusChoices.FAILED).count() == 1
|
|
assert gateway.call_count == 2
|
|
|
|
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 f'/events/{self.event.slug}/registration?payment_id={payment.id}' in response.url
|
|
assert 'status=' not in response.url
|
|
|
|
def test_callback_redirects_if_payment_notification_redis_is_unavailable(self, mocker):
|
|
mock_post = mocker.patch('apps.payments.api.views.requests.post')
|
|
self.user.is_mobile_verified = True
|
|
self.user.save(update_fields=['is_mobile_verified'])
|
|
payment = Payment.objects.create(
|
|
user=self.user,
|
|
event=self.event,
|
|
base_amount=self.event.price,
|
|
amount=self.event.price,
|
|
status=Payment.OrderStatusChoices.PENDING,
|
|
authority='AUTH-REDIS-DOWN',
|
|
)
|
|
mock_post.return_value.json.return_value = {
|
|
'data': {'code': 100, 'ref_id': 'REF-REDIS-DOWN'}
|
|
}
|
|
mocker.patch(
|
|
'apps.payments.api.views.notify_user',
|
|
side_effect=redis.exceptions.ConnectionError('Redis unavailable'),
|
|
)
|
|
mocker.patch(
|
|
'apps.payments.api.views.send_critical_sms.delay',
|
|
side_effect=OperationalError('Celery broker unavailable'),
|
|
)
|
|
|
|
response = self.client.get(
|
|
'/api/payments/callback',
|
|
{'Authority': payment.authority, 'Status': 'OK'},
|
|
)
|
|
|
|
payment.refresh_from_db()
|
|
assert response.status_code == 302
|
|
assert f'/events/{self.event.slug}/registration?payment_id={payment.id}' in response.url
|
|
assert payment.status == Payment.OrderStatusChoices.PAID
|
|
|
|
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 f'/events/{self.event.slug}/registration?payment_id={payment.id}' in response.url
|
|
|
|
def test_callback_missing_authority_redirects_to_safe_unknown_result(self):
|
|
response = self.client.get('/api/payments/callback', {'Status': 'OK'})
|
|
assert response.status_code == 302
|
|
assert '/events?payment_callback=unmatched' in response.url
|
|
|
|
def test_callback_unknown_authority_redirects_to_safe_unknown_result(self):
|
|
response = self.client.get(
|
|
'/api/payments/callback',
|
|
{'Authority': 'AUTH-UNKNOWN', 'Status': 'OK'},
|
|
)
|
|
assert response.status_code == 302
|
|
assert '/events?payment_callback=unmatched' in response.url
|
|
assert 'AUTH-UNKNOWN' not in response.url
|
|
|
|
def test_callback_not_ok_is_verified_before_being_canceled(self, mocker):
|
|
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')
|
|
gateway = mocker.patch('apps.payments.api.views.requests.post')
|
|
gateway.return_value.json.return_value = {'data': {'code': -51}}
|
|
response = self.client.get('/api/payments/callback', {'Authority': 'AUTH789', 'Status': 'NOK'})
|
|
payment.refresh_from_db()
|
|
assert payment.status == Payment.OrderStatusChoices.CANCELED
|
|
assert f'/events/{self.event.slug}/registration?payment_id={payment.id}' in response.url
|
|
|
|
def test_late_canceled_callback_does_not_overwrite_verified_payment(self, mocker):
|
|
registration = Registration.objects.create(
|
|
event=self.event,
|
|
user=self.user,
|
|
status=Registration.StatusChoices.PENDING,
|
|
final_price=self.event.price,
|
|
)
|
|
payment = Payment.objects.create(
|
|
user=self.user,
|
|
event=self.event,
|
|
registration=registration,
|
|
base_amount=self.event.price,
|
|
amount=self.event.price,
|
|
status=Payment.OrderStatusChoices.PENDING,
|
|
authority='AUTH-LATE-NOK',
|
|
)
|
|
mock_post = mocker.patch('apps.payments.api.views.requests.post')
|
|
mock_response = mocker.Mock()
|
|
mock_response.json.return_value = {'data': {'code': 100, 'ref_id': 'REF-LATE-NOK'}}
|
|
mock_post.return_value = mock_response
|
|
|
|
success_response = self.client.get(
|
|
'/api/payments/callback',
|
|
{'Authority': payment.authority, 'Status': 'OK'},
|
|
)
|
|
late_failure_response = self.client.get(
|
|
'/api/payments/callback',
|
|
{'Authority': payment.authority, 'Status': 'NOK'},
|
|
)
|
|
|
|
payment.refresh_from_db()
|
|
registration.refresh_from_db()
|
|
assert payment.status == Payment.OrderStatusChoices.PAID
|
|
assert payment.verified_at is not None
|
|
assert registration.status == Registration.StatusChoices.CONFIRMED
|
|
expected_return = f'/events/{self.event.slug}/registration?registration_id={registration.ticket_id}'
|
|
assert expected_return in success_response.url
|
|
assert expected_return in late_failure_response.url
|
|
|
|
def test_create_payment_gateway_timeout_stays_pending(self, mocker):
|
|
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 == 200
|
|
assert response.json()['status'] == 'pending'
|
|
assert Payment.objects.filter(user=self.user, event=self.event, status=Payment.OrderStatusChoices.PENDING).exists()
|
|
|
|
def test_verification_timeout_stays_pending_and_queues_reconciliation(self, mocker):
|
|
payment = Payment.objects.create(user=self.user, event=self.event, base_amount=self.event.price, amount=self.event.price, status=Payment.OrderStatusChoices.PENDING, authority='AUTH-TIMEOUT')
|
|
mocker.patch('apps.payments.api.views.requests.post', side_effect=RuntimeError('gateway timeout'))
|
|
reconcile = mocker.patch('apps.payments.api.views.reconcile_payment.apply_async')
|
|
notify = mocker.patch('apps.payments.api.views.notify_user')
|
|
|
|
response = self.client.get('/api/payments/callback', {'Authority': payment.authority, 'Status': 'OK'})
|
|
|
|
payment.refresh_from_db()
|
|
assert response.status_code == 302
|
|
assert f'/events/{self.event.slug}/registration?payment_id={payment.id}' in response.url
|
|
assert payment.status == Payment.OrderStatusChoices.PENDING
|
|
reconcile.assert_called_once_with(args=(payment.id,), countdown=60)
|
|
notify.assert_not_called()
|
|
|
|
def test_payment_status_is_user_scoped_and_reflects_saved_state(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='AUTH-STATUS')
|
|
own = self.client.get(f'/api/payments/status/{payment.id}', **self._headers())
|
|
assert own.status_code == 200
|
|
assert own.json()['payment_id'] == payment.id
|
|
assert own.json()['status'] == 'pending'
|
|
assert own.json()['verification_state'] == 'awaiting_verification'
|
|
assert own.json()['verification_attempts'] == 0
|
|
assert own.json()['last_verification_at'] is None
|
|
assert own.json()['manual_review_at'] is None
|
|
|
|
other = User.objects.create_user(username='other_payment_owner', email='other-payment@example.com', mobile='09123456780', password=self.password)
|
|
other_token = create_jwt_token(other)
|
|
forbidden = self.client.get(f'/api/payments/status/{payment.id}', HTTP_AUTHORIZATION=f'Bearer {other_token}')
|
|
assert forbidden.status_code == 404
|
|
|
|
def test_payment_reference_lookup_requires_authentication_and_is_user_scoped(self):
|
|
payment = Payment.objects.create(
|
|
user=self.user,
|
|
event=self.event,
|
|
base_amount=self.event.price,
|
|
amount=self.event.price,
|
|
status=Payment.OrderStatusChoices.PAID,
|
|
ref_id="OWNER-ONLY-REF",
|
|
)
|
|
|
|
unauthenticated = self.client.get("/api/payments/by-ref/OWNER-ONLY-REF")
|
|
owned = self.client.get("/api/payments/by-ref/OWNER-ONLY-REF", **self._headers())
|
|
other = User.objects.create_user(
|
|
username="other_ref_owner",
|
|
email="other-ref-owner@example.com",
|
|
mobile="09123456781",
|
|
password=self.password,
|
|
)
|
|
other_token = create_jwt_token(other)
|
|
forbidden = self.client.get(
|
|
"/api/payments/by-ref/OWNER-ONLY-REF",
|
|
HTTP_AUTHORIZATION=f"Bearer {other_token}",
|
|
)
|
|
|
|
assert unauthenticated.status_code == 401
|
|
assert owned.status_code == 200
|
|
assert owned.json()["payment_id"] == payment.id
|
|
assert forbidden.status_code == 404
|
|
|
|
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]["event"]["id"] == own_event.id
|
|
assert payload[0]["amount"] == 100000
|
|
assert payload[0]["discount_amount"] == 20000
|
|
assert payload[0]["status"] == "paid"
|
|
assert set(payload[0]) == {"payment_id", "event", "base_amount", "discount_amount", "amount", "status", "refund_status", "created_at", "paid_at"}
|
|
assert payload[0]["payment_id"] == Payment.objects.get(user=current_user, event=own_event).id
|
|
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]
|