fix(payments): keep callback redirect resilient to Redis

This commit is contained in:
2026-10-01 15:20:26 +03:30
parent f924fcc52e
commit efeacaebd0
2 changed files with 68 additions and 18 deletions

View File

@@ -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:

View File

@@ -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')