# 接口 4:从表 4 取 push_status=0,组装文档报文。假连接录 SQL,不连真库,不发 HTTP。 from __future__ import annotations from app.outbound.contract_agreement import ( MAX_BATCH, batch_limit, build_contract_agreement_envelope, load_contract_agreement_envelope, ) from app.outbound.oracle_repo import OracleContractAgreementRepo from tests.fake_oracle import FakeDb # 构造一条表 4 行,供组装层单测用。 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 # 把表 4 行放进假库。list_unpushed 走真 SQL,调用记在 db.calls。 def _open(rows: list[dict]) -> tuple[OracleContractAgreementRepo, FakeDb]: db = FakeDb() for row in rows: db.agreements[row["fid"]] = row return OracleContractAgreementRepo(db.connection()), db # 取出最近一次 execute 的 SQL 和绑定。 def _last_execute(db: FakeDb) -> tuple[str, dict]: execute_calls = [call for call in db.calls if call[0] == "execute"] assert execute_calls _, sql, params = execute_calls[-1] return sql, params or {} # 未推行按文档信封字段组装;已推行跳过。 def test_assembles_unpushed_rows_as_document_envelope(): repo, db = _open( [ _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) sql, params = _last_execute(db) assert "rpa_contract_agreement_result" in sql.lower() assert "push_status = 0" in sql.lower() assert params["limit"] == 500 # 合同成功行和协议行可以同批。 def test_assembles_contract_success_and_agreement_rows(): repo, db = _open( [ _row(contract_status=2), _row( fid=300002, action_code=2, contract_status=None, contract_no="HT1", agreement_status=1, agreement_id="AGR1", agreement_dtl_id="AGRD1", ), ] ) envelope = load_contract_agreement_envelope(repo) assert envelope["data"][0]["actionCode"] == 1 assert envelope["data"][0]["contractStatus"] == 2 assert envelope["data"][1]["actionCode"] == 2 assert envelope["data"][1]["agreementStatus"] == 1 assert envelope["data"][1]["agreementId"] == "AGR1" assert envelope["data"][1]["agreementDtlId"] == "AGRD1" sql, _params = _last_execute(db) assert "rpa_contract_agreement_result" in sql.lower() # 空批时超限的同一 contractNo 仍整组进入。 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] # 批次已有行时,装不下的 contractNo 整组留下。 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] # 空 contractNo 按行独立,互不捆绑。 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] # 默认上限 500。 def test_batch_cap_is_500(): assert MAX_BATCH == 500 # 报文不含 push_status。 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 # 组装入口把 limit 传给仓库,让 SQL 侧截组。 def test_load_passes_limit_to_list_unpushed(): seen: dict = {} # 只记 list_unpushed 收到的 limit。 class SpyRepo: # 记下上限并返回空列表。 def list_unpushed(self, limit: int = 500) -> list[dict]: seen["limit"] = limit return [] load_contract_agreement_envelope(SpyRepo(), limit=3) assert seen["limit"] == 3 # OMS_OUTBOUND_BATCH_LIMIT 覆盖默认 500。 def test_batch_limit_reads_env(monkeypatch): monkeypatch.setenv("OMS_OUTBOUND_BATCH_LIMIT", "3") assert batch_limit() == 3