feat: 接口4从表4组装合同协议报文

This commit is contained in:
Yuuuu 2026-10-05 22:38:14 +08:00
parent a80cce5fc5
commit 47ad02a6dc
3 changed files with 196 additions and 1 deletions

View File

@ -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

View File

@ -1,4 +1,4 @@
"""内存表 2。只给单测用。""" """内存表 2 / 表 4。只给单测用。"""
from __future__ import annotations from __future__ import annotations
@ -8,3 +8,11 @@ class MemoryCartSettlementRepo:
def list_unpushed(self) -> list[dict]: def list_unpushed(self) -> list[dict]:
return [dict(row) for row in self._rows if row.get("push_status", 0) == 0] 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]

View File

@ -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