"""现金流量表结构分析模块测试 — 八类企业定性判定 (P1 2026-09-03) 覆盖 cashflow_structure.py: GET /api/cma/cashflow-structure/rules 八类规则表 GET /api/cma/cashflow-structure/periods 已注入期间 GET /api/cma/cashflow-structure/analysis 结构分析+类型判定+瀑布数据 POST /api/cma/cashflow-structure/values 三大活动净额注入(upsert) """ import pytest from fastapi.testclient import TestClient from sqlalchemy.orm import Session from tests.conftest import create_test_user, get_token_for_user, auth_header BASE = "/api/cma/cashflow-structure" class TestRules: def test_rules_8_types(self, client: TestClient, db: Session): create_test_user(db) token = get_token_for_user(client) resp = client.get(f"{BASE}/rules", headers=auth_header(token)) assert resp.status_code == 200 rules = resp.json()["rules"] assert len(rules) == 8 # 判定组合必须恰好 8 种且无重复 combos = [(r["o_sign"], r["i_sign"], r["f_sign"]) for r in rules] assert len(set(combos)) == 8 # 规范命名:不含民间叫法 assert all("现金流肖像" not in r["name"] for r in rules) def test_rules_unauthorized(self, client: TestClient): resp = client.get(f"{BASE}/rules") assert resp.status_code in (401, 403) class TestStructureAnalysis: def _post_value(self, client, token, **kw): payload = { "period": "2026-08", "operating_net": -10.0, "investing_net": 3.0, "financing_net": 12.0, # - + + → 类型7 输血生存型 "source": "official", "remark": "pytest 临时数据", } payload.update(kw) return client.post(f"{BASE}/values", headers=auth_header(token), json=payload) def test_empty_analysis(self, client: TestClient, db: Session): create_test_user(db) token = get_token_for_user(client) resp = client.get(f"{BASE}/analysis", headers=auth_header(token)) assert resp.status_code == 200 body = resp.json() assert body["data_available"] is False assert body["classification"] is None def test_upsert_and_classify_type7(self, client: TestClient, db: Session): create_test_user(db) token = get_token_for_user(client) resp = self._post_value(client, token) assert resp.status_code == 200 body = resp.json() assert body["ok"] is True assert body["classification"]["classified"] is True assert body["classification"]["type_no"] == 7 assert body["classification"]["name"] == "输血生存型" # 分析接口自动命中 r2 = client.get(f"{BASE}/analysis", headers=auth_header(token), params={"period": "2026-08"}) assert r2.status_code == 200 a = r2.json() assert a["data_available"] is True assert a["classification"]["type_no"] == 7 assert a["nets"]["operating"]["sign"] == "-" assert a["nets"]["investing"]["sign"] == "+" assert a["nets"]["financing"]["sign"] == "+" # periods / values 可查 r3 = client.get(f"{BASE}/periods", headers=auth_header(token)) assert "2026-08" in r3.json()["periods"] def test_upsert_overwrite_same_period(self, client: TestClient, db: Session): create_test_user(db) token = get_token_for_user(client) self._post_value(client, token) resp2 = self._post_value(client, token, operating_net=50.0, investing_net=-20.0, financing_net=-10.0) assert resp2.status_code == 200 assert resp2.json()["classification"]["type_no"] == 1 # + - - → 成熟健康型 rows = client.get(f"{BASE}/values", headers=auth_header(token)).json()["items"] assert len(rows) == 1 # 同期间覆盖不重复插入 def test_type6_7_boundary_like_bohai(self, client: TestClient, db: Session): """博海结构判定回归:经营弱/负 + 筹资依赖 → 类型6(- - +) / 类型7(- + +)""" create_test_user(db) token = get_token_for_user(client) resp = self._post_value(client, token, operating_net=-5.0, investing_net=-2.0, financing_net=9.0) assert resp.json()["classification"]["type_no"] == 6 resp = self._post_value(client, token, operating_net=-5.0, investing_net=2.0, financing_net=9.0) assert resp.json()["classification"]["type_no"] == 7 def test_invalid_period(self, client: TestClient, db: Session): create_test_user(db) token = get_token_for_user(client) resp = self._post_value(client, token, period="2026/08") assert resp.status_code == 422