добавлено получение накладной в cse
Deploy / deploy (push) Successful in 2m33s

This commit is contained in:
Раис Юсупалиев
2026-06-27 07:10:31 +03:00
parent 2d8f31d3d9
commit facdde00c9
14 changed files with 581 additions and 100 deletions
+37 -1
View File
@@ -21,12 +21,17 @@ from app.adapters.delivery_providers.cse.mapper import (
map_cse_calc_response_for_tariff_code,
)
from app.adapters.delivery_providers.cse.order_mapper import (
CSEOrderInfo,
CSEOrderRegistrationParams,
CSEOrderRegistrationResult,
build_calc_body_for_calculation,
build_calc_body_for_payment,
build_tracking_body_for_order,
build_waybill_print_form_body,
map_cse_tracking_response,
map_cse_save_order_request,
map_cse_save_order_response,
map_cse_waybill_print_form_response,
split_tariff_code,
)
from app.adapters.delivery_providers.cse.soap import (
@@ -113,6 +118,30 @@ class CSEClient:
except CSEMappingError as exc:
raise CSEClientError(str(exc)) from exc
async def get_order(self, order_number: str) -> CSEOrderInfo:
root = await self._post(
"Tracking",
build_tracking_body_for_order(order_number),
request_error_message="CSE Tracking request was rejected with status",
)
try:
return map_cse_tracking_response(root)
except CSEMappingError as exc:
raise CSEClientError(str(exc)) from exc
async def download_waybill_pdf(self, waybill_number: str) -> bytes:
root = await self._post(
"GetFormsForDocuments",
build_waybill_print_form_body(waybill_number),
request_error_message=(
"CSE GetFormsForDocuments request was rejected with status"
),
)
try:
return map_cse_waybill_print_form_response(root)
except CSEMappingError as exc:
raise CSEClientError(str(exc)) from exc
async def _post(
self,
operation: str,
@@ -152,7 +181,8 @@ class CSEClient:
status_code=response.status_code,
)
raise CSEClientError(
f"CSE {operation} request failed with status {response.status_code}."
f"CSE {operation} request failed with status "
f"{response.status_code}."
)
if 400 <= response.status_code < 500:
@@ -312,3 +342,9 @@ class CSEProvider(DeliveryProvider):
self, request: InitPaymentRequest, order_uuid: str
) -> CSEOrderRegistrationResult:
return await self._client.save_order(request, order_uuid)
async def get_order(self, order_number: str) -> CSEOrderInfo:
return await self._client.get_order(order_number)
async def download_waybill_pdf(self, waybill_number: str) -> bytes:
return await self._client.download_waybill_pdf(waybill_number)
@@ -1,5 +1,7 @@
"""CSE Calc and SaveDocuments payload mappers."""
"""CSE Calc, SaveDocuments and waybill payload mappers."""
import base64
import binascii
from dataclasses import dataclass
from app.adapters.delivery_providers.cse.constants import (
@@ -32,6 +34,13 @@ class CSEOrderRegistrationResult:
order_number: str
@dataclass(frozen=True)
class CSEOrderInfo:
order_number: str
status_code: str | None
waybill_number: str | None
def build_calc_body_for_calculation(
request: DeliveryCalculationRequest,
delivery_type: str = "",
@@ -176,6 +185,73 @@ def map_cse_save_order_response(root: Element) -> CSEOrderRegistrationResult:
raise CSEMappingError("CSE SaveDocuments response is missing document Number.")
def build_tracking_body_for_order(order_number: str) -> dict[str, Element]:
return {
"documents": Element(
key="Documents",
items=[Element(key=order_number)],
),
"parameters": Element(
key="parameters",
items=[
make_field("DocumentType", "Order"),
make_field("OnlySelectedType", True, "boolean"),
],
),
}
def map_cse_tracking_response(root: Element) -> CSEOrderInfo:
for document in root.items:
order_number = document.property_value("Number") or document.key
if not order_number:
continue
return CSEOrderInfo(
order_number=order_number,
status_code=_latest_tracking_status(document),
waybill_number=_extract_waybill_number(document),
)
raise CSEMappingError("CSE Tracking response is missing document data.")
def build_waybill_print_form_body(waybill_number: str) -> dict[str, Element]:
return {
"documents": Element(
key="Documents",
items=[Element(key=waybill_number)],
),
"parameters": Element(
key="parameters",
items=[
make_field("DocumentType", "waybill"),
make_field("Type", "print"),
make_field(
"Name",
"Универсальная печатная "
"форма документа НАКЛАДНАЯ",
),
make_field("Format", "pdf"),
make_field("OnlySelectedType", True, "boolean"),
],
),
}
def map_cse_waybill_print_form_response(root: Element) -> bytes:
for document in root.items:
bdata = document.bdata
if not bdata:
continue
try:
normalized_bdata = "".join(bdata.split())
return base64.b64decode(normalized_bdata, validate=True)
except (binascii.Error, ValueError) as exc:
raise CSEMappingError(
"CSE GetFormsForDocuments response contains invalid BData."
) from exc
raise CSEMappingError("CSE GetFormsForDocuments response is missing BData.")
def _parcel_type(request: DeliveryCalculationRequest) -> str | None:
return request.parcel_type.value if request.parcel_type is not None else None
@@ -190,3 +266,30 @@ def _compose_address(address: Address) -> str:
def _format_decimal(value: float) -> str:
return format(value, "g")
def _latest_tracking_status(document: Element) -> str | None:
dated: list[tuple[str, str]] = []
for state in document.items:
if not state.key:
continue
date_time = state.property_value("DateTime") or ""
dated.append((date_time, state.key))
if not dated:
return None
dated.sort(key=lambda item: item[0])
return dated[-1][1]
def _extract_waybill_number(document: Element) -> str | None:
for table in document.tables:
if table.key != "Waybills":
continue
for waybill in table.items:
document_type = waybill.property_value("DocumentType")
if document_type is not None and document_type.lower() != "waybill":
continue
number = waybill.property_value("Number") or waybill.key
if number:
return number
return None
@@ -18,6 +18,7 @@ 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")
_BINARY_TAG = "BData"
@dataclass
@@ -27,6 +28,7 @@ class Element:
key: str | None = None
value: str | None = None
value_type: str | None = None
bdata: str | None = None
fields: list["Element"] = field(default_factory=list)
items: list["Element"] = field(default_factory=list) # <List>
tables: list["Element"] = field(default_factory=list)
@@ -159,6 +161,8 @@ def _parse_element(node: ET.Element) -> Element:
element.tables.append(_parse_element(child))
elif local == "Properties":
element.properties.append(_parse_element(child))
elif local == _BINARY_TAG:
element.bdata = (child.text or "").strip()
return element