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: