Добавлен поллер накладной
This commit is contained in:
@@ -19,13 +19,17 @@ from app.adapters.delivery_providers.cdek.mapper import (
|
||||
map_cdek_response_for_tariff_code,
|
||||
)
|
||||
from app.adapters.delivery_providers.cdek.order_mapper import (
|
||||
CDEKOrderInfo,
|
||||
CDEKOrderMappingError,
|
||||
CDEKOrderRegistrationResult,
|
||||
CDEKWaybillInfo,
|
||||
centimeters_string_to_int,
|
||||
kilograms_string_to_grams,
|
||||
map_cdek_existing_order_response,
|
||||
map_cdek_order_info_response,
|
||||
map_cdek_order_request,
|
||||
map_cdek_order_response,
|
||||
map_cdek_waybill_info_response,
|
||||
resolve_cdek_city_code,
|
||||
)
|
||||
from app.cities import cities_map
|
||||
@@ -61,6 +65,7 @@ class CDEKClient:
|
||||
normalized_base_url = base_url.rstrip("/")
|
||||
self._tariff_url = f"{normalized_base_url}/calculator/tarifflist"
|
||||
self._orders_url = f"{normalized_base_url}/orders"
|
||||
self._print_orders_url = f"{normalized_base_url}/print/orders"
|
||||
self._timeout_seconds = timeout_seconds
|
||||
self._retry_attempts = retry_attempts
|
||||
self._retry_backoff_seconds = retry_backoff_seconds
|
||||
@@ -222,6 +227,85 @@ class CDEKClient:
|
||||
|
||||
raise CDEKClientError("CDEK order registration failed unexpectedly.")
|
||||
|
||||
async def get_order(self, cdek_order_uuid: str) -> CDEKOrderInfo:
|
||||
url = f"{self._orders_url}/{cdek_order_uuid}"
|
||||
raw_payload = await self._get_json(
|
||||
url,
|
||||
failure_message="CDEK get order failed",
|
||||
)
|
||||
try:
|
||||
return map_cdek_order_info_response(raw_payload)
|
||||
except CDEKOrderMappingError as exc:
|
||||
raise CDEKClientError(
|
||||
"CDEK get order response payload is invalid."
|
||||
) from exc
|
||||
|
||||
async def get_waybill(self, cdek_waybill_uuid: str) -> CDEKWaybillInfo:
|
||||
url = f"{self._print_orders_url}/{cdek_waybill_uuid}"
|
||||
raw_payload = await self._get_json(
|
||||
url,
|
||||
failure_message="CDEK get waybill failed",
|
||||
)
|
||||
try:
|
||||
return map_cdek_waybill_info_response(raw_payload)
|
||||
except CDEKOrderMappingError as exc:
|
||||
raise CDEKClientError(
|
||||
"CDEK get waybill response payload is invalid."
|
||||
) from exc
|
||||
|
||||
async def _get_json(self, url: str, *, failure_message: str) -> dict[str, Any]:
|
||||
for attempt in range(self._retry_attempts + 1):
|
||||
try:
|
||||
token = await self._auth_client.get_access_token()
|
||||
response = await self._http_client.get(
|
||||
url,
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
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 CDEKClientError(
|
||||
f"{failure_message} 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
|
||||
raise CDEKClientError(
|
||||
f"{failure_message} with retriable status "
|
||||
f"{response.status_code}."
|
||||
)
|
||||
|
||||
if 400 <= response.status_code < 500:
|
||||
log.warning(
|
||||
"cdek_get_request_rejected",
|
||||
url=url,
|
||||
status_code=response.status_code,
|
||||
response_body=_response_text_or_none(response),
|
||||
)
|
||||
raise CDEKRequestError(
|
||||
f"{failure_message} with status {response.status_code}."
|
||||
)
|
||||
|
||||
try:
|
||||
response.raise_for_status()
|
||||
raw_payload = response.json()
|
||||
except (httpx.HTTPError, TypeError, ValueError) as exc:
|
||||
raise CDEKClientError(
|
||||
f"{failure_message}: invalid response payload."
|
||||
) from exc
|
||||
|
||||
if not isinstance(raw_payload, dict):
|
||||
raise CDEKClientError(
|
||||
f"{failure_message}: payload must be a JSON object."
|
||||
)
|
||||
return raw_payload
|
||||
|
||||
raise CDEKClientError(f"{failure_message} unexpectedly.")
|
||||
|
||||
def _retry_delay(self, attempt: int) -> float:
|
||||
return self._retry_backoff_seconds * (attempt + 1)
|
||||
|
||||
@@ -361,6 +445,12 @@ class CDEKProvider(DeliveryProvider):
|
||||
) -> CDEKOrderRegistrationResult:
|
||||
return await self._client.register_order(request)
|
||||
|
||||
async def get_order(self, cdek_order_uuid: str) -> CDEKOrderInfo:
|
||||
return await self._client.get_order(cdek_order_uuid)
|
||||
|
||||
async def get_waybill(self, cdek_waybill_uuid: str) -> CDEKWaybillInfo:
|
||||
return await self._client.get_waybill(cdek_waybill_uuid)
|
||||
|
||||
|
||||
def _response_json_or_none(response: httpx.Response) -> object | None:
|
||||
try:
|
||||
|
||||
@@ -29,6 +29,19 @@ class CDEKOrderRegistrationResult:
|
||||
waybill_url: str | None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CDEKOrderInfo:
|
||||
order_uuid: str
|
||||
status_code: str | None
|
||||
waybill_uuid: str | None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CDEKWaybillInfo:
|
||||
waybill_uuid: str
|
||||
url: str | None
|
||||
|
||||
|
||||
def map_cdek_order_request(request: InitPaymentRequest) -> dict[str, Any]:
|
||||
payload: dict[str, Any] = {
|
||||
"number": request.order_uuid,
|
||||
@@ -89,6 +102,65 @@ def map_cdek_existing_order_response(
|
||||
)
|
||||
|
||||
|
||||
def map_cdek_order_info_response(payload: dict[str, Any]) -> CDEKOrderInfo:
|
||||
entity = payload.get("entity")
|
||||
if not isinstance(entity, dict):
|
||||
raise CDEKOrderMappingError(
|
||||
"CDEK order info response must include entity object."
|
||||
)
|
||||
|
||||
order_uuid = entity.get("uuid")
|
||||
if not isinstance(order_uuid, str) or not order_uuid:
|
||||
raise CDEKOrderMappingError(
|
||||
"CDEK order info response must include entity.uuid."
|
||||
)
|
||||
|
||||
status_code = _latest_status_code(entity.get("statuses"))
|
||||
waybill_uuid, _ = _extract_waybill(payload)
|
||||
return CDEKOrderInfo(
|
||||
order_uuid=order_uuid,
|
||||
status_code=status_code,
|
||||
waybill_uuid=waybill_uuid,
|
||||
)
|
||||
|
||||
|
||||
def map_cdek_waybill_info_response(payload: dict[str, Any]) -> CDEKWaybillInfo:
|
||||
entity = payload.get("entity")
|
||||
if not isinstance(entity, dict):
|
||||
raise CDEKOrderMappingError(
|
||||
"CDEK waybill info response must include entity object."
|
||||
)
|
||||
|
||||
waybill_uuid = entity.get("uuid")
|
||||
if not isinstance(waybill_uuid, str) or not waybill_uuid:
|
||||
raise CDEKOrderMappingError(
|
||||
"CDEK waybill info response must include entity.uuid."
|
||||
)
|
||||
|
||||
url_value = entity.get("url")
|
||||
waybill_url = url_value if isinstance(url_value, str) and url_value else None
|
||||
return CDEKWaybillInfo(waybill_uuid=waybill_uuid, url=waybill_url)
|
||||
|
||||
|
||||
def _latest_status_code(statuses: object) -> str | None:
|
||||
if not isinstance(statuses, list) or not statuses:
|
||||
return None
|
||||
dated: list[tuple[str, str]] = []
|
||||
for entry in statuses:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
code = entry.get("code")
|
||||
if not isinstance(code, str) or not code:
|
||||
continue
|
||||
date_time = entry.get("date_time")
|
||||
date_time_key = date_time if isinstance(date_time, str) else ""
|
||||
dated.append((date_time_key, code))
|
||||
if not dated:
|
||||
return None
|
||||
dated.sort(key=lambda item: item[0])
|
||||
return dated[-1][1]
|
||||
|
||||
|
||||
def _extract_waybill(payload: dict[str, Any]) -> tuple[str | None, str | None]:
|
||||
related_entities = payload.get("related_entities")
|
||||
if not isinstance(related_entities, list):
|
||||
|
||||
Reference in New Issue
Block a user