diff --git a/apps/payments/api/views.py b/apps/payments/api/views.py index 687c7d5..9ba26a8 100644 --- a/apps/payments/api/views.py +++ b/apps/payments/api/views.py @@ -13,6 +13,8 @@ from typing import List from ninja import Query, Router from ninja.errors import HttpError import requests +import redis +from kombu.exceptions import OperationalError as KombuOperationalError from apps.payments.models import Payment, DiscountCode from apps.events.models import Event, Registration @@ -236,26 +238,38 @@ def _event_action_url(event: Event) -> str: def _notify_payment_status(payment: Payment, *, title: str, message: str, level: str): - notify_user( - payment.user_id, - { - "type": "payment_status", - "title": title, - "message": message, - "level": level, - "action_url": _event_action_url(payment.event), - "entity_type": "payment", - "entity_id": payment.id, - "meta": { - "event_id": payment.event_id, - "payment_id": payment.id, - "ref_id": payment.ref_id, - "status": payment.status, + try: + notify_user( + payment.user_id, + { + "type": "payment_status", + "title": title, + "message": message, + "level": level, + "action_url": _event_action_url(payment.event), + "entity_type": "payment", + "entity_id": payment.id, + "meta": { + "event_id": payment.event_id, + "payment_id": payment.id, + "ref_id": payment.ref_id, + "status": payment.status, + }, }, - }, - ) + ) + except redis.exceptions.RedisError: + logger.exception( + "Unable to store payment status notification for payment %s; payment state is already saved", + payment.id, + ) if payment.user.mobile and payment.user.is_mobile_verified: - send_critical_sms.delay(payment.user.mobile, "payment_status", payment.event.title) + try: + send_critical_sms.delay(payment.user.mobile, "payment_status", payment.event.title) + except (KombuOperationalError, redis.exceptions.RedisError): + logger.exception( + "Unable to queue payment status SMS for payment %s; payment state is already saved", + payment.id, + ) def _payment_status_name(payment: Payment) -> str: diff --git a/apps/payments/tests/integration/test_payments.py b/apps/payments/tests/integration/test_payments.py index 58c0300..49bacb7 100644 --- a/apps/payments/tests/integration/test_payments.py +++ b/apps/payments/tests/integration/test_payments.py @@ -1,6 +1,8 @@ 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 @@ -171,6 +173,40 @@ class TestPaymentsAPIIntegrationTests: 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')