This commit is contained in:
@@ -7,7 +7,10 @@ import httpx
|
|||||||
import structlog
|
import structlog
|
||||||
|
|
||||||
from app.adapters.delivery_providers.base import DeliveryProvider
|
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.constants import (
|
||||||
|
CSE_PROVIDER_NAME,
|
||||||
|
delivery_type_requires_pvz,
|
||||||
|
)
|
||||||
from app.adapters.delivery_providers.cse.errors import (
|
from app.adapters.delivery_providers.cse.errors import (
|
||||||
CSEClientError,
|
CSEClientError,
|
||||||
CSEMappingError,
|
CSEMappingError,
|
||||||
@@ -24,10 +27,12 @@ from app.adapters.delivery_providers.cse.order_mapper import (
|
|||||||
build_calc_body_for_payment,
|
build_calc_body_for_payment,
|
||||||
map_cse_save_order_request,
|
map_cse_save_order_request,
|
||||||
map_cse_save_order_response,
|
map_cse_save_order_response,
|
||||||
|
split_tariff_code,
|
||||||
)
|
)
|
||||||
from app.adapters.delivery_providers.cse.soap import (
|
from app.adapters.delivery_providers.cse.soap import (
|
||||||
Element,
|
Element,
|
||||||
build_envelope,
|
build_envelope,
|
||||||
|
make_field,
|
||||||
parse_response,
|
parse_response,
|
||||||
)
|
)
|
||||||
from app.config import AdapterConfig
|
from app.config import AdapterConfig
|
||||||
@@ -71,6 +76,27 @@ class CSEClient:
|
|||||||
request_error_message="CSE calc request was rejected with status",
|
request_error_message="CSE calc request was rejected with status",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def get_delivery_types(self) -> list[tuple[str, str]]:
|
||||||
|
"""Return available delivery schemes as (name, human label) pairs."""
|
||||||
|
|
||||||
|
body = {
|
||||||
|
"parameters": Element(
|
||||||
|
key="parameters",
|
||||||
|
items=[make_field("Reference", "DeliveryType")],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
root = await self._post(
|
||||||
|
"GetReferenceData",
|
||||||
|
body,
|
||||||
|
request_error_message="CSE delivery types request was rejected with status",
|
||||||
|
)
|
||||||
|
result: list[tuple[str, str]] = []
|
||||||
|
for entry in root.items:
|
||||||
|
name = entry.value
|
||||||
|
if name:
|
||||||
|
result.append((name, entry.field_value("Information") or name))
|
||||||
|
return result
|
||||||
|
|
||||||
async def save_order(
|
async def save_order(
|
||||||
self, request: InitPaymentRequest, order_uuid: str
|
self, request: InitPaymentRequest, order_uuid: str
|
||||||
) -> CSEOrderRegistrationResult:
|
) -> CSEOrderRegistrationResult:
|
||||||
@@ -183,6 +209,7 @@ class CSEProvider(DeliveryProvider):
|
|||||||
def __init__(self, client: CSEClient, *, cache_ttl_seconds: int = 900) -> None:
|
def __init__(self, client: CSEClient, *, cache_ttl_seconds: int = 900) -> None:
|
||||||
self._client = client
|
self._client = client
|
||||||
self.cache_ttl_seconds = cache_ttl_seconds
|
self.cache_ttl_seconds = cache_ttl_seconds
|
||||||
|
self._delivery_types: list[tuple[str, str]] | None = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_adapter_config(
|
def from_adapter_config(
|
||||||
@@ -200,7 +227,6 @@ class CSEProvider(DeliveryProvider):
|
|||||||
payer=adapter_config.cse_payer,
|
payer=adapter_config.cse_payer,
|
||||||
payment_method=adapter_config.cse_payment_method,
|
payment_method=adapter_config.cse_payment_method,
|
||||||
shipping_method=adapter_config.cse_shipping_method,
|
shipping_method=adapter_config.cse_shipping_method,
|
||||||
urgency=adapter_config.cse_urgency,
|
|
||||||
),
|
),
|
||||||
timeout_seconds=adapter_config.cse_timeout_seconds,
|
timeout_seconds=adapter_config.cse_timeout_seconds,
|
||||||
retry_attempts=adapter_config.cse_retry_attempts,
|
retry_attempts=adapter_config.cse_retry_attempts,
|
||||||
@@ -211,23 +237,71 @@ class CSEProvider(DeliveryProvider):
|
|||||||
async def get_prices(
|
async def get_prices(
|
||||||
self, request: DeliveryCalculationRequest
|
self, request: DeliveryCalculationRequest
|
||||||
) -> list[DeliveryPrice]:
|
) -> list[DeliveryPrice]:
|
||||||
body = build_calc_body_for_calculation(request)
|
delivery_types = await self._resolve_delivery_types()
|
||||||
|
|
||||||
|
results = await asyncio.gather(
|
||||||
|
*(
|
||||||
|
self._prices_for_delivery_type(request, name, label)
|
||||||
|
for name, label in delivery_types
|
||||||
|
),
|
||||||
|
return_exceptions=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
prices: list[DeliveryPrice] = []
|
||||||
|
errors: list[BaseException] = []
|
||||||
|
for result in results:
|
||||||
|
if isinstance(result, BaseException):
|
||||||
|
errors.append(result)
|
||||||
|
else:
|
||||||
|
prices.extend(result)
|
||||||
|
|
||||||
|
if not prices and errors:
|
||||||
|
raise errors[0]
|
||||||
|
return prices
|
||||||
|
|
||||||
|
async def _prices_for_delivery_type(
|
||||||
|
self,
|
||||||
|
request: DeliveryCalculationRequest,
|
||||||
|
delivery_type: str,
|
||||||
|
delivery_type_label: str,
|
||||||
|
) -> list[DeliveryPrice]:
|
||||||
|
body = build_calc_body_for_calculation(request, delivery_type)
|
||||||
root = await self._client.calc(body)
|
root = await self._client.calc(body)
|
||||||
try:
|
try:
|
||||||
return map_cse_calc_response(root)
|
return map_cse_calc_response(
|
||||||
|
root,
|
||||||
|
delivery_type=delivery_type,
|
||||||
|
delivery_type_label=delivery_type_label,
|
||||||
|
)
|
||||||
except CSEMappingError as exc:
|
except CSEMappingError as exc:
|
||||||
raise CSEClientError("CSE calc response payload is invalid.") from exc
|
raise CSEClientError("CSE calc response payload is invalid.") from exc
|
||||||
|
|
||||||
|
async def _resolve_delivery_types(self) -> list[tuple[str, str]]:
|
||||||
|
if self._delivery_types is None:
|
||||||
|
self._delivery_types = await self._client.get_delivery_types()
|
||||||
|
# Exclude schemes that require a pickup point (PVZ) at registration,
|
||||||
|
# since PVZ is not yet supported (only door-to-door is fulfillable).
|
||||||
|
usable = [
|
||||||
|
(name, label)
|
||||||
|
for name, label in self._delivery_types
|
||||||
|
if not delivery_type_requires_pvz(name, label)
|
||||||
|
]
|
||||||
|
# Fall back to the contract-default scheme (empty delivery_type) so
|
||||||
|
# calculation still works if no usable schemes are returned.
|
||||||
|
return usable or [("", "")]
|
||||||
|
|
||||||
async def get_payment_price(
|
async def get_payment_price(
|
||||||
self,
|
self,
|
||||||
request: InitPaymentRequest,
|
request: InitPaymentRequest,
|
||||||
) -> DeliveryPrice | None:
|
) -> DeliveryPrice | None:
|
||||||
body = build_calc_body_for_payment(request)
|
tariff_code = request.system_data.tariff.tariff_code
|
||||||
|
delivery_type, _ = split_tariff_code(tariff_code)
|
||||||
|
body = build_calc_body_for_payment(request, delivery_type)
|
||||||
root = await self._client.calc(body)
|
root = await self._client.calc(body)
|
||||||
try:
|
try:
|
||||||
return map_cse_calc_response_for_tariff_code(
|
return map_cse_calc_response_for_tariff_code(
|
||||||
root,
|
root,
|
||||||
tariff_code=request.system_data.tariff.tariff_code,
|
tariff_code=tariff_code,
|
||||||
)
|
)
|
||||||
except CSEMappingError as exc:
|
except CSEMappingError as exc:
|
||||||
raise CSEClientError(
|
raise CSEClientError(
|
||||||
|
|||||||
@@ -12,6 +12,16 @@ from app.cities import cities_map
|
|||||||
# Provider name surfaced in unified DeliveryPrice/SystemDataTariff.
|
# Provider name surfaced in unified DeliveryPrice/SystemDataTariff.
|
||||||
CSE_PROVIDER_NAME = "cse"
|
CSE_PROVIDER_NAME = "cse"
|
||||||
|
|
||||||
|
# Keywords in a DeliveryType name/label that imply a pickup point (PVZ) is
|
||||||
|
# required at registration (SenderPVZ/RecipientPVZ). PVZ is not yet supported,
|
||||||
|
# so such schemes are excluded from price calculation.
|
||||||
|
_PVZ_SCHEME_KEYWORDS = ("склад", "самовывоз", "почтов", "пвз")
|
||||||
|
|
||||||
|
|
||||||
|
def delivery_type_requires_pvz(name: str, label: str) -> bool:
|
||||||
|
haystack = f"{name} {label}".lower()
|
||||||
|
return any(keyword in haystack for keyword in _PVZ_SCHEME_KEYWORDS)
|
||||||
|
|
||||||
# CSE returns currency short names (e.g. "RUR"); normalize to ISO-4217.
|
# CSE returns currency short names (e.g. "RUR"); normalize to ISO-4217.
|
||||||
_CURRENCY_NAME_TO_CODE = {
|
_CURRENCY_NAME_TO_CODE = {
|
||||||
"RUR": "RUB",
|
"RUR": "RUB",
|
||||||
|
|||||||
@@ -13,12 +13,17 @@ from app.schemas.response import DeliveryPrice
|
|||||||
_TARIFF_KEY = "Tariff"
|
_TARIFF_KEY = "Tariff"
|
||||||
|
|
||||||
|
|
||||||
def map_cse_calc_response(root: Element) -> list[DeliveryPrice]:
|
def map_cse_calc_response(
|
||||||
|
root: Element,
|
||||||
|
*,
|
||||||
|
delivery_type: str = "",
|
||||||
|
delivery_type_label: str = "",
|
||||||
|
) -> list[DeliveryPrice]:
|
||||||
"""Map a parsed ``Calc`` ``return`` Element into unified delivery prices."""
|
"""Map a parsed ``Calc`` ``return`` Element into unified delivery prices."""
|
||||||
|
|
||||||
prices: list[DeliveryPrice] = []
|
prices: list[DeliveryPrice] = []
|
||||||
for tariff in _iter_tariffs(root):
|
for tariff in _iter_tariffs(root):
|
||||||
price = _map_tariff(tariff)
|
price = _map_tariff(tariff, delivery_type, delivery_type_label)
|
||||||
if price is not None:
|
if price is not None:
|
||||||
prices.append(price)
|
prices.append(price)
|
||||||
return prices
|
return prices
|
||||||
@@ -28,15 +33,23 @@ def map_cse_calc_response_for_tariff_code(
|
|||||||
root: Element,
|
root: Element,
|
||||||
*,
|
*,
|
||||||
tariff_code: str,
|
tariff_code: str,
|
||||||
|
delivery_type_label: str = "",
|
||||||
) -> DeliveryPrice | None:
|
) -> DeliveryPrice | None:
|
||||||
"""Return the tariff matching the given GUID, or ``None`` when absent."""
|
"""Return the tariff matching ``tariff_code`` (``"<DeliveryType>|<Urgency>"``)."""
|
||||||
|
|
||||||
|
delivery_type, _, urgency = tariff_code.partition("|")
|
||||||
for tariff in _iter_tariffs(root):
|
for tariff in _iter_tariffs(root):
|
||||||
if tariff.value == tariff_code:
|
if _tariff_urgency(tariff) == urgency:
|
||||||
return _map_tariff(tariff)
|
return _map_tariff(tariff, delivery_type, delivery_type_label)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _tariff_urgency(tariff: Element) -> str | None:
|
||||||
|
"""Urgency GUID of a Calc tariff (fallback: the tariff record GUID)."""
|
||||||
|
|
||||||
|
return tariff.field_value("Urgency") or tariff.value
|
||||||
|
|
||||||
|
|
||||||
def _iter_tariffs(root: Element):
|
def _iter_tariffs(root: Element):
|
||||||
for destination in root.items:
|
for destination in root.items:
|
||||||
for tariff in destination.items:
|
for tariff in destination.items:
|
||||||
@@ -44,10 +57,17 @@ def _iter_tariffs(root: Element):
|
|||||||
yield tariff
|
yield tariff
|
||||||
|
|
||||||
|
|
||||||
def _map_tariff(tariff: Element) -> DeliveryPrice | None:
|
def _map_tariff(
|
||||||
tariff_code = tariff.value
|
tariff: Element,
|
||||||
if not tariff_code:
|
delivery_type: str,
|
||||||
|
delivery_type_label: str,
|
||||||
|
) -> DeliveryPrice | None:
|
||||||
|
urgency = _tariff_urgency(tariff)
|
||||||
|
if not urgency:
|
||||||
return None
|
return None
|
||||||
|
# Unified tariff_code carries both the delivery scheme and the urgency so
|
||||||
|
# registration (SaveDocuments) can set DeliveryOfCargo and Urgency.
|
||||||
|
tariff_code = f"{delivery_type}|{urgency}"
|
||||||
|
|
||||||
raw_total = tariff.field_value("Total")
|
raw_total = tariff.field_value("Total")
|
||||||
if raw_total is None:
|
if raw_total is None:
|
||||||
@@ -60,9 +80,12 @@ def _map_tariff(tariff: Element) -> DeliveryPrice | None:
|
|||||||
if not price.is_finite() or price <= 0:
|
if not price.is_finite() or price <= 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
service_name = tariff.field_value("Service") or tariff.field_value("UrgencyName")
|
base_name = tariff.field_value("Service") or tariff.field_value("UrgencyName")
|
||||||
if not service_name:
|
if not base_name:
|
||||||
service_name = tariff_code
|
base_name = urgency
|
||||||
|
service_name = (
|
||||||
|
f"{base_name} — {delivery_type_label}" if delivery_type_label else base_name
|
||||||
|
)
|
||||||
|
|
||||||
currency = normalize_currency(tariff.field_value("CurrencyName"))
|
currency = normalize_currency(tariff.field_value("CurrencyName"))
|
||||||
|
|
||||||
|
|||||||
@@ -16,12 +16,15 @@ _DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
|
|||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class CSEOrderRegistrationParams:
|
class CSEOrderRegistrationParams:
|
||||||
"""Contract-specific required parameters for SaveDocuments."""
|
"""Contract-specific required parameters for SaveDocuments.
|
||||||
|
|
||||||
|
``Urgency`` is not here: it comes from the tariff the client selected
|
||||||
|
(``system_data.tariff.tariff_code`` holds the urgency GUID).
|
||||||
|
"""
|
||||||
|
|
||||||
payer: str
|
payer: str
|
||||||
payment_method: str
|
payment_method: str
|
||||||
shipping_method: str
|
shipping_method: str
|
||||||
urgency: str
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -31,39 +34,54 @@ class CSEOrderRegistrationResult:
|
|||||||
|
|
||||||
def build_calc_body_for_calculation(
|
def build_calc_body_for_calculation(
|
||||||
request: DeliveryCalculationRequest,
|
request: DeliveryCalculationRequest,
|
||||||
|
delivery_type: str = "",
|
||||||
) -> dict[str, Element]:
|
) -> dict[str, Element]:
|
||||||
destination = Element(
|
fields = [
|
||||||
key="Destination",
|
make_field("SenderGeography", resolve_cse_geography(request.from_city)),
|
||||||
fields=[
|
make_field("RecipientGeography", resolve_cse_geography(request.to_city)),
|
||||||
make_field("SenderGeography", resolve_cse_geography(request.from_city)),
|
make_field("TypeOfCargo", resolve_type_of_cargo(_parcel_type(request))),
|
||||||
make_field("RecipientGeography", resolve_cse_geography(request.to_city)),
|
make_field("Weight", _format_decimal(request.weight_kg), "float"),
|
||||||
make_field("TypeOfCargo", resolve_type_of_cargo(_parcel_type(request))),
|
make_field("Qty", "1", "int"),
|
||||||
make_field("Weight", _format_decimal(request.weight_kg), "float"),
|
]
|
||||||
make_field("Qty", "1", "int"),
|
_append_delivery_type(fields, delivery_type)
|
||||||
],
|
return _calc_body(Element(key="Destination", fields=fields))
|
||||||
)
|
|
||||||
return _calc_body(destination)
|
|
||||||
|
|
||||||
|
|
||||||
def build_calc_body_for_payment(request: InitPaymentRequest) -> dict[str, Element]:
|
def build_calc_body_for_payment(
|
||||||
|
request: InitPaymentRequest,
|
||||||
|
delivery_type: str = "",
|
||||||
|
) -> dict[str, Element]:
|
||||||
system_data = request.system_data
|
system_data = request.system_data
|
||||||
destination = Element(
|
fields = [
|
||||||
key="Destination",
|
make_field(
|
||||||
fields=[
|
"SenderGeography",
|
||||||
make_field(
|
resolve_cse_geography(request.sender_address.city_id),
|
||||||
"SenderGeography",
|
),
|
||||||
resolve_cse_geography(request.sender_address.city_id),
|
make_field(
|
||||||
),
|
"RecipientGeography",
|
||||||
make_field(
|
resolve_cse_geography(request.receiver_address.city_id),
|
||||||
"RecipientGeography",
|
),
|
||||||
resolve_cse_geography(request.receiver_address.city_id),
|
make_field("TypeOfCargo", resolve_type_of_cargo(system_data.parcel_type)),
|
||||||
),
|
make_field("Weight", system_data.weight, "float"),
|
||||||
make_field("TypeOfCargo", resolve_type_of_cargo(system_data.parcel_type)),
|
make_field("Qty", "1", "int"),
|
||||||
make_field("Weight", system_data.weight, "float"),
|
]
|
||||||
make_field("Qty", "1", "int"),
|
_append_delivery_type(fields, delivery_type)
|
||||||
],
|
return _calc_body(Element(key="Destination", fields=fields))
|
||||||
)
|
|
||||||
return _calc_body(destination)
|
|
||||||
|
def _append_delivery_type(fields: list[Element], delivery_type: str) -> None:
|
||||||
|
if delivery_type:
|
||||||
|
fields.append(make_field("DeliveryType", delivery_type))
|
||||||
|
|
||||||
|
|
||||||
|
def split_tariff_code(tariff_code: str) -> tuple[str, str]:
|
||||||
|
"""Split the CSE ``tariff_code`` into (delivery_type, urgency).
|
||||||
|
|
||||||
|
The unified tariff_code encodes both dimensions as ``"<DeliveryType>|<Urgency>"``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
delivery_type, _, urgency = tariff_code.partition("|")
|
||||||
|
return delivery_type, urgency
|
||||||
|
|
||||||
|
|
||||||
def _calc_body(destination: Element) -> dict[str, Element]:
|
def _calc_body(destination: Element) -> dict[str, Element]:
|
||||||
@@ -83,6 +101,7 @@ def map_cse_save_order_request(
|
|||||||
) -> dict[str, Element]:
|
) -> dict[str, Element]:
|
||||||
system_data = request.system_data
|
system_data = request.system_data
|
||||||
take_date = request.pickup_date.strftime(_DATETIME_FORMAT)
|
take_date = request.pickup_date.strftime(_DATETIME_FORMAT)
|
||||||
|
delivery_type, urgency = split_tariff_code(system_data.tariff.tariff_code)
|
||||||
|
|
||||||
fields = [
|
fields = [
|
||||||
make_field("TakeDate", take_date, "dateTime"),
|
make_field("TakeDate", take_date, "dateTime"),
|
||||||
@@ -101,7 +120,7 @@ def map_cse_save_order_request(
|
|||||||
),
|
),
|
||||||
make_field("RecipientAddress", _compose_address(request.receiver_address)),
|
make_field("RecipientAddress", _compose_address(request.receiver_address)),
|
||||||
make_field("RecipientPhone", request.receiver_contact.phone),
|
make_field("RecipientPhone", request.receiver_contact.phone),
|
||||||
make_field("Urgency", params.urgency),
|
make_field("Urgency", urgency),
|
||||||
make_field("Payer", params.payer, "float"),
|
make_field("Payer", params.payer, "float"),
|
||||||
make_field("PaymentMethod", params.payment_method, "float"),
|
make_field("PaymentMethod", params.payment_method, "float"),
|
||||||
make_field("ShippingMethod", params.shipping_method),
|
make_field("ShippingMethod", params.shipping_method),
|
||||||
@@ -109,6 +128,8 @@ def map_cse_save_order_request(
|
|||||||
make_field("Weight", system_data.weight, "float"),
|
make_field("Weight", system_data.weight, "float"),
|
||||||
make_field("CargoPackageQty", "1", "float"),
|
make_field("CargoPackageQty", "1", "float"),
|
||||||
]
|
]
|
||||||
|
if delivery_type:
|
||||||
|
fields.append(make_field("DeliveryOfCargo", delivery_type))
|
||||||
|
|
||||||
if request.delivery_date is not None:
|
if request.delivery_date is not None:
|
||||||
fields.append(
|
fields.append(
|
||||||
|
|||||||
+1
-1
@@ -54,10 +54,10 @@ class AdapterConfig(BaseModel):
|
|||||||
cse_timeout_seconds: float = Field(default=10.0, gt=0)
|
cse_timeout_seconds: float = Field(default=10.0, gt=0)
|
||||||
cse_cache_ttl_seconds: int = Field(default=900, gt=0)
|
cse_cache_ttl_seconds: int = Field(default=900, gt=0)
|
||||||
# Contract-specific required parameters for SaveDocuments (order registration).
|
# Contract-specific required parameters for SaveDocuments (order registration).
|
||||||
|
# Urgency is not here: it comes from the tariff selected by the client.
|
||||||
cse_payer: str = ""
|
cse_payer: str = ""
|
||||||
cse_payment_method: str = ""
|
cse_payment_method: str = ""
|
||||||
cse_shipping_method: str = ""
|
cse_shipping_method: str = ""
|
||||||
cse_urgency: str = ""
|
|
||||||
|
|
||||||
|
|
||||||
class TBankPaymentAuthConfig(BaseModel):
|
class TBankPaymentAuthConfig(BaseModel):
|
||||||
|
|||||||
@@ -29,10 +29,9 @@ adapter:
|
|||||||
cse_retry_backoff_seconds: 0.2
|
cse_retry_backoff_seconds: 0.2
|
||||||
cse_timeout_seconds: 10.0
|
cse_timeout_seconds: 10.0
|
||||||
cse_cache_ttl_seconds: 900
|
cse_cache_ttl_seconds: 900
|
||||||
cse_payer: "${CSE_PAYER}"
|
cse_payer: "0" # Заказчик
|
||||||
cse_payment_method: "${CSE_PAYMENT_METHOD}"
|
cse_payment_method: "1" # Безналичный расчёт
|
||||||
cse_shipping_method: "${CSE_SHIPPING_METHOD}"
|
cse_shipping_method: "5052d0b3-5ea3-46f2-823f-1472686a51dd" # авто
|
||||||
cse_urgency: "${CSE_URGENCY}"
|
|
||||||
|
|
||||||
tbank_payment:
|
tbank_payment:
|
||||||
init_url: "https://securepay.tinkoff.ru/v2/Init"
|
init_url: "https://securepay.tinkoff.ru/v2/Init"
|
||||||
|
|||||||
+3
-4
@@ -29,10 +29,9 @@ adapter:
|
|||||||
cse_retry_backoff_seconds: 0.2
|
cse_retry_backoff_seconds: 0.2
|
||||||
cse_timeout_seconds: 10.0
|
cse_timeout_seconds: 10.0
|
||||||
cse_cache_ttl_seconds: 900
|
cse_cache_ttl_seconds: 900
|
||||||
cse_payer: "test-payer"
|
cse_payer: "0"
|
||||||
cse_payment_method: "test-payment-method"
|
cse_payment_method: "1"
|
||||||
cse_shipping_method: "test-shipping-method"
|
cse_shipping_method: "5052d0b3-5ea3-46f2-823f-1472686a51dd"
|
||||||
cse_urgency: "test-urgency"
|
|
||||||
|
|
||||||
tbank_payment:
|
tbank_payment:
|
||||||
init_url: "https://securepay.tinkoff.ru/v2/Init"
|
init_url: "https://securepay.tinkoff.ru/v2/Init"
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ _CALC_RESPONSE = """<?xml version="1.0" encoding="UTF-8"?>
|
|||||||
<m:Fields><m:Key>Total</m:Key><m:Value>1114.92</m:Value></m:Fields>
|
<m:Fields><m:Key>Total</m:Key><m:Value>1114.92</m:Value></m:Fields>
|
||||||
<m:Fields><m:Key>CurrencyName</m:Key><m:Value>RUR</m:Value></m:Fields>
|
<m:Fields><m:Key>CurrencyName</m:Key><m:Value>RUR</m:Value></m:Fields>
|
||||||
<m:Fields><m:Key>Service</m:Key><m:Value>Россия доставка</m:Value></m:Fields>
|
<m:Fields><m:Key>Service</m:Key><m:Value>Россия доставка</m:Value></m:Fields>
|
||||||
|
<m:Fields><m:Key>Urgency</m:Key><m:Value>urg-std</m:Value></m:Fields>
|
||||||
<m:Fields><m:Key>MinPeriod</m:Key><m:Value>3</m:Value></m:Fields>
|
<m:Fields><m:Key>MinPeriod</m:Key><m:Value>3</m:Value></m:Fields>
|
||||||
<m:Fields><m:Key>MaxPeriod</m:Key><m:Value>5</m:Value></m:Fields>
|
<m:Fields><m:Key>MaxPeriod</m:Key><m:Value>5</m:Value></m:Fields>
|
||||||
</m:List>
|
</m:List>
|
||||||
@@ -44,6 +45,7 @@ _CALC_RESPONSE = """<?xml version="1.0" encoding="UTF-8"?>
|
|||||||
<m:Fields><m:Key>Total</m:Key><m:Value>2000.00</m:Value></m:Fields>
|
<m:Fields><m:Key>Total</m:Key><m:Value>2000.00</m:Value></m:Fields>
|
||||||
<m:Fields><m:Key>CurrencyName</m:Key><m:Value>RUR</m:Value></m:Fields>
|
<m:Fields><m:Key>CurrencyName</m:Key><m:Value>RUR</m:Value></m:Fields>
|
||||||
<m:Fields><m:Key>Service</m:Key><m:Value>Экспресс</m:Value></m:Fields>
|
<m:Fields><m:Key>Service</m:Key><m:Value>Экспресс</m:Value></m:Fields>
|
||||||
|
<m:Fields><m:Key>Urgency</m:Key><m:Value>urg-exp</m:Value></m:Fields>
|
||||||
<m:Fields><m:Key>MinPeriod</m:Key><m:Value>1</m:Value></m:Fields>
|
<m:Fields><m:Key>MinPeriod</m:Key><m:Value>1</m:Value></m:Fields>
|
||||||
<m:Fields><m:Key>MaxPeriod</m:Key><m:Value>2</m:Value></m:Fields>
|
<m:Fields><m:Key>MaxPeriod</m:Key><m:Value>2</m:Value></m:Fields>
|
||||||
</m:List>
|
</m:List>
|
||||||
@@ -83,6 +85,27 @@ _ERROR_RESPONSE = """<?xml version="1.0" encoding="UTF-8"?>
|
|||||||
</soap:Body>
|
</soap:Body>
|
||||||
</soap:Envelope>"""
|
</soap:Envelope>"""
|
||||||
|
|
||||||
|
_DELIVERY_TYPES_RESPONSE = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
|
||||||
|
<soap:Body>
|
||||||
|
<m:GetReferenceDataResponse xmlns:m="http://www.cargo3.ru">
|
||||||
|
<m:return>
|
||||||
|
<m:Key>deliverytype</m:Key>
|
||||||
|
<m:List>
|
||||||
|
<m:Key>NameOfdeliverytype</m:Key>
|
||||||
|
<m:Value>ДоставкаДоДверей</m:Value>
|
||||||
|
<m:Fields><m:Key>Information</m:Key><m:Value>Дверь-Дверь</m:Value></m:Fields>
|
||||||
|
</m:List>
|
||||||
|
<m:List>
|
||||||
|
<m:Key>NameOfdeliverytype</m:Key>
|
||||||
|
<m:Value>СкладДверь</m:Value>
|
||||||
|
<m:Fields><m:Key>Information</m:Key><m:Value>Склад-Дверь</m:Value></m:Fields>
|
||||||
|
</m:List>
|
||||||
|
</m:return>
|
||||||
|
</m:GetReferenceDataResponse>
|
||||||
|
</soap:Body>
|
||||||
|
</soap:Envelope>"""
|
||||||
|
|
||||||
|
|
||||||
class SequenceHTTPClient:
|
class SequenceHTTPClient:
|
||||||
def __init__(self, results: list[Any]) -> None:
|
def __init__(self, results: list[Any]) -> None:
|
||||||
@@ -109,7 +132,6 @@ def _params() -> CSEOrderRegistrationParams:
|
|||||||
payer="payer-1",
|
payer="payer-1",
|
||||||
payment_method="pm-1",
|
payment_method="pm-1",
|
||||||
shipping_method="sm-1",
|
shipping_method="sm-1",
|
||||||
urgency="urgency-1",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -166,19 +188,29 @@ def test_parse_calc_response_round_trip() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_provider_get_prices_maps_all_tariffs() -> None:
|
def test_provider_get_prices_maps_all_tariffs() -> None:
|
||||||
response = httpx.Response(200, text=_CALC_RESPONSE)
|
client, http_client = _build_client(
|
||||||
client, _ = _build_client([response])
|
[
|
||||||
|
httpx.Response(200, text=_DELIVERY_TYPES_RESPONSE),
|
||||||
|
httpx.Response(200, text=_CALC_RESPONSE),
|
||||||
|
]
|
||||||
|
)
|
||||||
provider = CSEProvider(client)
|
provider = CSEProvider(client)
|
||||||
|
|
||||||
prices = asyncio.run(provider.get_prices(_calc_request()))
|
prices = asyncio.run(provider.get_prices(_calc_request()))
|
||||||
|
|
||||||
|
# PVZ-requiring scheme (Склад-Дверь) is excluded: only delivery types + one
|
||||||
|
# Calc (for door-to-door) are requested.
|
||||||
|
assert len(http_client.calls) == 2
|
||||||
assert [price.provider for price in prices] == ["cse", "cse"]
|
assert [price.provider for price in prices] == ["cse", "cse"]
|
||||||
assert [price.tariff_code for price in prices] == ["tariff-guid-1", "tariff-guid-2"]
|
assert [price.tariff_code for price in prices] == [
|
||||||
|
"ДоставкаДоДверей|urg-std",
|
||||||
|
"ДоставкаДоДверей|urg-exp",
|
||||||
|
]
|
||||||
assert prices[0].price == Decimal("1114.92")
|
assert prices[0].price == Decimal("1114.92")
|
||||||
assert prices[0].currency == "RUB"
|
assert prices[0].currency == "RUB"
|
||||||
assert prices[0].delivery_days_min == 3
|
assert prices[0].delivery_days_min == 3
|
||||||
assert prices[0].delivery_days_max == 5
|
assert prices[0].delivery_days_max == 5
|
||||||
assert prices[0].service_name == "Россия доставка"
|
assert prices[0].service_name == "Россия доставка — Дверь-Дверь"
|
||||||
|
|
||||||
|
|
||||||
def test_provider_get_payment_price_selects_matching_tariff() -> None:
|
def test_provider_get_payment_price_selects_matching_tariff() -> None:
|
||||||
@@ -193,7 +225,7 @@ def test_provider_get_payment_price_selects_matching_tariff() -> None:
|
|||||||
"price": 200000,
|
"price": 200000,
|
||||||
"deliveryDaysMin": 1,
|
"deliveryDaysMin": 1,
|
||||||
"deliveryDaysMax": 2,
|
"deliveryDaysMax": 2,
|
||||||
"tariffCode": "tariff-guid-2",
|
"tariffCode": "ДоставкаДоДверей|urg-exp",
|
||||||
},
|
},
|
||||||
"parcelType": "parcel",
|
"parcelType": "parcel",
|
||||||
"weight": "1.0",
|
"weight": "1.0",
|
||||||
@@ -204,7 +236,7 @@ def test_provider_get_payment_price_selects_matching_tariff() -> None:
|
|||||||
price = asyncio.run(provider.get_payment_price(request))
|
price = asyncio.run(provider.get_payment_price(request))
|
||||||
|
|
||||||
assert price is not None
|
assert price is not None
|
||||||
assert price.tariff_code == "tariff-guid-2"
|
assert price.tariff_code == "ДоставкаДоДверей|urg-exp"
|
||||||
assert price.price == Decimal("2000.00")
|
assert price.price == Decimal("2000.00")
|
||||||
|
|
||||||
|
|
||||||
@@ -221,9 +253,38 @@ def test_provider_register_order_returns_document_number() -> None:
|
|||||||
assert http_client.calls[0]["url"] == "http://lk-test.cse.ru/1c/ws/web1c.1cws"
|
assert http_client.calls[0]["url"] == "http://lk-test.cse.ru/1c/ws/web1c.1cws"
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_order_request_uses_selected_tariff_urgency() -> None:
|
||||||
|
request = make_init_payment_request(
|
||||||
|
systemData={
|
||||||
|
"tariff": {
|
||||||
|
"provider": "cse",
|
||||||
|
"serviceName": "Экспресс",
|
||||||
|
"price": 200000,
|
||||||
|
"deliveryDaysMin": 1,
|
||||||
|
"deliveryDaysMax": 2,
|
||||||
|
"tariffCode": "ДоставкаДоДверей|urg-exp",
|
||||||
|
},
|
||||||
|
"parcelType": "parcel",
|
||||||
|
"weight": "1.0",
|
||||||
|
"dimensions": {"length": "10", "width": "10", "height": "10"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
body = order_mapper.map_cse_save_order_request(request, "order-uuid-1", _params())
|
||||||
|
order = body["data"].items[0]
|
||||||
|
|
||||||
|
assert order.field_value("Urgency") == "urg-exp"
|
||||||
|
assert order.field_value("DeliveryOfCargo") == "ДоставкаДоДверей"
|
||||||
|
assert order.field_value("Payer") == "payer-1"
|
||||||
|
assert order.field_value("ShippingMethod") == "sm-1"
|
||||||
|
|
||||||
|
|
||||||
def test_application_error_in_response_raises_request_error() -> None:
|
def test_application_error_in_response_raises_request_error() -> None:
|
||||||
response = httpx.Response(200, text=_ERROR_RESPONSE)
|
client, _ = _build_client(
|
||||||
client, _ = _build_client([response])
|
[
|
||||||
|
httpx.Response(200, text=_DELIVERY_TYPES_RESPONSE),
|
||||||
|
httpx.Response(200, text=_ERROR_RESPONSE),
|
||||||
|
]
|
||||||
|
)
|
||||||
provider = CSEProvider(client)
|
provider = CSEProvider(client)
|
||||||
|
|
||||||
with pytest.raises(CSERequestError):
|
with pytest.raises(CSERequestError):
|
||||||
|
|||||||
Reference in New Issue
Block a user