@@ -179,6 +179,7 @@ class CSEClient:
|
|||||||
"cse_request_server_error",
|
"cse_request_server_error",
|
||||||
operation=operation,
|
operation=operation,
|
||||||
status_code=response.status_code,
|
status_code=response.status_code,
|
||||||
|
response_excerpt=_response_excerpt(response.text),
|
||||||
)
|
)
|
||||||
raise CSEClientError(
|
raise CSEClientError(
|
||||||
f"CSE {operation} request failed with status "
|
f"CSE {operation} request failed with status "
|
||||||
@@ -233,6 +234,10 @@ class CSEClient:
|
|||||||
return self._retry_backoff_seconds * (2**attempt)
|
return self._retry_backoff_seconds * (2**attempt)
|
||||||
|
|
||||||
|
|
||||||
|
def _response_excerpt(text: str, *, limit: int = 1000) -> str:
|
||||||
|
return " ".join(text.split())[:limit]
|
||||||
|
|
||||||
|
|
||||||
class CSEProvider(DeliveryProvider):
|
class CSEProvider(DeliveryProvider):
|
||||||
name = CSE_PROVIDER_NAME
|
name = CSE_PROVIDER_NAME
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
"""SOAP (cargo3) Element serialization and parsing for CSE.
|
"""SOAP (cargo3) Element serialization and parsing for CSE.
|
||||||
|
|
||||||
CSE exposes a SOAP/1C web service ("Карго") that transfers data through nested
|
CSE exposes a SOAP/1C web service ("Карго") that transfers data through nested
|
||||||
universal ``Element`` structures (``Key``/``Value``/``ValueType``/``Fields``/
|
universal ``Element`` structures (``Key``/``Value``/``ValueType``/
|
||||||
``List``/``Tables``/``Properties``). This module builds request envelopes and
|
``Properties``/``Fields``/``List``/``Tables``). This module builds request
|
||||||
parses responses into a plain Python representation that mappers can navigate.
|
envelopes and parses responses into a plain Python representation that mappers
|
||||||
|
can navigate.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -15,8 +16,6 @@ from xml.etree import ElementTree as ET
|
|||||||
CARGO_NS = "http://www.cargo3.ru"
|
CARGO_NS = "http://www.cargo3.ru"
|
||||||
SOAP_NS = "http://www.w3.org/2003/05/soap-envelope"
|
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")
|
_SCALAR_TAGS = ("Key", "Value", "ValueType")
|
||||||
_BINARY_TAG = "BData"
|
_BINARY_TAG = "BData"
|
||||||
|
|
||||||
@@ -102,14 +101,14 @@ def _append_element(parent: ET.Element, tag: str, element: Element) -> None:
|
|||||||
_scalar(node, "Value", element.value)
|
_scalar(node, "Value", element.value)
|
||||||
if element.value_type is not None:
|
if element.value_type is not None:
|
||||||
_scalar(node, "ValueType", element.value_type)
|
_scalar(node, "ValueType", element.value_type)
|
||||||
|
for child in element.properties:
|
||||||
|
_append_element(node, "Properties", child)
|
||||||
for child in element.fields:
|
for child in element.fields:
|
||||||
_append_element(node, "Fields", child)
|
_append_element(node, "Fields", child)
|
||||||
for child in element.items:
|
for child in element.items:
|
||||||
_append_element(node, "List", child)
|
_append_element(node, "List", child)
|
||||||
for child in element.tables:
|
for child in element.tables:
|
||||||
_append_element(node, "Tables", child)
|
_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:
|
def _scalar(parent: ET.Element, tag: str, text: str) -> None:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from enum import Enum
|
|||||||
|
|
||||||
class TBankPaymentNotificationAction(str, Enum):
|
class TBankPaymentNotificationAction(str, Enum):
|
||||||
ACKNOWLEDGE_ONLY = "acknowledge_only"
|
ACKNOWLEDGE_ONLY = "acknowledge_only"
|
||||||
REGISTER_CDEK_ORDER = "register_cdek_order"
|
REGISTER_PROVIDER_ORDER = "register_provider_order"
|
||||||
|
|
||||||
|
|
||||||
def resolve_tbank_payment_notification_action(
|
def resolve_tbank_payment_notification_action(
|
||||||
@@ -15,7 +15,7 @@ def resolve_tbank_payment_notification_action(
|
|||||||
error_code: str,
|
error_code: str,
|
||||||
) -> TBankPaymentNotificationAction:
|
) -> TBankPaymentNotificationAction:
|
||||||
if status == "CONFIRMED" and success is True and error_code == "0":
|
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
|
return TBankPaymentNotificationAction.ACKNOWLEDGE_ONLY
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import pytest
|
|||||||
|
|
||||||
from app.adapters.delivery_providers.cse import (
|
from app.adapters.delivery_providers.cse import (
|
||||||
CSEClient,
|
CSEClient,
|
||||||
|
CSEClientError,
|
||||||
CSEProvider,
|
CSEProvider,
|
||||||
CSERequestError,
|
CSERequestError,
|
||||||
)
|
)
|
||||||
@@ -314,6 +315,37 @@ def test_build_envelope_contains_credentials_and_payload() -> None:
|
|||||||
assert "geo-2" in text
|
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("<m:Key>Order</m:Key>")
|
||||||
|
properties_index = text.index("<m:Properties>", order_start)
|
||||||
|
fields_index = text.index("<m:Fields>", order_start)
|
||||||
|
assert properties_index < fields_index
|
||||||
|
|
||||||
|
|
||||||
def test_parse_calc_response_round_trip() -> None:
|
def test_parse_calc_response_round_trip() -> None:
|
||||||
root = parse_response(_CALC_RESPONSE, "Calc")
|
root = parse_response(_CALC_RESPONSE, "Calc")
|
||||||
assert root.key == "Calc"
|
assert root.key == "Calc"
|
||||||
@@ -580,3 +612,59 @@ def test_application_error_in_response_raises_request_error() -> None:
|
|||||||
|
|
||||||
with pytest.raises(CSERequestError):
|
with pytest.raises(CSERequestError):
|
||||||
asyncio.run(provider.get_prices(_calc_request()))
|
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="<fault>\n CSE rejected SaveDocuments because field X is invalid \n</fault>",
|
||||||
|
)
|
||||||
|
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": (
|
||||||
|
"<fault> CSE rejected SaveDocuments because field X is invalid "
|
||||||
|
"</fault>"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|||||||
@@ -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(
|
result = resolve_tbank_payment_notification_action(
|
||||||
status="CONFIRMED",
|
status="CONFIRMED",
|
||||||
success=True,
|
success=True,
|
||||||
error_code="0",
|
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:
|
def test_confirmed_without_success_acknowledges_only() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user