feat(t221): add exact-order erp connector
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
"""把 ERP 原始对象缩减为下游允许保存的货运字段。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from .errors import ERPProtocolError
|
||||
|
||||
|
||||
def normalize_freight_result(result: Mapping[str, Any]) -> dict[str, Any]:
|
||||
records = result.get("records")
|
||||
if not isinstance(records, list):
|
||||
raise ERPProtocolError("货运查询结果缺少 records 数组")
|
||||
|
||||
orders: list[dict[str, Any]] = []
|
||||
seen_orders: dict[str, dict[str, Any]] = {}
|
||||
for record in records:
|
||||
if not isinstance(record, Mapping):
|
||||
raise ERPProtocolError("货运查询结果包含非对象记录")
|
||||
stock = record.get("stock")
|
||||
detail = record.get("detail")
|
||||
if not isinstance(stock, Mapping) or not isinstance(detail, Mapping):
|
||||
raise ERPProtocolError("货运记录缺少 stock 或 detail 对象")
|
||||
|
||||
stock_id = _external_id(stock.get("id"), "货运单")
|
||||
detail_id = _external_id(detail.get("id"), "货运详情")
|
||||
if stock_id != detail_id:
|
||||
raise ERPProtocolError("货运列表和详情身份不一致")
|
||||
|
||||
raw_items = detail.get("details")
|
||||
if not isinstance(raw_items, list):
|
||||
raise ERPProtocolError("货运详情缺少 details 数组")
|
||||
item_by_id: dict[str, dict[str, Any]] = {}
|
||||
for raw_item in raw_items:
|
||||
if not isinstance(raw_item, Mapping):
|
||||
raise ERPProtocolError("货运商品明细包含非对象元素")
|
||||
item = _normalize_item(raw_item)
|
||||
existing = item_by_id.get(item["external_item_id"])
|
||||
if existing is not None and existing != item:
|
||||
raise ERPProtocolError("货运商品明细存在冲突的重复身份")
|
||||
item_by_id[item["external_item_id"]] = item
|
||||
|
||||
order = {
|
||||
"external_stock_id": stock_id,
|
||||
"source_code": _text(stock.get("code")),
|
||||
"platform_order_no": _optional_text(stock.get("orderCode")),
|
||||
"shop_name": _optional_text(
|
||||
detail.get("shopName")
|
||||
if _text(detail.get("shopName"))
|
||||
else stock.get("shopName")
|
||||
),
|
||||
"source_created_at": _optional_text(
|
||||
detail.get("created")
|
||||
if _text(detail.get("created"))
|
||||
else stock.get("created")
|
||||
),
|
||||
"order_status": _optional_scalar(
|
||||
stock.get("orderStatus")
|
||||
if stock.get("orderStatus") is not None
|
||||
else detail.get("status")
|
||||
),
|
||||
"purchase_status": _optional_scalar(stock.get("purchaseStatus")),
|
||||
"is_canceled": _optional_bool(stock.get("isCancel")),
|
||||
"items": sorted(
|
||||
item_by_id.values(),
|
||||
key=lambda value: _identity_sort_key(
|
||||
value["external_item_id"]
|
||||
),
|
||||
),
|
||||
}
|
||||
existing_order = seen_orders.get(stock_id)
|
||||
if existing_order is not None and existing_order != order:
|
||||
raise ERPProtocolError("货运查询结果存在冲突的重复货运单")
|
||||
seen_orders[stock_id] = order
|
||||
|
||||
orders.extend(
|
||||
sorted(
|
||||
seen_orders.values(),
|
||||
key=lambda value: _identity_sort_key(
|
||||
value["external_stock_id"]
|
||||
),
|
||||
)
|
||||
)
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"query": {"mode": "ORDER_NUMBER"},
|
||||
"orders": orders,
|
||||
}
|
||||
|
||||
|
||||
def _normalize_item(item: Mapping[str, Any]) -> dict[str, Any]:
|
||||
title = _text(item.get("productTitle"))
|
||||
if not title:
|
||||
title = _text(item.get("detailProductName"))
|
||||
sku = _text(item.get("sku"))
|
||||
if not sku:
|
||||
sku = _text(item.get("variationSku"))
|
||||
return {
|
||||
"external_item_id": _external_id(item.get("id"), "货运商品"),
|
||||
"title": title,
|
||||
"product_spec": _text(item.get("productSpec")),
|
||||
"sku": sku,
|
||||
"quantity": _positive_int_or_none(item.get("productQty")),
|
||||
"product_thumb_ref": _optional_scalar(item.get("productThumb")),
|
||||
"purchase_status": _optional_scalar(item.get("purchaseStatus")),
|
||||
}
|
||||
|
||||
|
||||
def _external_id(value: Any, label: str) -> str:
|
||||
if isinstance(value, bool):
|
||||
raise ERPProtocolError(f"{label}外部身份无效")
|
||||
try:
|
||||
normalized = int(value)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ERPProtocolError(f"{label}外部身份无效") from exc
|
||||
if normalized <= 0:
|
||||
raise ERPProtocolError(f"{label}外部身份无效")
|
||||
return str(normalized)
|
||||
|
||||
|
||||
def _positive_int_or_none(value: Any) -> int | None:
|
||||
if isinstance(value, bool):
|
||||
return None
|
||||
try:
|
||||
normalized = int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return normalized if normalized > 0 else None
|
||||
|
||||
|
||||
def _text(value: Any) -> str:
|
||||
return value.strip() if isinstance(value, str) else ""
|
||||
|
||||
|
||||
def _optional_text(value: Any) -> str | None:
|
||||
normalized = _text(value)
|
||||
return normalized or None
|
||||
|
||||
|
||||
def _optional_scalar(value: Any) -> str | None:
|
||||
if value is None or isinstance(value, (dict, list, tuple, set)):
|
||||
return None
|
||||
normalized = str(value).strip()
|
||||
return normalized or None
|
||||
|
||||
|
||||
def _optional_bool(value: Any) -> bool | None:
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if value in (0, "0"):
|
||||
return False
|
||||
if value in (1, "1"):
|
||||
return True
|
||||
return None
|
||||
|
||||
|
||||
def _identity_sort_key(value: str) -> tuple[int, str]:
|
||||
return (int(value), value)
|
||||
Reference in New Issue
Block a user