fix(payments): keep callback redirect resilient to Redis
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user