"""审批流集成测试(二次分配方案为首个接入对象)。 守的是(计划 T4 验收矩阵): - 开关关闭(种子默认 0):提单即「自动通过」——status=3 且两级节点 action=3 留痕(留痕不断链,关闭=直接生效的需求口径) - 开关开启:status=1 → 财务通过 → status=2 → 领导通过 → status=3 - 驳回:整体驳回 status=4,意见留痕;驳回必填意见(空驳 400) - 节点准入:status=1 仅财务、status=2 仅领导;**admin 不代办**(403); 普通用户/营销后勤进不了模块(403) - 再编辑撤旧建新:在途单 status=5 + node=0/action=4 撤回留痕,新单新建 - 方案为空不提单 - 开关读一次:同一提交路径内参数变化不影响判定(种子级默认行为锁定) 角色用户经 make_user 造(finance/leader/marketing_ops 三新角色种子在库)。 开关联动走参数变更批次(PUT /system/params 端到端,不直改库)。 """ import uuid import pytest from fastapi.testclient import TestClient from tests.integration.factories import business_payload, customer pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("legacy_calculation")] _BUSINESSES = "/api/v1/business/businesses" _APPROVALS = "/api/v1/system/approvals" _PARAMS = "/api/v1/system/params" _PERSONNEL = "/api/v1/base/personnel" def _person(client: TestClient, headers: dict) -> dict: resp = client.post( _PERSONNEL, json={ "personnel_name": f"t_审批_{uuid.uuid4().hex[:8]}", "business_role_id": _any_role_id(), "salary_standard": "3000.00", }, headers=headers, ) assert resp.status_code == 200, resp.text return resp.json()["data"] def _any_role_id() -> int: from sqlalchemy import select from app.core.constants import BUSINESS_ROLE_SALESPERSON from app.core.db import get_session_factory from app.models import BusinessRole factory = get_session_factory() with factory() as session: return int( session.execute( select(BusinessRole.id).where( BusinessRole.role_code == BUSINESS_ROLE_SALESPERSON, BusinessRole.status == 1, ) ).scalar_one() ) def _create_with_plan(client: TestClient, headers: dict, personnel_id: int) -> dict: cust = customer(client, headers) resp = client.post( _BUSINESSES, json=business_payload( cust["id"], secondary_allocations=[{"personnel_id": personnel_id, "ratio": "50.00"}], ), headers=headers, ) assert resp.status_code == 200, resp.text return resp.json()["data"] def _latest_order(client: TestClient, headers: dict, refer_no: str) -> dict: resp = client.get(_APPROVALS, params={"keyword": refer_no, "pageSize": 1}, headers=headers) assert resp.status_code == 200, resp.text rows = resp.json()["data"]["list"] assert rows, f"未找到审批单:{refer_no}" detail = client.get(f"{_APPROVALS}/{rows[0]['id']}", headers=headers) return detail.json()["data"] def _set_switch(client: TestClient, admin_headers: dict, value: str) -> None: """经参数变更批次把二次分配审批开关置值(立即生效)。""" resp = client.post( f"{_PARAMS}/changes", json={ "change_type": 1, "remark": f"测试切换审批开关={value}", "items": [ { "param_code": "approval.secondary_allocation.enabled", "dimension_value": "", "value": value, } ], }, headers=admin_headers, ) assert resp.status_code == 200, resp.text @pytest.fixture def finance(make_user, auth_headers) -> dict: """财务角色账号(审批第二节点)。""" user = make_user(["finance"]) user["headers"] = auth_headers(user) return user @pytest.fixture def leader(make_user, auth_headers) -> dict: """领导角色账号(审批第三节点)。""" user = make_user(["leader"]) user["headers"] = auth_headers(user) return user @pytest.fixture def switched_on(client: TestClient, admin: dict) -> None: """把二次分配审批开关置 1(立即生效;用例结束由 conftest 恢复种子基线)。""" _set_switch(client, admin["headers"], "1") class TestSwitchOff: """开关关闭(用例显式设置,不依赖种子默认值):提单即自动通过。""" def test_创建带方案_自动通过全节点留痕( self, client: TestClient, admin: dict, finance: dict ) -> None: _set_switch(client, admin["headers"], "0") # V8 起种子默认开启 p = _person(client, admin["headers"]) data = _create_with_plan(client, admin["headers"], p["id"]) order = _latest_order(client, finance["headers"], data["business_no"]) assert order["status"] == 3 assert order["order_no"].startswith("AP") auto_steps = [s for s in order["steps"] if s["action"] == 3] assert len(auto_steps) == 2 assert {s["node"] for s in auto_steps} == {1, 2} assert all(s["operator_name"] == "系统" for s in auto_steps) def test_方案为空不提单(self, client: TestClient, admin: dict, finance: dict) -> None: cust = customer(client, admin["headers"]) resp = client.post(_BUSINESSES, json=business_payload(cust["id"]), headers=admin["headers"]) assert resp.status_code == 200 rows = client.get( _APPROVALS, params={"keyword": resp.json()["data"]["business_no"]}, headers=finance["headers"], ).json()["data"]["list"] assert rows == [] class TestSwitchOn: """开关开启:两节点人工流转。""" @pytest.fixture def switched_on(self, client: TestClient, admin: dict) -> None: _set_switch(client, admin["headers"], "1") def test_两节点流转至通过( self, client: TestClient, admin: dict, finance, leader, switched_on ) -> None: p = _person(client, admin["headers"]) data = _create_with_plan(client, admin["headers"], p["id"]) order = _latest_order(client, finance["headers"], data["business_no"]) assert order["status"] == 1 # 财务通过 → 待领导 resp = client.post( f"{_APPROVALS}/{order['id']}/action", json={"action": 1, "opinion": "同意"}, headers=finance["headers"], ) assert resp.status_code == 200, resp.text assert resp.json()["data"]["status"] == 2 # 领导通过 → 已通过 resp = client.post( f"{_APPROVALS}/{order['id']}/action", json={"action": 1}, headers=leader["headers"], ) assert resp.status_code == 200, resp.text assert resp.json()["data"]["status"] == 3 manual = [s for s in resp.json()["data"]["steps"] if s["action"] == 1] assert {s["node"] for s in manual} == {1, 2} def test_财务驳回_整体驳回且意见留痕( self, client: TestClient, admin: dict, finance, leader, switched_on ) -> None: _ = leader p = _person(client, admin["headers"]) data = _create_with_plan(client, admin["headers"], p["id"]) order = _latest_order(client, finance["headers"], data["business_no"]) resp = client.post( f"{_APPROVALS}/{order['id']}/action", json={"action": 2, "opinion": "比例分配不合理"}, headers=finance["headers"], ) assert resp.status_code == 200, resp.text out = resp.json()["data"] assert out["status"] == 4 rejected = [s for s in out["steps"] if s["action"] == 2] assert rejected and rejected[0]["opinion"] == "比例分配不合理" def test_驳回缺意见被拒( self, client: TestClient, admin: dict, finance, leader, switched_on ) -> None: p = _person(client, admin["headers"]) data = _create_with_plan(client, admin["headers"], p["id"]) order = _latest_order(client, finance["headers"], data["business_no"]) resp = client.post( f"{_APPROVALS}/{order['id']}/action", json={"action": 2}, headers=finance["headers"], ) assert resp.status_code == 400 def test_节点准入_领导不能审财务节点_admin不代办( self, client: TestClient, admin: dict, finance, leader, switched_on ) -> None: p = _person(client, admin["headers"]) data = _create_with_plan(client, admin["headers"], p["id"]) order = _latest_order(client, leader["headers"], data["business_no"]) # status=1 是财务节点:领导不行,admin 也不代办 for headers in (leader["headers"], admin["headers"]): resp = client.post( f"{_APPROVALS}/{order['id']}/action", json={"action": 1}, headers=headers, ) assert resp.status_code == 403 def test_重复操作已通过单被拒( self, client: TestClient, admin: dict, finance, leader, switched_on ) -> None: p = _person(client, admin["headers"]) data = _create_with_plan(client, admin["headers"], p["id"]) order = _latest_order(client, finance["headers"], data["business_no"]) client.post( f"{_APPROVALS}/{order['id']}/action", json={"action": 1}, headers=finance["headers"] ) client.post( f"{_APPROVALS}/{order['id']}/action", json={"action": 1}, headers=leader["headers"] ) resp = client.post( f"{_APPROVALS}/{order['id']}/action", json={"action": 1}, headers=finance["headers"] ) assert resp.status_code == 400 def test_驳回后方案行清空_不留在生效表( self, client: TestClient, admin: dict, finance, switched_on ) -> None: """被否决的方案不得以「当前生效行」身份留在表里(M3 快照会取到)。""" p = _person(client, admin["headers"]) data = _create_with_plan(client, admin["headers"], p["id"]) order = _latest_order(client, finance["headers"], data["business_no"]) resp = client.post( f"{_APPROVALS}/{order['id']}/action", json={"action": 2, "opinion": "方案不合理"}, headers=finance["headers"], ) assert resp.status_code == 200 detail = client.get(f"{_BUSINESSES}/{data['id']}", headers=admin["headers"]).json()["data"] assert detail["secondary_allocations"] == [], "被驳回的方案应从生效表清空" assert detail["allocation_hint"] is None class TestResubmit: """再编辑撤旧建新 / 清空方案撤在途单。""" def test_在途单撤回并新建( self, client: TestClient, admin: dict, finance, leader, switched_on ) -> None: p = _person(client, admin["headers"]) data = _create_with_plan(client, admin["headers"], p["id"]) first = _latest_order(client, finance["headers"], data["business_no"]) assert first["status"] == 1 # 再编辑(仍带方案)→ 旧在途单撤回、新单待审 detail = client.get(f"{_BUSINESSES}/{data['id']}", headers=admin["headers"]).json()["data"] payload = business_payload( detail["customer_id"], secondary_allocations=[{"personnel_id": p["id"], "ratio": "80.00"}], ) resp = client.put(f"{_BUSINESSES}/{data['id']}", json=payload, headers=admin["headers"]) assert resp.status_code == 200, resp.text rows = client.get( _APPROVALS, params={"keyword": data["business_no"], "pageSize": 10}, headers=finance["headers"], ).json()["data"]["list"] statuses = {row["status"] for row in rows} assert 5 in statuses and 1 in statuses, f"未见撤回+新建:{statuses}" old_detail = client.get(f"{_APPROVALS}/{first['id']}", headers=finance["headers"]).json() withdraw_steps = [s for s in old_detail["data"]["steps"] if s["action"] == 4] assert withdraw_steps and withdraw_steps[0]["node"] == 0 def test_编辑清空方案_在途单撤回不再悬空( self, client: TestClient, admin: dict, finance, switched_on ) -> None: """空数组=清空方案:旧在途单必须撤回,否则挂着不存在的方案走审批。""" p = _person(client, admin["headers"]) data = _create_with_plan(client, admin["headers"], p["id"]) first = _latest_order(client, finance["headers"], data["business_no"]) assert first["status"] == 1 detail = client.get(f"{_BUSINESSES}/{data['id']}", headers=admin["headers"]).json()["data"] payload = business_payload(detail["customer_id"], secondary_allocations=[]) resp = client.put(f"{_BUSINESSES}/{data['id']}", json=payload, headers=admin["headers"]) assert resp.status_code == 200, resp.text rows = client.get( _APPROVALS, params={"keyword": data["business_no"], "pageSize": 10}, headers=finance["headers"], ).json()["data"]["list"] statuses = {row["status"] for row in rows} assert statuses == {5}, f"清空方案后旧在途单应撤回且无新单:{statuses}" assert ( client.get(f"{_APPROVALS}/{first['id']}", headers=finance["headers"]).json()["data"][ "status" ] == 5 ) def test_删除业务在途单禁删_终态单随删( self, client: TestClient, admin: dict, finance, switched_on ) -> None: """ref_id 固化无外键:在途单禁删(防指向已删业务的活单继续被审批), 终态单随业务删除一并清理(已通过/已驳回的历史事实由留痕日志承载)。""" p = _person(client, admin["headers"]) data = _create_with_plan(client, admin["headers"], p["id"]) # 在途:禁删 resp = client.delete(f"{_BUSINESSES}/{data['id']}", headers=admin["headers"]) assert resp.status_code == 400 assert "在途审批单" in resp.json()["message"] # 驳回(终态)后:可删,且审批单随之清理 order = _latest_order(client, finance["headers"], data["business_no"]) client.post( f"{_APPROVALS}/{order['id']}/action", json={"action": 2, "opinion": "驳回后可删"}, headers=finance["headers"], ) resp = client.delete(f"{_BUSINESSES}/{data['id']}", headers=admin["headers"]) assert resp.status_code == 200, resp.text rows = client.get( _APPROVALS, params={"keyword": data["business_no"]}, headers=finance["headers"], ).json()["data"]["list"] assert rows == [], "终态审批单应随业务删除一并清理" class TestModuleAccess: """模块准入:普通用户与营销后勤不可见(权限清单待澄清 #6)。""" def test_普通用户403(self, client: TestClient, normal: dict) -> None: assert client.get(_APPROVALS, headers=normal["headers"]).status_code == 403 def test_营销后勤403_暂不可见(self, client: TestClient, make_user, auth_headers) -> None: ops = make_user(["marketing_ops"]) ops["headers"] = auth_headers(ops) assert client.get(_APPROVALS, headers=ops["headers"]).status_code == 403 def test_财务与领导可见(self, client: TestClient, make_user, auth_headers) -> None: for code in ("finance", "leader"): user = make_user([code]) user["headers"] = auth_headers(user) assert client.get(_APPROVALS, headers=user["headers"]).status_code == 200 class TestContentSnapshot: """审批单内容快照与版本号(code-review 遗留②)。 守的是:复核者看到的是**送审时**的方案,而不是审批期间被改过的当前 方案;历史单(升级前无快照)必须显式标注「当前内容回退」,不能把 当前内容冒充送审内容。 """ def test_提交时快照固化_再改方案不回写旧单( self, client: TestClient, admin: dict, finance: dict, switched_on ) -> None: p = _person(client, admin["headers"]) data = _create_with_plan(client, admin["headers"], p["id"]) first = _latest_order(client, finance["headers"], data["business_no"]) assert first["version_no"] == 1 assert first["content_source"] == "snapshot" snapshot = first["snapshot"] assert snapshot["schema_version"] == 1 assert snapshot["order_type"] == 1 assert snapshot["captured_at"], "快照须固化采集时间" assert snapshot["data"]["business_no"] == data["business_no"] assert snapshot["data"]["allocations"][0]["ratio"] == "50.00" assert snapshot["data"]["allocation_total"] == "50.00" assert snapshot["data"]["allocation_balance"] == "50.00" # 编辑方案为 80%:旧在途单撤回,新单 V2 快照取新值;旧单快照不变 detail = client.get(f"{_BUSINESSES}/{data['id']}", headers=admin["headers"]).json()["data"] payload = business_payload( detail["customer_id"], secondary_allocations=[{"personnel_id": p["id"], "ratio": "80.00"}], ) resp = client.put(f"{_BUSINESSES}/{data['id']}", json=payload, headers=admin["headers"]) assert resp.status_code == 200, resp.text second = _latest_order(client, finance["headers"], data["business_no"]) assert second["version_no"] == 2 assert second["snapshot"]["data"]["allocations"][0]["ratio"] == "80.00" old = client.get(f"{_APPROVALS}/{first['id']}", headers=finance["headers"]).json()["data"] assert old["status"] == 5, "重新提交须撤回旧在途单" assert old["snapshot"]["data"]["allocations"][0]["ratio"] == "50.00" def test_历史单无快照_回退当前内容并标注( self, client: TestClient, admin: dict, finance: dict, switched_on ) -> None: """升级前的审批单没有快照列:回退读当前内容,但来源必须标 live。""" from app.core.db import get_session_factory from app.models import ApprovalOrder p = _person(client, admin["headers"]) data = _create_with_plan(client, admin["headers"], p["id"]) order = _latest_order(client, finance["headers"], data["business_no"]) factory = get_session_factory() with factory() as session: row = session.get(ApprovalOrder, order["id"]) assert row is not None row.snapshot_json = None session.commit() detail = client.get(f"{_APPROVALS}/{order['id']}", headers=finance["headers"]).json()[ "data" ] assert detail["content_source"] == "live" assert detail["snapshot"]["captured_at"] is None assert detail["snapshot"]["data"]["business_no"] == data["business_no"] def test_引用对象缺失且无快照_内容缺失( self, client: TestClient, admin: dict, finance: dict, switched_on ) -> None: from app.core.db import get_session_factory from app.models import ApprovalOrder p = _person(client, admin["headers"]) data = _create_with_plan(client, admin["headers"], p["id"]) order = _latest_order(client, finance["headers"], data["business_no"]) factory = get_session_factory() with factory() as session: row = session.get(ApprovalOrder, order["id"]) assert row is not None row.snapshot_json = None row.ref_id = 999_999_999 session.commit() detail = client.get(f"{_APPROVALS}/{order['id']}", headers=finance["headers"]).json()[ "data" ] assert detail["content_source"] == "missing" assert detail["snapshot"] is None def _payload_from_detail(detail: dict, **overrides: object) -> dict: """按业务详情原样重建提交载荷(明细行照抄,用于「内容未变」的场景)。""" items = [ { "product_name": it["product_name"], "product_category": it["product_category"], "quantity": it["quantity"], "sale_price": it["sale_price"], } for it in detail["items"] ] body = business_payload( detail["customer_id"], items=items, secondary_allocations=detail["secondary_allocations"], ) body.update(overrides) return body class TestNoRedundantResubmit: """送审内容未变不重提、方案未变不新增版本(版本与留痕降噪)。 守的是:反复保存同一份业务(只改备注)不应刷出一串方案版本与 「撤回+新建」的审批留痕;而内容真变了(金额或方案)必须重提, 否则审批人看到的是过期内容。 """ def test_方案与金额均未变_不新增版本也不重提( self, client: TestClient, admin: dict, finance: dict, switched_on ) -> None: p = _person(client, admin["headers"]) data = _create_with_plan(client, admin["headers"], p["id"]) first = _latest_order(client, finance["headers"], data["business_no"]) assert first["status"] == 1 detail = client.get(f"{_BUSINESSES}/{data['id']}", headers=admin["headers"]).json()["data"] payload = _payload_from_detail(detail) payload["remark"] = "只改了备注" resp = client.put(f"{_BUSINESSES}/{data['id']}", json=payload, headers=admin["headers"]) assert resp.status_code == 200, resp.text # 方案版本不增 after = client.get(f"{_BUSINESSES}/{data['id']}", headers=admin["headers"]).json()["data"] assert [(h["version_no"], h["is_active"]) for h in after["allocation_history"]] == [ (1, 1) ], "方案内容未变不应新增版本行" # 审批单不重提:仍是原来那一张、仍待财务复核 rows = client.get( _APPROVALS, params={"keyword": data["business_no"], "pageSize": 10}, headers=finance["headers"], ).json()["data"]["list"] assert len(rows) == 1, f"送审内容未变不应重提,实得 {len(rows)} 张单" assert rows[0]["id"] == first["id"] assert rows[0]["status"] == 1, "在途单不应被撤回" assert rows[0]["version_no"] == 1 def test_只改金额不改方案_方案版本不变但审批单重提( self, client: TestClient, admin: dict, finance: dict, switched_on ) -> None: """金额变了 → 送审内容变了 → 必须重提;但方案本身没变 → 不新增版本。""" p = _person(client, admin["headers"]) data = _create_with_plan(client, admin["headers"], p["id"]) first = _latest_order(client, finance["headers"], data["business_no"]) detail = client.get(f"{_BUSINESSES}/{data['id']}", headers=admin["headers"]).json()["data"] payload = _payload_from_detail(detail) payload["items"][0]["sale_price"] = "200.00" resp = client.put(f"{_BUSINESSES}/{data['id']}", json=payload, headers=admin["headers"]) assert resp.status_code == 200, resp.text after = client.get(f"{_BUSINESSES}/{data['id']}", headers=admin["headers"]).json()["data"] assert [(h["version_no"], h["is_active"]) for h in after["allocation_history"]] == [ (1, 1) ], "方案未变不应新增版本行" second = _latest_order(client, finance["headers"], data["business_no"]) assert second["version_no"] == 2, "金额变了必须重提" assert second["snapshot"]["data"]["sale_amount"] == "20000.00" old = client.get(f"{_APPROVALS}/{first['id']}", headers=finance["headers"]).json()["data"] assert old["status"] == 5 def test_改方案比例_方案版本与审批单版本同时递增( self, client: TestClient, admin: dict, finance: dict, switched_on ) -> None: p = _person(client, admin["headers"]) data = _create_with_plan(client, admin["headers"], p["id"]) first = _latest_order(client, finance["headers"], data["business_no"]) detail = client.get(f"{_BUSINESSES}/{data['id']}", headers=admin["headers"]).json()["data"] payload = _payload_from_detail( detail, secondary_allocations=[{"personnel_id": p["id"], "ratio": "80.00"}] ) resp = client.put(f"{_BUSINESSES}/{data['id']}", json=payload, headers=admin["headers"]) assert resp.status_code == 200, resp.text after = client.get(f"{_BUSINESSES}/{data['id']}", headers=admin["headers"]).json()["data"] assert [(h["version_no"], h["is_active"]) for h in after["allocation_history"]] == [ (1, 0), (2, 1), ], "方案变了应新增版本并把旧行置历史" second = _latest_order(client, finance["headers"], data["business_no"]) assert second["version_no"] == 2 old = client.get(f"{_APPROVALS}/{first['id']}", headers=finance["headers"]).json()["data"] assert old["status"] == 5 def test_无在途单时保存仍提新单( self, client: TestClient, admin: dict, finance: dict, leader: dict, switched_on ) -> None: """保守规则:只有「存在在途单且内容一致」才不重提。 方案已审批通过后再保存,仍按原语义重新送审。""" p = _person(client, admin["headers"]) data = _create_with_plan(client, admin["headers"], p["id"]) order = _latest_order(client, finance["headers"], data["business_no"]) client.post( f"{_APPROVALS}/{order['id']}/action", json={"action": 1}, headers=finance["headers"] ) client.post( f"{_APPROVALS}/{order['id']}/action", json={"action": 1}, headers=leader["headers"] ) detail = client.get(f"{_BUSINESSES}/{data['id']}", headers=admin["headers"]).json()["data"] payload = _payload_from_detail(detail) payload["remark"] = "已通过后再保存" resp = client.put(f"{_BUSINESSES}/{data['id']}", json=payload, headers=admin["headers"]) assert resp.status_code == 200, resp.text rows = client.get( _APPROVALS, params={"keyword": data["business_no"], "pageSize": 10}, headers=finance["headers"], ).json()["data"]["list"] assert len(rows) == 2, "无在途单时保存应重新送审" assert max(r["version_no"] for r in rows) == 2 class TestPendingSnapshotMatch: """在途单送审内容一致性判定(approval_service.pending_matches_snapshot)。""" def test_三种情形(self, client: TestClient, admin: dict, finance: dict, switched_on) -> None: from app.core.db import get_session_factory from app.services import approval_service p = _person(client, admin["headers"]) data = _create_with_plan(client, admin["headers"], p["id"]) order = _latest_order(client, finance["headers"], data["business_no"]) snapshot = order["snapshot"]["data"] factory = get_session_factory() with factory() as session: # 1) 内容一致 → True assert ( approval_service.pending_matches_snapshot( session, order_type=1, ref_id=data["id"], snapshot=snapshot, ) is True ) # 2) 内容不同 → False assert ( approval_service.pending_matches_snapshot( session, order_type=1, ref_id=data["id"], snapshot={"allocations": []} ) is False ) # 3) 无在途单 → False(引用对象换成别的) assert ( approval_service.pending_matches_snapshot( session, order_type=1, ref_id=999_999_999, snapshot=snapshot ) is False ) # 4) 快照为 None → False assert ( approval_service.pending_matches_snapshot( session, order_type=1, ref_id=data["id"], snapshot=None ) is False ) class TestAllocationVersions: """二次分配方案版本化(code-review 遗留①)。 守的是:编辑替换与驳回清空只把旧行置历史(is_active=0),不物理删除, 方案参与人与比例可回溯;生效行始终唯一,核算取数不受历史行干扰。 """ def test_编辑方案产生新版本_旧版本保留( self, client: TestClient, admin: dict, switched_on ) -> None: p = _person(client, admin["headers"]) data = _create_with_plan(client, admin["headers"], p["id"]) detail = client.get(f"{_BUSINESSES}/{data['id']}", headers=admin["headers"]).json()["data"] assert [(h["version_no"], h["is_active"]) for h in detail["allocation_history"]] == [(1, 1)] payload = business_payload( detail["customer_id"], secondary_allocations=[{"personnel_id": p["id"], "ratio": "80.00"}], ) resp = client.put(f"{_BUSINESSES}/{data['id']}", json=payload, headers=admin["headers"]) assert resp.status_code == 200, resp.text detail = client.get(f"{_BUSINESSES}/{data['id']}", headers=admin["headers"]).json()["data"] history = [ (h["version_no"], h["is_active"], h["ratio"]) for h in detail["allocation_history"] ] assert history == [(1, 0, "50.00"), (2, 1, "80.00")] assert [a["ratio"] for a in detail["secondary_allocations"]] == ["80.00"] def test_驳回后生效行为空_历史行保留( self, client: TestClient, admin: dict, finance: dict, switched_on ) -> None: p = _person(client, admin["headers"]) data = _create_with_plan(client, admin["headers"], p["id"]) order = _latest_order(client, finance["headers"], data["business_no"]) resp = client.post( f"{_APPROVALS}/{order['id']}/action", json={"action": 2, "opinion": "方案不合理"}, headers=finance["headers"], ) assert resp.status_code == 200, resp.text detail = client.get(f"{_BUSINESSES}/{data['id']}", headers=admin["headers"]).json()["data"] assert detail["secondary_allocations"] == [], "被驳回方案不得留在生效行" assert detail["allocation_hint"] is None history = detail["allocation_history"] assert len(history) == 1 assert history[0]["version_no"] == 1 assert history[0]["is_active"] == 0 assert history[0]["ratio"] == "50.00"