fix(cma): entity isolation batch4 (cash plan writes, budget ownership, read leaks, okr template)

P0-A cash.py: scope api_update_plan / api_delete_plan / api_complete_plan /
api_register_payment to session entity (cross-entity id -> 404, aligning with
the file's existing get_entity_id endpoints).
P0-B budget.py: write entity_id on BudgetPlan creation + require the KPI to
belong to the session entity; scope /comparison, /comparison/kpi/{id} and
/deviation-report reads to the session entity.
P1 notifications.py: explicit field whitelist (name/config/enabled) on
update_channel to close mass-assignment.
P1 read leaks scoped to session entity: bot_kpis list, data_quality check/stats
(KPI calibre only; quality-log table has no entity_id column), kpis causality
chain + associate-map (KPI and map ownership), dashboard my-kpis,
customer_dashboard list, kpi_governance audit, budget_generate candidates,
driver_budget history (+ entity_id on its write path), expenses stats
(+ entity_id on its write path).
P2 okr_templates.py: write entity_id on the StrategicMap created by apply.

tests: tests/test_cross_entity_isolation_batch4_20260921.py (30 cases;
RED before fix = 22 failed, GREEN after = 30 passed; full suite 1302 passed /
1 xfailed / 0 failed).
This commit is contained in:
Hermes CI Fix
2026-09-21 09:37:38 +08:00
parent 8c5853f64b
commit a0762b367f
14 changed files with 749 additions and 44 deletions
+25 -12
View File
@@ -198,9 +198,12 @@ def api_create_plan(request: Request, data: dict, db: Session = Depends(get_db))
@router.put("/plans/{plan_id}")
def api_update_plan(plan_id: int, data: dict, db: Session = Depends(get_db)):
"""更新收付款计划"""
plan = db.query(CashPlan).filter(CashPlan.id == plan_id).first()
def api_update_plan(plan_id: int, data: dict, db: Session = Depends(get_db),
entity_id: int = Depends(get_entity_id)):
"""更新收付款计划(账套隔离 2026-09-21:跨账套 id → 404)"""
plan = db.query(CashPlan).filter(
CashPlan.id == plan_id, CashPlan.entity_id == entity_id,
).first()
if not plan:
raise HTTPException(404, "计划不存在")
if "plan_type" in data:
@@ -248,9 +251,12 @@ def api_update_plan(plan_id: int, data: dict, db: Session = Depends(get_db)):
@router.delete("/plans/{plan_id}")
def api_delete_plan(plan_id: int, db: Session = Depends(get_db)):
"""删除收付款计划"""
plan = db.query(CashPlan).filter(CashPlan.id == plan_id).first()
def api_delete_plan(plan_id: int, db: Session = Depends(get_db),
entity_id: int = Depends(get_entity_id)):
"""删除收付款计划(账套隔离 2026-09-21:跨账套 id → 404)"""
plan = db.query(CashPlan).filter(
CashPlan.id == plan_id, CashPlan.entity_id == entity_id,
).first()
if not plan:
raise HTTPException(404, "计划不存在")
db.delete(plan)
@@ -259,9 +265,12 @@ def api_delete_plan(plan_id: int, db: Session = Depends(get_db)):
@router.post("/plans/{plan_id}/complete")
def api_complete_plan(plan_id: int, db: Session = Depends(get_db)):
"""标记计划为已完成(收款到账/付款完成)"""
plan = db.query(CashPlan).filter(CashPlan.id == plan_id).first()
def api_complete_plan(plan_id: int, db: Session = Depends(get_db),
entity_id: int = Depends(get_entity_id)):
"""标记计划为已完成(收款到账/付款完成)(账套隔离 2026-09-21:跨账套 id → 404)"""
plan = db.query(CashPlan).filter(
CashPlan.id == plan_id, CashPlan.entity_id == entity_id,
).first()
if not plan:
raise HTTPException(404, "计划不存在")
plan.status = "completed"
@@ -616,9 +625,13 @@ def api_receivables(
@router.post("/receivables/{plan_id}/payment")
def api_register_payment(plan_id: int, data: dict, db: Session = Depends(get_db)):
"""回款登记 — 更新计划状态 + 自动消除到期未收款预警 + 联动催收行动/F_AR_DAYS"""
plan = db.query(CashPlan).filter(CashPlan.id == plan_id).first()
def api_register_payment(plan_id: int, data: dict, db: Session = Depends(get_db),
entity_id: int = Depends(get_entity_id)):
"""回款登记 — 更新计划状态 + 自动消除到期未收款预警 + 联动催收行动/F_AR_DAYS
(账套隔离 2026-09-21:跨账套 id → 404)"""
plan = db.query(CashPlan).filter(
CashPlan.id == plan_id, CashPlan.entity_id == entity_id,
).first()
if not plan:
raise HTTPException(404, "应收计划不存在")
if plan.plan_type != "receive":