From 3ed9fa06112db07ba567338ea374915e453654cb Mon Sep 17 00:00:00 2001 From: Hermes CI Fix Date: Sun, 4 Oct 2026 22:37:20 +0800 Subject: [PATCH] =?UTF-8?q?fix(cvp):=20O1=20residue=20-=20e2=20rebate=5Fpo?= =?UTF-8?q?licy=5Fpct=20=E7=BD=AE=20null=EF=BC=88=E5=8F=A3=E5=BE=84?= =?UTF-8?q?=E5=8D=95=E4=B8=80=E6=BA=90=20=3D=20bep=5Fcaliber=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:/bep-card 端点对 e2(博海)回落模块内硬编码常量 BEP_REBATE_POLICY_PCT=84.61, 而 bep_caliber.ENTITY_CALIBERS[2]["policy_pct"] 为 None(博海无渠补科目)⇒ 下游 API 消费者会把 payload 里的 rebate_policy_pct=84.61 误读为「博海有 84.61% 政策渠补率」。 改动(严格限定 dashboard.py 一个模块 + 1 条守护用例): - 删除模块级常量 BEP_REBATE_POLICY_PCT;四处回落分支改为直接取口径值 (未注册口径分支置 None;其余取 cal.get("policy_pct") / 已算出的 policy_pct)。 - 口径单一事实源维持 app/utils/bep_caliber.py,端点不做任何回落。 - 新增 tests/test_bep_rebate_actual_caliber.py::test_o1_policy_pct_single_source_no_hardcoded_fallback (改前必红 / 改后必绿,e1=84.61 作同端点负对照)。 改前/改后(端点函数直调,生产库只读,e1/e2 同批): 改前 e2 inputs.rebate_policy_pct = 84.61 → 改后 null;e1 恒为 84.61(负对照不变)。 --- backend/app/api/dashboard.py | 22 +++++++++++-------- .../tests/test_bep_rebate_actual_caliber.py | 19 ++++++++++++++++ 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/backend/app/api/dashboard.py b/backend/app/api/dashboard.py index ac6369f8..3c67c34b 100644 --- a/backend/app/api/dashboard.py +++ b/backend/app/api/dashboard.py @@ -42,6 +42,10 @@ router = APIRouter(prefix="/api/cma/dashboard", tags=["驾驶舱"], # 2026-10-02 口径修订(任总 11:43 CST 放行「改,这样数据更加真实」): # ① 渠补率不再用政策常量 84.61 参与计算,改用派生落库的实测累计值; # 政策常量仅作对照返回(inputs.rebate_policy_pct),**不参与计算**; +# ⚠️ 2026-10-04(O1 收敛):该对照值的**唯一来源** = app/utils/bep_caliber.py 的 +# ENTITY_CALIBERS[entity]["policy_pct"](酣客 e1 = 84.61;博海 e2 = None,无渠补科目)。 +# 端点各处一律直接取口径值,**不得**再回落本模块内的硬编码常量——否则 e2 会被下游 +# API 消费者误读为「博海有 84.61% 政策渠补率」(口径单一源原则)。 # ② 账面/现金两条路径**统一读派生落库值**——此前 book_bep 实时复算 voucher、 # cash_bep 读派生值,9 月数据入库而派生器未跑时同卡两口径不同步; # ③ fail-closed:六条派生值(含渠补率)**同期齐全**才出卡;缺数 ⇒ available=False, @@ -63,8 +67,9 @@ BEP_REQUIRED_CODES = tuple(c for c in BEP_CODES if c != BEP_REBATE_CODE) BEP_SALES_SUBJECT = "5001" BEP_ADMIN_PREFIX = "5602" BEP_DEP_SUBJECT = "560213" -# 政策常量:仅作对照展示(2026-10-02 起不参与任何计算;博海无渠补科目 ⇒ None) -BEP_REBATE_POLICY_PCT = 84.61 +# ⚠️ 2026-10-04(O1 收敛):本模块**不再**定义政策渠补率常量(原 BEP_REBATE_POLICY_PCT)。 +# 政策常量的唯一来源 = app/utils/bep_caliber.py::ENTITY_CALIBERS[entity]["policy_pct"] +# (酣客 e1 = 84.61;博海 e2 = None,因无渠补科目)。端点各处直接取口径值,不做回落。 def _bep_sum_field(db, entity_id: int, period_from: str, period_to: str, @@ -170,7 +175,8 @@ def get_bep_card( if cal is None: return {"available": False, "entity_id": entity_id, "message": f"entity={entity_id} 未注册 BEP 口径(见 app/utils/bep_caliber.py)", - "rebate_policy_pct": BEP_REBATE_POLICY_PCT, "readonly": True} + # 未注册口径 ⇒ 无政策常量来源(口径单一源):不得回落硬编码值 + "rebate_policy_pct": None, "readonly": True} kpis = {k.kpi_code: k for k in db.query(KPIDefinition).filter( KPIDefinition.entity_id == entity_id, @@ -205,7 +211,7 @@ def get_bep_card( if not book_periods: return {"available": False, "entity_id": entity_id, "message": "尚无派生值,请先运行 kpi_bep_deriver.py", - "rebate_policy_pct": BEP_REBATE_POLICY_PCT, "readonly": True} + "rebate_policy_pct": cal.get("policy_pct"), "readonly": True} period = book_periods[0] year = period[:4] months = int(period[5:7]) @@ -227,8 +233,7 @@ def get_bep_card( "label": label, "reason": reason, "vc_pct": by_code.get(BEP_VC_CODE, {}).get(period), "rebate_pct": by_code.get(BEP_REBATE_CODE, {}).get(period), - "rebate_policy_pct": (cal.get("policy_pct") - if cal.get("policy_pct") is not None else BEP_REBATE_POLICY_PCT), + "rebate_policy_pct": cal.get("policy_pct"), "entity_name": cal["name"], "missing_codes": missing_at_period, "derived_periods": book_periods[:6], @@ -242,8 +247,7 @@ def get_bep_card( % ",".join(missing_at_period)) return {"available": False, "entity_id": entity_id, "period": period, "message": msg, "missing_codes": missing_at_period, - "rebate_policy_pct": (cal.get("policy_pct") if cal.get("policy_pct") is not None - else BEP_REBATE_POLICY_PCT), + "rebate_policy_pct": cal.get("policy_pct"), "derived_periods": book_periods[:6], "readonly": True} # 派生落库值(同期)—— 账面 / 现金两条路径同一来源,卡面不做实时复算 @@ -290,7 +294,7 @@ def get_bep_card( "dep_avg": round(dep_avg, 2), "vc_pct": vc_pct, "rebate_pct": rebate_pct, - "rebate_policy_pct": BEP_REBATE_POLICY_PCT, # 政策常量:仅作对照,不参与计算 + "rebate_policy_pct": policy_pct, # 口径单一源:bep_caliber.ENTITY_CALIBERS[entity]["policy_pct"] "rebate_source": ("derived:F_BEP_VC_RATE(⑤完整口径:销货成本+税金及附加+渠补−厂补)" if entity_id == 1 else "derived:F_BEP_VC_RATE(Σ6401借÷Σ6001贷)"), diff --git a/backend/tests/test_bep_rebate_actual_caliber.py b/backend/tests/test_bep_rebate_actual_caliber.py index 00f6c3f5..2f93a62f 100644 --- a/backend/tests/test_bep_rebate_actual_caliber.py +++ b/backend/tests/test_bep_rebate_actual_caliber.py @@ -345,3 +345,22 @@ def test_acceptance_probe_2026_08(db): for name, got, want, tol in checks: assert got is not None, f"{name}: got None, want {want}±{tol}" assert abs(float(got) - want) <= tol, f"{name}: got {got}, want {want}±{tol}" + + +# ── 五、O1 收敛(2026-10-04):政策渠补率口径单一源,端点不得回落硬编码常量 ──────── + +def test_o1_policy_pct_single_source_no_hardcoded_fallback(): + """O1:dashboard 模块不得再定义硬编码政策常量;policy_pct 唯一来源 = bep_caliber。 + + 改前(HEAD 587d5b3):`dashboard.BEP_REBATE_POLICY_PCT == 84.61` 且端点对 e2 回落该常量 + ⇒ 本用例第一条断言必红(改前/改后判据,非事后追认)。 + 改后:无该属性;e1 = 84.61(同端点负对照)、e2 = None(无渠补科目)。 + """ + from app.api import dashboard as DASH + from app.utils import bep_caliber as CAL + + assert not hasattr(DASH, "BEP_REBATE_POLICY_PCT"), ( + "dashboard 仍在定义硬编码政策渠补率常量 ⇒ 口径非单一源") + assert CAL.caliber_for(1)["policy_pct"] == 84.61 # e1 保留(负对照) + assert CAL.caliber_for(2)["policy_pct"] is None # e2 置 None(无渠补科目) +