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 import Query, Router
|
||||||
from ninja.errors import HttpError
|
from ninja.errors import HttpError
|
||||||
import requests
|
import requests
|
||||||
|
import redis
|
||||||
|
from kombu.exceptions import OperationalError as KombuOperationalError
|
||||||
|
|
||||||
from apps.payments.models import Payment, DiscountCode
|
from apps.payments.models import Payment, DiscountCode
|
||||||
from apps.events.models import Event, Registration
|
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):
|
def _notify_payment_status(payment: Payment, *, title: str, message: str, level: str):
|
||||||
notify_user(
|
try:
|
||||||
payment.user_id,
|
notify_user(
|
||||||
{
|
payment.user_id,
|
||||||
"type": "payment_status",
|
{
|
||||||
"title": title,
|
"type": "payment_status",
|
||||||
"message": message,
|
"title": title,
|
||||||
"level": level,
|
"message": message,
|
||||||
"action_url": _event_action_url(payment.event),
|
"level": level,
|
||||||
"entity_type": "payment",
|
"action_url": _event_action_url(payment.event),
|
||||||
"entity_id": payment.id,
|
"entity_type": "payment",
|
||||||
"meta": {
|
"entity_id": payment.id,
|
||||||
"event_id": payment.event_id,
|
"meta": {
|
||||||
"payment_id": payment.id,
|
"event_id": payment.event_id,
|
||||||
"ref_id": payment.ref_id,
|
"payment_id": payment.id,
|
||||||
"status": payment.status,
|
"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:
|
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:
|
def _payment_status_name(payment: Payment) -> str:
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import json
|
import json
|
||||||
|
import redis
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from kombu.exceptions import OperationalError
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from core.authentication import create_jwt_token
|
from core.authentication import create_jwt_token
|
||||||
from apps.events.models import Event, Registration
|
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 f'/events/{self.event.slug}/registration?payment_id={payment.id}' in response.url
|
||||||
assert 'status=' not 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):
|
def test_callback_failure_redirects_failed(self, mocker):
|
||||||
mock_post = mocker.patch('apps.payments.api.views.requests.post')
|
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')
|
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