"""把 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)