diff --git a/app/adapters/delivery_providers/cse/client.py b/app/adapters/delivery_providers/cse/client.py
index c28fa19..51a3f15 100644
--- a/app/adapters/delivery_providers/cse/client.py
+++ b/app/adapters/delivery_providers/cse/client.py
@@ -179,6 +179,7 @@ class CSEClient:
"cse_request_server_error",
operation=operation,
status_code=response.status_code,
+ response_excerpt=_response_excerpt(response.text),
)
raise CSEClientError(
f"CSE {operation} request failed with status "
@@ -233,6 +234,10 @@ class CSEClient:
return self._retry_backoff_seconds * (2**attempt)
+def _response_excerpt(text: str, *, limit: int = 1000) -> str:
+ return " ".join(text.split())[:limit]
+
+
class CSEProvider(DeliveryProvider):
name = CSE_PROVIDER_NAME
diff --git a/app/adapters/delivery_providers/cse/soap.py b/app/adapters/delivery_providers/cse/soap.py
index d4ca74f..c5b8574 100644
--- a/app/adapters/delivery_providers/cse/soap.py
+++ b/app/adapters/delivery_providers/cse/soap.py
@@ -1,9 +1,10 @@
"""SOAP (cargo3) Element serialization and parsing for CSE.
CSE exposes a SOAP/1C web service ("Карго") that transfers data through nested
-universal ``Element`` structures (``Key``/``Value``/``ValueType``/``Fields``/
-``List``/``Tables``/``Properties``). This module builds request envelopes and
-parses responses into a plain Python representation that mappers can navigate.
+universal ``Element`` structures (``Key``/``Value``/``ValueType``/
+``Properties``/``Fields``/``List``/``Tables``). This module builds request
+envelopes and parses responses into a plain Python representation that mappers
+can navigate.
"""
from __future__ import annotations
@@ -15,8 +16,6 @@ from xml.etree import ElementTree as ET
CARGO_NS = "http://www.cargo3.ru"
SOAP_NS = "http://www.w3.org/2003/05/soap-envelope"
-# Child tags of an Element that hold nested Element lists.
-_LIST_TAGS = ("Fields", "List", "Tables", "Properties")
_SCALAR_TAGS = ("Key", "Value", "ValueType")
_BINARY_TAG = "BData"
@@ -102,14 +101,14 @@ def _append_element(parent: ET.Element, tag: str, element: Element) -> None:
_scalar(node, "Value", element.value)
if element.value_type is not None:
_scalar(node, "ValueType", element.value_type)
+ for child in element.properties:
+ _append_element(node, "Properties", child)
for child in element.fields:
_append_element(node, "Fields", child)
for child in element.items:
_append_element(node, "List", child)
for child in element.tables:
_append_element(node, "Tables", child)
- for child in element.properties:
- _append_element(node, "Properties", child)
def _scalar(parent: ET.Element, tag: str, text: str) -> None:
diff --git a/app/domain/payment_notifications.py b/app/domain/payment_notifications.py
index c78b9bd..da2a68d 100644
--- a/app/domain/payment_notifications.py
+++ b/app/domain/payment_notifications.py
@@ -5,7 +5,7 @@ from enum import Enum
class TBankPaymentNotificationAction(str, Enum):
ACKNOWLEDGE_ONLY = "acknowledge_only"
- REGISTER_CDEK_ORDER = "register_cdek_order"
+ REGISTER_PROVIDER_ORDER = "register_provider_order"
def resolve_tbank_payment_notification_action(
@@ -15,7 +15,7 @@ def resolve_tbank_payment_notification_action(
error_code: str,
) -> TBankPaymentNotificationAction:
if status == "CONFIRMED" and success is True and error_code == "0":
- return TBankPaymentNotificationAction.REGISTER_CDEK_ORDER
+ return TBankPaymentNotificationAction.REGISTER_PROVIDER_ORDER
return TBankPaymentNotificationAction.ACKNOWLEDGE_ONLY
diff --git a/tests/adapters/delivery_providers/cse/test_cse_adapter.py b/tests/adapters/delivery_providers/cse/test_cse_adapter.py
index 642fc23..8389512 100644
--- a/tests/adapters/delivery_providers/cse/test_cse_adapter.py
+++ b/tests/adapters/delivery_providers/cse/test_cse_adapter.py
@@ -7,6 +7,7 @@ import pytest
from app.adapters.delivery_providers.cse import (
CSEClient,
+ CSEClientError,
CSEProvider,
CSERequestError,
)
@@ -314,6 +315,37 @@ def test_build_envelope_contains_credentials_and_payload() -> None:
assert "geo-2" in text
+def test_save_order_envelope_serializes_properties_before_fields() -> None:
+ body = order_mapper.map_cse_save_order_request(
+ make_init_payment_request(
+ systemData={
+ "tariff": {
+ "provider": "cse",
+ "serviceName": "Экспресс",
+ "price": 200000,
+ "deliveryDaysMin": 1,
+ "deliveryDaysMax": 2,
+ "tariffCode": "ДоставкаДоДверей|tariff-guid-2|urg-exp",
+ },
+ "parcelType": "parcel",
+ "weight": "1.0",
+ "dimensions": {"length": "10", "width": "10", "height": "10"},
+ }
+ ),
+ "order-uuid-1",
+ _params(),
+ )
+
+ text = build_envelope(
+ "SaveDocuments", login="test", password="2016", body=body
+ ).decode("utf-8")
+
+ order_start = text.index("Order")
+ properties_index = text.index("", order_start)
+ fields_index = text.index("", order_start)
+ assert properties_index < fields_index
+
+
def test_parse_calc_response_round_trip() -> None:
root = parse_response(_CALC_RESPONSE, "Calc")
assert root.key == "Calc"
@@ -580,3 +612,59 @@ def test_application_error_in_response_raises_request_error() -> None:
with pytest.raises(CSERequestError):
asyncio.run(provider.get_prices(_calc_request()))
+
+
+def test_server_error_logs_response_excerpt(monkeypatch: pytest.MonkeyPatch) -> None:
+ warning_calls: list[dict[str, Any]] = []
+
+ def capture_warning(event: str, **kwargs: Any) -> None:
+ warning_calls.append({"event": event, **kwargs})
+
+ monkeypatch.setattr(
+ "app.adapters.delivery_providers.cse.client.log.warning",
+ capture_warning,
+ )
+ response = httpx.Response(
+ 500,
+ text="\n CSE rejected SaveDocuments because field X is invalid \n",
+ )
+ client, _ = _build_client([response])
+ provider = CSEProvider(client)
+
+ with pytest.raises(CSEClientError):
+ asyncio.run(
+ provider.register_order(
+ make_init_payment_request(
+ systemData={
+ "tariff": {
+ "provider": "cse",
+ "serviceName": "Экспресс",
+ "price": 200000,
+ "deliveryDaysMin": 1,
+ "deliveryDaysMax": 2,
+ "tariffCode": "ДоставкаДоДверей|tariff-guid-2|urg-exp",
+ },
+ "parcelType": "parcel",
+ "weight": "1.0",
+ "dimensions": {
+ "length": "10",
+ "width": "10",
+ "height": "10",
+ },
+ }
+ ),
+ "order-uuid-1",
+ )
+ )
+
+ assert warning_calls == [
+ {
+ "event": "cse_request_server_error",
+ "operation": "SaveDocuments",
+ "status_code": 500,
+ "response_excerpt": (
+ " CSE rejected SaveDocuments because field X is invalid "
+ ""
+ ),
+ }
+ ]
diff --git a/tests/domain/test_payment_notifications.py b/tests/domain/test_payment_notifications.py
index c55e0b4..8fc4efd 100644
--- a/tests/domain/test_payment_notifications.py
+++ b/tests/domain/test_payment_notifications.py
@@ -5,14 +5,14 @@ from app.domain.payment_notifications import (
)
-def test_confirmed_success_zero_error_code_registers_cdek_order() -> None:
+def test_confirmed_success_zero_error_code_registers_provider_order() -> None:
result = resolve_tbank_payment_notification_action(
status="CONFIRMED",
success=True,
error_code="0",
)
- assert result is TBankPaymentNotificationAction.REGISTER_CDEK_ORDER
+ assert result is TBankPaymentNotificationAction.REGISTER_PROVIDER_ORDER
def test_confirmed_without_success_acknowledges_only() -> None: