diff --git a/rpa_oms/app/outbound/contract_agreement.py b/rpa_oms/app/outbound/contract_agreement.py new file mode 100644 index 0000000..9756055 --- /dev/null +++ b/rpa_oms/app/outbound/contract_agreement.py @@ -0,0 +1,69 @@ +"""接口 4:从表 4 取未推行,组装文档报文。 + +同一非空 contractNo 必须在同一批 data 里。一批最多 500 条。 +空 contractNo(失败行)按行独立。不改 push_status,不发 HTTP。 +""" +from __future__ import annotations + +from collections import OrderedDict + +MAX_BATCH = 500 + + +def load_contract_agreement_envelope(repo, limit: int = MAX_BATCH) -> dict: + return build_contract_agreement_envelope(repo.list_unpushed(), limit=limit) + + +def build_contract_agreement_envelope(rows: list[dict], limit: int = MAX_BATCH) -> dict: + chosen = _take_complete_groups(rows, limit) + return {"data": [_to_item(row) for row in chosen]} + + +def _group_key(row: dict): + number = row.get("contract_no") + if number is None or str(number).strip() == "": + return ("fid", row["fid"]) + return ("no", str(number).strip()) + + +def _take_complete_groups(rows: list[dict], limit: int) -> list[dict]: + groups: OrderedDict = OrderedDict() + for row in rows: + groups.setdefault(_group_key(row), []).append(row) + batch: list[dict] = [] + for group in groups.values(): + if len(batch) + len(group) > limit and batch: + break + batch.extend(group) + return batch + + +def _to_item(row: dict) -> dict: + return { + "fid": _as_int(row["fid"]), + "omsId": _as_int(row["oms_id"]), + "actionCode": _as_int(row["action_code"]), + "contractStatus": _as_int(row.get("contract_status")), + "contractFailReason": _as_str(row.get("contract_fail_reason")), + "contractNo": _as_str(row.get("contract_no")), + "agreementStatus": _as_int(row.get("agreement_status")), + "agreementFailReason": _as_str(row.get("agreement_fail_reason")), + "agreementId": _as_str(row.get("agreement_id")), + "agreementDtlId": _as_str(row.get("agreement_dtl_id")), + "orgCode": _as_str(row.get("org_code")), + } + + +def _as_int(value): + if value is None: + return None + return int(value) + + +def _as_str(value): + if value is None: + return None + text = str(value) + if text.strip() == "": + return None + return text diff --git a/rpa_oms/app/outbound/memory_repo.py b/rpa_oms/app/outbound/memory_repo.py index 8b9996c..33cdb12 100644 --- a/rpa_oms/app/outbound/memory_repo.py +++ b/rpa_oms/app/outbound/memory_repo.py @@ -1,4 +1,4 @@ -"""内存表 2。只给单测用。""" +"""内存表 2 / 表 4。只给单测用。""" from __future__ import annotations @@ -8,3 +8,11 @@ class MemoryCartSettlementRepo: def list_unpushed(self) -> list[dict]: return [dict(row) for row in self._rows if row.get("push_status", 0) == 0] + + +class MemoryContractAgreementRepo: + def __init__(self, rows: list[dict] | None = None): + self._rows = [dict(row) for row in (rows or [])] + + def list_unpushed(self) -> list[dict]: + return [dict(row) for row in self._rows if row.get("push_status", 0) == 0] diff --git a/rpa_oms/tests/test_contract_agreement_outbound.py b/rpa_oms/tests/test_contract_agreement_outbound.py new file mode 100644 index 0000000..43509da --- /dev/null +++ b/rpa_oms/tests/test_contract_agreement_outbound.py @@ -0,0 +1,118 @@ +"""接口 4:从表 4 取 push_status=0,组装文档报文。不连 Oracle,不发 HTTP。""" +from __future__ import annotations + +from app.outbound.contract_agreement import ( + MAX_BATCH, + build_contract_agreement_envelope, + load_contract_agreement_envelope, +) +from app.outbound.memory_repo import MemoryContractAgreementRepo + + +def _row(**overrides): + row = { + "fid": 300001, + "oms_id": 20260928000001, + "action_code": 1, + "contract_status": 1, + "contract_fail_reason": None, + "contract_no": "HT1", + "agreement_status": None, + "agreement_fail_reason": None, + "agreement_id": None, + "agreement_dtl_id": None, + "org_code": "901", + "push_status": 0, + } + row.update(overrides) + return row + + +def test_assembles_unpushed_rows_as_document_envelope(): + repo = MemoryContractAgreementRepo( + [ + _row(), + _row( + fid=300002, + action_code=2, + contract_status=None, + contract_no="HT1", + agreement_status=1, + agreement_id="AGR1", + agreement_dtl_id="AGRD1", + ), + _row(fid=300003, push_status=1, contract_no="HT-SKIP"), + ] + ) + envelope = load_contract_agreement_envelope(repo) + assert envelope == { + "data": [ + { + "fid": 300001, + "omsId": 20260928000001, + "actionCode": 1, + "contractStatus": 1, + "contractFailReason": None, + "contractNo": "HT1", + "agreementStatus": None, + "agreementFailReason": None, + "agreementId": None, + "agreementDtlId": None, + "orgCode": "901", + }, + { + "fid": 300002, + "omsId": 20260928000001, + "actionCode": 2, + "contractStatus": None, + "contractFailReason": None, + "contractNo": "HT1", + "agreementStatus": 1, + "agreementFailReason": None, + "agreementId": "AGR1", + "agreementDtlId": "AGRD1", + "orgCode": "901", + }, + ] + } + assert isinstance(envelope["data"][0]["omsId"], int) + + +def test_oversized_contract_no_group_enters_when_batch_empty(): + rows = [ + _row(fid=1, oms_id=1, contract_no="HT-BIG"), + _row(fid=2, oms_id=2, contract_no="HT-BIG"), + _row(fid=3, oms_id=3, contract_no="HT-BIG"), + ] + envelope = build_contract_agreement_envelope(rows, limit=2) + assert [item["fid"] for item in envelope["data"]] == [1, 2, 3] + + +def test_does_not_split_contract_no_when_batch_already_has_rows(): + rows = [ + _row(fid=1, oms_id=1, contract_no="HT-A"), + _row(fid=2, oms_id=2, contract_no="HT-B"), + _row(fid=3, oms_id=3, contract_no="HT-B"), + ] + envelope = build_contract_agreement_envelope(rows, limit=2) + assert [item["fid"] for item in envelope["data"]] == [1] + + +def test_empty_contract_no_rows_are_independent(): + rows = [ + _row(fid=1, oms_id=1, contract_no=None, contract_status=3), + _row(fid=2, oms_id=2, contract_no="", contract_status=3), + _row(fid=3, oms_id=3, contract_no="HT-C"), + ] + envelope = build_contract_agreement_envelope(rows, limit=1) + assert [item["fid"] for item in envelope["data"]] == [1] + + +def test_batch_cap_is_500(): + assert MAX_BATCH == 500 + + +def test_payload_omits_push_status(): + item = build_contract_agreement_envelope([_row()])["data"][0] + assert "pushStatus" not in item + assert "push_status" not in item