Добавлен провайдер доставки CSE: SOAP-адаптер (Calc + SaveDocuments), маршрутизация init-payment по провайдеру, обобщение tariff_code до строки, география CSE в cities_map
Deploy / deploy (push) Failing after 52s
Deploy / deploy (push) Failing after 52s
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
"""CSE SOAP client and provider adapter."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
import httpx
|
||||
import structlog
|
||||
|
||||
from app.adapters.delivery_providers.base import DeliveryProvider
|
||||
from app.adapters.delivery_providers.cse.constants import CSE_PROVIDER_NAME
|
||||
from app.adapters.delivery_providers.cse.errors import (
|
||||
CSEClientError,
|
||||
CSEMappingError,
|
||||
CSERequestError,
|
||||
)
|
||||
from app.adapters.delivery_providers.cse.mapper import (
|
||||
map_cse_calc_response,
|
||||
map_cse_calc_response_for_tariff_code,
|
||||
)
|
||||
from app.adapters.delivery_providers.cse.order_mapper import (
|
||||
CSEOrderRegistrationParams,
|
||||
CSEOrderRegistrationResult,
|
||||
build_calc_body_for_calculation,
|
||||
build_calc_body_for_payment,
|
||||
map_cse_save_order_request,
|
||||
map_cse_save_order_response,
|
||||
)
|
||||
from app.adapters.delivery_providers.cse.soap import (
|
||||
Element,
|
||||
build_envelope,
|
||||
parse_response,
|
||||
)
|
||||
from app.config import AdapterConfig
|
||||
from app.schemas.payment import InitPaymentRequest
|
||||
from app.schemas.request import DeliveryCalculationRequest
|
||||
from app.schemas.response import DeliveryPrice
|
||||
|
||||
log = structlog.get_logger(__name__)
|
||||
|
||||
_SOAP_HEADERS = {"Content-Type": "application/soap+xml; charset=utf-8"}
|
||||
|
||||
|
||||
class CSEClient:
|
||||
def __init__(
|
||||
self,
|
||||
http_client: httpx.AsyncClient,
|
||||
*,
|
||||
base_url: str,
|
||||
login: str,
|
||||
password: str,
|
||||
registration_params: CSEOrderRegistrationParams,
|
||||
timeout_seconds: float = 10.0,
|
||||
retry_attempts: int = 2,
|
||||
retry_backoff_seconds: float = 0.2,
|
||||
sleep: Callable[[float], Awaitable[None]] = asyncio.sleep,
|
||||
) -> None:
|
||||
self._http_client = http_client
|
||||
self._url = base_url
|
||||
self._login = login
|
||||
self._password = password
|
||||
self._registration_params = registration_params
|
||||
self._timeout_seconds = timeout_seconds
|
||||
self._retry_attempts = retry_attempts
|
||||
self._retry_backoff_seconds = retry_backoff_seconds
|
||||
self._sleep = sleep
|
||||
|
||||
async def calc(self, body: dict[str, Element]) -> Element:
|
||||
return await self._post(
|
||||
"Calc",
|
||||
body,
|
||||
request_error_message="CSE calc request was rejected with status",
|
||||
)
|
||||
|
||||
async def save_order(
|
||||
self, request: InitPaymentRequest, order_uuid: str
|
||||
) -> CSEOrderRegistrationResult:
|
||||
body = map_cse_save_order_request(
|
||||
request, order_uuid, self._registration_params
|
||||
)
|
||||
root = await self._post(
|
||||
"SaveDocuments",
|
||||
body,
|
||||
request_error_message="CSE SaveDocuments request was rejected with status",
|
||||
)
|
||||
try:
|
||||
return map_cse_save_order_response(root)
|
||||
except CSEMappingError as exc:
|
||||
raise CSEClientError(str(exc)) from exc
|
||||
|
||||
async def _post(
|
||||
self,
|
||||
operation: str,
|
||||
body: dict[str, Element],
|
||||
*,
|
||||
request_error_message: str,
|
||||
) -> Element:
|
||||
envelope = build_envelope(
|
||||
operation,
|
||||
login=self._login,
|
||||
password=self._password,
|
||||
body=body,
|
||||
)
|
||||
for attempt in range(self._retry_attempts + 1):
|
||||
try:
|
||||
response = await self._http_client.post(
|
||||
self._url,
|
||||
content=envelope,
|
||||
headers=_SOAP_HEADERS,
|
||||
timeout=self._timeout_seconds,
|
||||
)
|
||||
except (httpx.TimeoutException, httpx.TransportError) as exc:
|
||||
if attempt < self._retry_attempts:
|
||||
await self._sleep(self._retry_delay(attempt))
|
||||
continue
|
||||
raise CSEClientError(
|
||||
f"CSE {operation} request failed after retry attempts."
|
||||
) from exc
|
||||
|
||||
if self._should_retry(response.status_code):
|
||||
if attempt < self._retry_attempts:
|
||||
await self._sleep(self._retry_delay(attempt))
|
||||
continue
|
||||
log.warning(
|
||||
"cse_request_server_error",
|
||||
operation=operation,
|
||||
status_code=response.status_code,
|
||||
)
|
||||
raise CSEClientError(
|
||||
f"CSE {operation} request failed with status {response.status_code}."
|
||||
)
|
||||
|
||||
if 400 <= response.status_code < 500:
|
||||
log.warning(
|
||||
"cse_request_rejected",
|
||||
operation=operation,
|
||||
status_code=response.status_code,
|
||||
)
|
||||
raise CSERequestError(
|
||||
f"{request_error_message} {response.status_code}."
|
||||
)
|
||||
|
||||
try:
|
||||
root = parse_response(response.text, operation)
|
||||
except ValueError as exc:
|
||||
raise CSEClientError(
|
||||
f"CSE {operation} response payload is invalid."
|
||||
) from exc
|
||||
|
||||
self._raise_for_response_error(root, operation, request_error_message)
|
||||
return root
|
||||
|
||||
raise CSEClientError(f"CSE {operation} request failed unexpectedly.")
|
||||
|
||||
@staticmethod
|
||||
def _raise_for_response_error(
|
||||
root: Element,
|
||||
operation: str,
|
||||
request_error_message: str,
|
||||
) -> None:
|
||||
for prop in root.properties:
|
||||
if prop.key == "Error":
|
||||
codes = [item.value for item in prop.items if item.value]
|
||||
log.warning(
|
||||
"cse_response_error",
|
||||
operation=operation,
|
||||
error_codes=codes,
|
||||
)
|
||||
raise CSERequestError(
|
||||
f"{request_error_message} application error {codes}."
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _should_retry(status_code: int) -> bool:
|
||||
return status_code == 429 or 500 <= status_code < 600
|
||||
|
||||
def _retry_delay(self, attempt: int) -> float:
|
||||
return self._retry_backoff_seconds * (2**attempt)
|
||||
|
||||
|
||||
class CSEProvider(DeliveryProvider):
|
||||
name = CSE_PROVIDER_NAME
|
||||
|
||||
def __init__(self, client: CSEClient, *, cache_ttl_seconds: int = 900) -> None:
|
||||
self._client = client
|
||||
self.cache_ttl_seconds = cache_ttl_seconds
|
||||
|
||||
@classmethod
|
||||
def from_adapter_config(
|
||||
cls,
|
||||
*,
|
||||
http_client: httpx.AsyncClient,
|
||||
adapter_config: AdapterConfig,
|
||||
) -> "CSEProvider":
|
||||
client = CSEClient(
|
||||
http_client=http_client,
|
||||
base_url=adapter_config.cse_base_url,
|
||||
login=adapter_config.cse_login,
|
||||
password=adapter_config.cse_password,
|
||||
registration_params=CSEOrderRegistrationParams(
|
||||
payer=adapter_config.cse_payer,
|
||||
payment_method=adapter_config.cse_payment_method,
|
||||
shipping_method=adapter_config.cse_shipping_method,
|
||||
urgency=adapter_config.cse_urgency,
|
||||
),
|
||||
timeout_seconds=adapter_config.cse_timeout_seconds,
|
||||
retry_attempts=adapter_config.cse_retry_attempts,
|
||||
retry_backoff_seconds=adapter_config.cse_retry_backoff_seconds,
|
||||
)
|
||||
return cls(client=client, cache_ttl_seconds=adapter_config.cse_cache_ttl_seconds)
|
||||
|
||||
async def get_prices(
|
||||
self, request: DeliveryCalculationRequest
|
||||
) -> list[DeliveryPrice]:
|
||||
body = build_calc_body_for_calculation(request)
|
||||
root = await self._client.calc(body)
|
||||
try:
|
||||
return map_cse_calc_response(root)
|
||||
except CSEMappingError as exc:
|
||||
raise CSEClientError("CSE calc response payload is invalid.") from exc
|
||||
|
||||
async def get_payment_price(
|
||||
self,
|
||||
request: InitPaymentRequest,
|
||||
) -> DeliveryPrice | None:
|
||||
body = build_calc_body_for_payment(request)
|
||||
root = await self._client.calc(body)
|
||||
try:
|
||||
return map_cse_calc_response_for_tariff_code(
|
||||
root,
|
||||
tariff_code=request.system_data.tariff.tariff_code,
|
||||
)
|
||||
except CSEMappingError as exc:
|
||||
raise CSEClientError(
|
||||
"CSE payment price validation response payload is invalid."
|
||||
) from exc
|
||||
|
||||
async def register_order(
|
||||
self, request: InitPaymentRequest, order_uuid: str
|
||||
) -> CSEOrderRegistrationResult:
|
||||
return await self._client.save_order(request, order_uuid)
|
||||
Reference in New Issue
Block a user