Добавлена валидация цены

This commit is contained in:
Раис Юсупалиев
2026-05-13 16:35:22 +03:00
parent 6b66af5eb3
commit 39cd5ddc7a
24 changed files with 1021 additions and 56 deletions
+99 -5
View File
@@ -3,13 +3,21 @@
import asyncio
from collections.abc import Awaitable, Callable
from typing import Any
import logging
import httpx
import structlog
from app.adapters.delivery_providers.base import DeliveryProvider, ProviderRequestError
from app.adapters.delivery_providers.base import (
DeliveryProvider,
ProviderClientError,
ProviderRequestError,
)
from app.adapters.delivery_providers.cdek.auth import CDEKAuthClient
from app.adapters.delivery_providers.cdek.mapper import map_cdek_response
from app.adapters.delivery_providers.cdek.mapper import (
CDEKMappingError,
map_cdek_response,
map_cdek_response_for_tariff_code,
)
from app.adapters.delivery_providers.cdek.order_mapper import (
CDEKOrderMappingError,
map_cdek_existing_order_response,
@@ -21,10 +29,10 @@ from app.config import AdapterConfig
from app.schemas.payment import InitPaymentRequest
from app.schemas.request import DeliveryCalculationRequest
from app.schemas.response import DeliveryPrice
log = logging.getLogger(__name__)
log = structlog.get_logger(__name__)
class CDEKClientError(RuntimeError):
class CDEKClientError(ProviderClientError):
"""Raised when CDEK tariff request fails."""
@@ -58,6 +66,29 @@ class CDEKClient:
self, request: DeliveryCalculationRequest
) -> dict[str, Any]:
payload = await self._build_payload(request)
return await self._post_tariff_payload(
payload,
request_error_message=None,
)
async def get_raw_payment_price(
self,
request: InitPaymentRequest,
) -> dict[str, Any]:
payload = self._build_payment_price_payload(request)
return await self._post_tariff_payload(
payload,
request_error_message=(
"CDEK payment price validation request was rejected with status"
),
)
async def _post_tariff_payload(
self,
payload: dict[str, Any],
*,
request_error_message: str | None,
) -> dict[str, Any]:
for attempt in range(self._retry_attempts + 1):
try:
token = await self._auth_client.get_access_token()
@@ -79,10 +110,27 @@ class CDEKClient:
if attempt < self._retry_attempts:
await self._sleep(self._retry_delay(attempt))
continue
log.warning(
"cdek_tariff_request_server_error",
status_code=response.status_code,
response_body=_response_text_or_none(response),
request_payload=payload,
)
raise CDEKClientError(
f"CDEK tariff request failed with status {response.status_code}."
)
if request_error_message is not None and 400 <= response.status_code < 500:
log.warning(
"cdek_tariff_request_rejected",
status_code=response.status_code,
response_body=_response_text_or_none(response),
request_payload=payload,
)
raise CDEKRequestError(
f"{request_error_message} {response.status_code}."
)
try:
response.raise_for_status()
raw_payload = response.json()
@@ -182,6 +230,30 @@ class CDEKClient:
],
}
@staticmethod
def _build_payment_price_payload(
request: InitPaymentRequest,
) -> dict[str, Any]:
payload: dict[str, Any] = {
"type": request.type,
"from_location": request.from_location.model_dump(mode="python"),
"to_location": request.to_location.model_dump(mode="python"),
"packages": [
{
"weight": package.weight * 1000,
"length": package.length,
"width": package.width,
"height": package.height,
}
for package in request.packages
],
}
if request.services is not None:
payload["services"] = [
service.model_dump(mode="python") for service in request.services
]
return payload
@staticmethod
def _get_cdek_city_code(city_id: int) -> int:
city_entry = cities_map.get(str(city_id))
@@ -247,6 +319,21 @@ class CDEKProvider(DeliveryProvider):
raw_payload = await self._client.get_raw_price(request)
return map_cdek_response(raw_payload)
async def get_payment_price(
self,
request: InitPaymentRequest,
) -> DeliveryPrice | None:
raw_payload = await self._client.get_raw_payment_price(request)
try:
return map_cdek_response_for_tariff_code(
raw_payload,
tariff_code=request.tariff_code,
)
except CDEKMappingError as exc:
raise CDEKClientError(
"CDEK payment price validation response payload is invalid."
) from exc
async def register_order(self, request: InitPaymentRequest) -> str:
return await self._client.register_order(request)
@@ -256,3 +343,10 @@ def _response_json_or_none(response: httpx.Response) -> object | None:
return response.json()
except (TypeError, ValueError):
return None
def _response_text_or_none(response: httpx.Response) -> str | None:
try:
return response.text
except Exception:
return None
@@ -19,6 +19,24 @@ def map_cdek_response(payload: dict[str, Any]) -> list[DeliveryPrice]:
]
def map_cdek_response_for_tariff_code(
payload: dict[str, Any],
*,
tariff_code: int,
) -> DeliveryPrice | None:
tariff_codes = payload.get("tariff_codes")
if not isinstance(tariff_codes, list):
raise CDEKMappingError("CDEK response must include tariff_codes.")
payload_currency = payload.get("currency")
for tariff in tariff_codes:
if not isinstance(tariff, dict):
raise CDEKMappingError("CDEK tariff entry must be an object.")
if _tariff_code_matches(tariff.get("tariff_code"), tariff_code):
return _map_tariff(tariff, payload_currency=payload_currency)
return None
def _map_tariff(
tariff: dict[str, Any],
*,
@@ -47,6 +65,8 @@ def _map_tariff(
currency=str(raw_currency).upper(),
delivery_days_min=int(period_min),
delivery_days_max=int(period_max),
tariff_code=_extract_tariff_code(tariff.get("tariff_code")),
bypass_parcel_type_filter=True,
)
except (ArithmeticError, TypeError, ValueError) as exc:
raise CDEKMappingError("CDEK response fields have invalid values.") from exc
@@ -62,3 +82,21 @@ def _get_tariffs(payload: dict[str, Any]) -> list[dict[str, Any]]:
raise CDEKMappingError("CDEK tariff entry must be an object.")
tariffs.append(tariff)
return tariffs
def _tariff_code_matches(value: object, expected_tariff_code: int) -> bool:
if isinstance(value, bool) or value is None:
return False
try:
return int(value) == expected_tariff_code
except (TypeError, ValueError):
return False
def _extract_tariff_code(value: object) -> int | None:
if isinstance(value, bool) or value is None:
return None
try:
return int(value)
except (TypeError, ValueError):
return None