This commit is contained in:
@@ -233,6 +233,13 @@ class CDEKClient:
|
||||
url,
|
||||
failure_message="CDEK get order failed",
|
||||
)
|
||||
requests_with_errors = _extract_requests_with_errors(raw_payload)
|
||||
if requests_with_errors:
|
||||
log.warning(
|
||||
"cdek_order_request_errors",
|
||||
cdek_order_uuid=cdek_order_uuid,
|
||||
requests=requests_with_errors,
|
||||
)
|
||||
try:
|
||||
return map_cdek_order_info_response(raw_payload)
|
||||
except CDEKOrderMappingError as exc:
|
||||
@@ -513,3 +520,28 @@ def _response_text_or_none(response: httpx.Response) -> str | None:
|
||||
return response.text
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _extract_requests_with_errors(
|
||||
payload: dict[str, Any],
|
||||
) -> list[dict[str, object]]:
|
||||
requests = payload.get("requests")
|
||||
if not isinstance(requests, list):
|
||||
return []
|
||||
|
||||
requests_with_errors: list[dict[str, object]] = []
|
||||
for request in requests:
|
||||
if not isinstance(request, dict):
|
||||
continue
|
||||
errors = request.get("errors")
|
||||
if not isinstance(errors, list) or not errors:
|
||||
continue
|
||||
requests_with_errors.append(
|
||||
{
|
||||
"request_uuid": request.get("request_uuid"),
|
||||
"type": request.get("type"),
|
||||
"state": request.get("state"),
|
||||
"errors": errors,
|
||||
}
|
||||
)
|
||||
return requests_with_errors
|
||||
|
||||
@@ -17,3 +17,40 @@ def resolve_tbank_payment_notification_action(
|
||||
if status == "CONFIRMED" and success is True and error_code == "0":
|
||||
return TBankPaymentNotificationAction.REGISTER_CDEK_ORDER
|
||||
return TBankPaymentNotificationAction.ACKNOWLEDGE_ONLY
|
||||
|
||||
|
||||
# Монотонный приоритет статусов платежа TBank по жизненному циклу.
|
||||
# Используется, чтобы внеочередное уведомление не понижало уже записанный статус
|
||||
# (например, AUTHORIZED, пришедший после CONFIRMED, не должен затирать CONFIRMED).
|
||||
# Неизвестные статусы получают ранг 0 и не перезаписывают известный статус.
|
||||
_TBANK_PAYMENT_STATUS_RANK: dict[str, int] = {
|
||||
"NEW": 10,
|
||||
"FORM_SHOWED": 20,
|
||||
"AUTHORIZING": 30,
|
||||
"3DS_CHECKING": 30,
|
||||
"3DS_CHECKED": 30,
|
||||
"REJECTED": 40,
|
||||
"AUTH_FAIL": 40,
|
||||
"DEADLINE_EXPIRED": 40,
|
||||
"AUTHORIZED": 40,
|
||||
"CONFIRMING": 50,
|
||||
"CONFIRMED": 60,
|
||||
"REVERSING": 70,
|
||||
"PARTIAL_REVERSED": 70,
|
||||
"REVERSED": 70,
|
||||
"REFUNDING": 70,
|
||||
"PARTIAL_REFUNDED": 70,
|
||||
"REFUNDED": 70,
|
||||
"CANCELED": 70,
|
||||
}
|
||||
|
||||
|
||||
def tbank_payment_status_rank(status: str) -> int:
|
||||
return _TBANK_PAYMENT_STATUS_RANK.get(status, 0)
|
||||
|
||||
|
||||
def should_apply_tbank_payment_status(current: str | None, new: str) -> bool:
|
||||
"""Применять новый статус, только если он не понижает текущий по жизненному циклу."""
|
||||
if current is None:
|
||||
return True
|
||||
return tbank_payment_status_rank(new) >= tbank_payment_status_rank(current)
|
||||
|
||||
@@ -50,10 +50,13 @@ class OrderRepository:
|
||||
self,
|
||||
session: AsyncSession,
|
||||
order_uuid: str,
|
||||
*,
|
||||
for_update: bool = False,
|
||||
) -> Order | None:
|
||||
result = await session.execute(
|
||||
select(Order).where(Order.order_uuid == order_uuid)
|
||||
)
|
||||
statement = select(Order).where(Order.order_uuid == order_uuid)
|
||||
if for_update:
|
||||
statement = statement.with_for_update()
|
||||
result = await session.execute(statement)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def mark_payment_status(
|
||||
|
||||
+18
-12
@@ -30,6 +30,7 @@ from app.adapters.tbank.base import (
|
||||
from app.domain.payment_notifications import (
|
||||
TBankPaymentNotificationAction,
|
||||
resolve_tbank_payment_notification_action,
|
||||
should_apply_tbank_payment_status,
|
||||
)
|
||||
from app.domain.price import (
|
||||
DEFAULT_PROVIDER_PRICE_MULTIPLIER,
|
||||
@@ -537,9 +538,12 @@ class AggregatorService:
|
||||
|
||||
try:
|
||||
async with self._order_repository.session() as session:
|
||||
# FOR UPDATE: сериализуем конкурентные уведомления по одному заказу,
|
||||
# чтобы внеочередной статус не затирал уже записанный (lost update).
|
||||
order = await self._order_repository.get_order_by_order_uuid(
|
||||
session,
|
||||
notification.OrderId,
|
||||
for_update=True,
|
||||
)
|
||||
if order is None:
|
||||
logger.warning(
|
||||
@@ -549,19 +553,21 @@ class AggregatorService:
|
||||
raise TBankPaymentNotificationProcessingError(
|
||||
"Order was not found for TBank payment notification."
|
||||
)
|
||||
updated_order = await self._order_repository.mark_payment_status(
|
||||
session,
|
||||
notification.OrderId,
|
||||
notification.Status,
|
||||
notification.PaymentId,
|
||||
)
|
||||
if updated_order is None:
|
||||
logger.warning(
|
||||
"tbank_payment_notification_order_not_found",
|
||||
order_uuid=notification.OrderId,
|
||||
if should_apply_tbank_payment_status(
|
||||
order.payment_status, notification.Status
|
||||
):
|
||||
await self._order_repository.mark_payment_status(
|
||||
session,
|
||||
notification.OrderId,
|
||||
notification.Status,
|
||||
notification.PaymentId,
|
||||
)
|
||||
raise TBankPaymentNotificationProcessingError(
|
||||
"Order was not found for TBank payment notification."
|
||||
else:
|
||||
logger.info(
|
||||
"tbank_payment_status_downgrade_skipped",
|
||||
order_uuid=notification.OrderId,
|
||||
current_status=order.payment_status,
|
||||
incoming_status=notification.Status,
|
||||
)
|
||||
return order
|
||||
except TBankPaymentNotificationProcessingError:
|
||||
|
||||
Reference in New Issue
Block a user