fix(bep-caliber): rebate rate uses YTD actual voucher cumulative (571103 debit / 5001 credit), book+cash card read derived, fail-closed
ci/woodpecker/push/main Pipeline was canceled
ci/woodpecker/push/main Pipeline was canceled
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
"""BEP 渠补率改「实测累计」口径 单测 — finance-20261002-bep-rebate-actual-caliber-fullstack
|
||||
|
||||
口径依据(财务Bot 2026-10-02 裁定,任总 11:43 CST 放行「改,这样数据更加真实」):
|
||||
渠补率(实测) = Σ(571103 **借方**) ÷ Σ(5001 **贷方**) × 100,期间 = 年初至当期(YTD),2 位小数;
|
||||
政策常量 84.61% **仅作对照展示**,不参与任何计算;
|
||||
fail-closed:571103 或 5001 缺数/为 0 ⇒ 该期**不出保本值**(pending + NULL),不静默回落常量。
|
||||
|
||||
覆盖两面:
|
||||
· scripts/kpi_bep_deriver.py —— 派生落库写入面(去常量、YTD 现算、fail-closed)
|
||||
· app/api/dashboard.py::/bep-card —— 卡面读取面(rebate_pct 读派生值、book/cash 同源读派生值)
|
||||
|
||||
夹具数值刻意复刻财务复算口径,使「改前(84.61%常量) / 改后(85.49%实测)」两套输出可逐项区分:
|
||||
2026-01..07:Σ5001贷 = 794.35 万 | Σ571103借 = 672.10 万 ⇒ 渠补率 84.61%(= 常量,故该期值不变)
|
||||
2026-01..08:Σ5001贷 = 808.79 万 | Σ571103借 = 691.43 万 ⇒ 渠补率 85.49%
|
||||
月均管理费(含折旧) 25.774 万 / 现金固定成本月均 18.2605 万(8 个月)
|
||||
⇒ 改前(15.39%):保本 167.5 / 118.7,覆盖 60.4% / 85.2%
|
||||
⇒ 改后(14.51%):保本 177.6 / 125.8,覆盖 56.9% / 80.3%
|
||||
"""
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
# 值变复核提示件默认落 finance 的 _reports/;测试期重定向到临时目录(防污染对端 profile)
|
||||
os.environ["BEP_REVIEW_NOTE_DIR"] = tempfile.mkdtemp(prefix="bep-test-review-notes-")
|
||||
|
||||
import pytest # noqa: E402
|
||||
|
||||
from app.models import KPIDefinition, KPIValue, VoucherDetail # noqa: E402
|
||||
from app.api.dashboard import get_bep_card # noqa: E402
|
||||
from scripts import kpi_bep_deriver as D # noqa: E402
|
||||
|
||||
ENTITY = 1
|
||||
D_DATE_07 = datetime.datetime(2026, 7, 31)
|
||||
D_DATE_08 = datetime.datetime(2026, 8, 31)
|
||||
|
||||
# ── 财务复算锚值(单位:元)────────────────────────────────────────────────
|
||||
# 2026-01..07 累计
|
||||
M07 = dict(period="2026-07", d=D_DATE_07,
|
||||
sales=7943500.0, rebate=6721000.0, admin_other=1307810.0, dep=526050.0)
|
||||
# 2026-08 单月增量(累计口径 01..08 = M07 + M08)
|
||||
M08 = dict(period="2026-08", d=D_DATE_08,
|
||||
sales=144400.0, rebate=193300.0, admin_other=153030.0, dep=75030.0)
|
||||
|
||||
KPI_CODES = (
|
||||
("F_BEP_REVENUE", "保本销售额", "万元", None, None, None),
|
||||
("F_BEP_COVER", "保本覆盖率", "%", ">=100", ">=85", "<85"),
|
||||
("F_BEP_GAP", "保本缺口", "%", "<=0", "<=15", ">15"),
|
||||
("F_BEP_REVENUE_CASH", "现金口径保本销售额", "万元", None, None, None),
|
||||
("F_BEP_COVER_CASH", "现金口径保本覆盖率", "%", ">=100", ">=85", "<85"),
|
||||
("F_BEP_REBATE", "实测渠补率", "%", None, None, None),
|
||||
)
|
||||
|
||||
_seq = [700000]
|
||||
|
||||
|
||||
def _voucher(db, row, subject_code, debit=0.0, credit=0.0, cf=0):
|
||||
_seq[0] += 1
|
||||
db.add(VoucherDetail(entity_id=ENTITY, voucher_no=str(_seq[0]),
|
||||
voucher_date=row["d"], subject_code=subject_code,
|
||||
subject_name="单测科目", debit_amount=debit,
|
||||
credit_amount=credit, summary="渠补率口径单测",
|
||||
carry_forward=cf, period=row["period"]))
|
||||
db.flush()
|
||||
|
||||
|
||||
def _month_rows(db, row, rebate_credit=0.0):
|
||||
"""一个月的凭证行:收入(5001贷) / 渠补(571103借) / 管理费(5602% 含折旧 560213)"""
|
||||
_voucher(db, row, "5001", credit=row["sales"])
|
||||
if row["rebate"] is not None:
|
||||
_voucher(db, row, "571103", debit=row["rebate"]) # 含显式 0 行(「存在但为 0」负例)
|
||||
# 571103 贷方冲销行(负对照:证明实现走**借方**口径而非净额)
|
||||
if rebate_credit:
|
||||
_voucher(db, row, "571103", credit=rebate_credit)
|
||||
_voucher(db, row, "560201", debit=row["admin_other"]) # 管理费其他明细
|
||||
_voucher(db, row, "560213", debit=row["dep"]) # 管理费用-折旧(含在 5602% 内)
|
||||
|
||||
|
||||
def _kpi_defs(db, include_rebate=True):
|
||||
for code, name, unit, g, y, r in KPI_CODES:
|
||||
if code == "F_BEP_REBATE" and not include_rebate:
|
||||
continue
|
||||
db.add(KPIDefinition(entity_id=ENTITY, kpi_code=code, kpi_name=name,
|
||||
dimension="finance", category="profitability",
|
||||
formula="单测", data_source="单测夹具", data_owner="财务部",
|
||||
frequency="monthly", unit=unit,
|
||||
threshold_green=g, threshold_yellow=y, threshold_red=r,
|
||||
kpi_level="strategic", status="active"))
|
||||
db.flush()
|
||||
|
||||
|
||||
def _fixture_01_08(db, rebate_credit=0.0):
|
||||
_kpi_defs(db)
|
||||
_month_rows(db, M07)
|
||||
_month_rows(db, M08, rebate_credit=rebate_credit)
|
||||
db.flush()
|
||||
|
||||
|
||||
def _derived_rows(db, period="2026-08"):
|
||||
out = {}
|
||||
for code, *_ in KPI_CODES:
|
||||
kpi = db.query(KPIDefinition).filter(KPIDefinition.entity_id == ENTITY,
|
||||
KPIDefinition.kpi_code == code).first()
|
||||
if kpi is None:
|
||||
continue
|
||||
v = db.query(KPIValue).filter(KPIValue.kpi_id == kpi.id,
|
||||
KPIValue.period == period,
|
||||
KPIValue.source_type == "derived").first()
|
||||
if v is not None:
|
||||
out[code] = v
|
||||
return out
|
||||
|
||||
|
||||
# ── 一、派生器:实测累计口径 ────────────────────────────────────────────────
|
||||
|
||||
def test_compute_rebate_rate_uses_ytd_actual(db):
|
||||
"""实测渠补率 = Σ571103借方 ÷ Σ5001贷方(YTD 累计,2 位小数)"""
|
||||
_fixture_01_08(db)
|
||||
info_07 = D.compute_rebate_rate(db, ENTITY, "2026-01", "2026-07")
|
||||
info_08 = D.compute_rebate_rate(db, ENTITY, "2026-01", "2026-08")
|
||||
assert info_07["available"] is True
|
||||
assert abs(info_07["rebate_pct"] - 84.61) < 0.005, info_07
|
||||
assert abs(info_07["rebate_sum_w"] - 672.10) < 0.01, info_07
|
||||
assert abs(info_07["sales_sum_w"] - 794.35) < 0.01, info_07
|
||||
assert info_08["available"] is True
|
||||
assert abs(info_08["rebate_pct"] - 85.49) < 0.005, info_08
|
||||
|
||||
|
||||
def test_deriver_debit_side_negative_control(db):
|
||||
"""571103 取**借方**而非净额(负对照):夹具含 10 万贷方冲销行。
|
||||
|
||||
若实现退回「净额」口径 ⇒ 渠补率 = (691.43-10)/808.79 = 84.25% ≠ 85.49%,本用例必红。
|
||||
"""
|
||||
_fixture_01_08(db, rebate_credit=100000.0)
|
||||
info = D.compute_rebate_rate(db, ENTITY, "2026-01", "2026-08")
|
||||
assert abs(info["rebate_pct"] - 85.49) < 0.005, info
|
||||
net_w = D._sum_field(db, ENTITY, "2026-01", "2026-08", "net", ("eq", D.REBATE_CODE)) / 10000.0
|
||||
assert abs(net_w - 681.43) < 0.01, net_w # 净额口径 = 691.43 − 10
|
||||
assert net_w != info["rebate_sum_w"] # 两种口径在本夹具下必然不同
|
||||
|
||||
|
||||
def test_deriver_2026_08_values_follow_actual_rebate(db):
|
||||
"""派生器落库:改后 177.6 / 125.8 / 56.9 / 80.3,渠补率 85.49(政策常量仅对照)"""
|
||||
_fixture_01_08(db)
|
||||
r = D.run_derive(db, ENTITY, "2026-08", None, write_kpi=True)
|
||||
assert r["data_status"] == "ok", r
|
||||
assert abs(r["rebate_actual_pct"] - 85.49) < 0.005, r
|
||||
assert r["rebate_policy_pct"] == 84.61 # 常量保留(仅对照)
|
||||
assert abs(r["values"]["F_BEP_REVENUE"] - 177.6) < 0.05, r["values"]
|
||||
assert abs(r["values"]["F_BEP_REVENUE_CASH"] - 125.8) < 0.05, r["values"]
|
||||
assert abs(r["values"]["F_BEP_COVER"] - 56.9) < 0.05, r["values"]
|
||||
assert abs(r["values"]["F_BEP_COVER_CASH"] - 80.3) < 0.05, r["values"]
|
||||
assert abs(r["values"]["F_BEP_REBATE"] - 85.49) < 0.005, r["values"]
|
||||
# 边界外反证:若仍吃常量 84.61(旧口径)= 167.5 / 118.7
|
||||
assert r["values"]["F_BEP_REVENUE"] != 167.5, r["values"]
|
||||
|
||||
rows = _derived_rows(db, "2026-08")
|
||||
assert set(rows) == {c for c, *_ in KPI_CODES}, sorted(rows)
|
||||
assert abs(float(rows["F_BEP_REBATE"].actual_value) - 85.49) < 0.005
|
||||
assert "571103" in rows["F_BEP_REBATE"].remark and "政策常量" in rows["F_BEP_REBATE"].remark
|
||||
|
||||
|
||||
def test_deriver_2026_07_unchanged_regression(db):
|
||||
"""回归:1-7 月实测渠补率恰为 84.61% ⇒ 与政策常量路径逐项等值(170.2 / 121.4)"""
|
||||
_kpi_defs(db)
|
||||
_month_rows(db, M07)
|
||||
db.flush()
|
||||
r_auto = D.run_derive(db, ENTITY, "2026-07", None, write_kpi=True)
|
||||
r_policy = D.run_derive(db, ENTITY, "2026-07", 84.61, write_kpi=True)
|
||||
assert abs(r_auto["rebate_actual_pct"] - 84.61) < 0.005, r_auto
|
||||
assert r_auto["values"] == r_policy["values"], (r_auto["values"], r_policy["values"])
|
||||
assert abs(r_auto["values"]["F_BEP_REVENUE"] - 170.2) < 0.05, r_auto["values"]
|
||||
assert abs(r_auto["values"]["F_BEP_REVENUE_CASH"] - 121.4) < 0.05, r_auto["values"]
|
||||
assert abs(r_auto["values"]["F_BEP_COVER"] - 66.7) < 0.05, r_auto["values"]
|
||||
assert abs(r_auto["values"]["F_BEP_COVER_CASH"] - 93.5) < 0.05, r_auto["values"]
|
||||
|
||||
|
||||
def test_deriver_policy_constant_not_used(db):
|
||||
"""政策常量不参与计算(负对照):渠补率 50% ⇒ 保本 = 月固定成本 ÷ 50%,而非 ÷ 15.39%"""
|
||||
_kpi_defs(db)
|
||||
row = dict(period="2026-08", d=D_DATE_08, sales=8000000.0, rebate=4000000.0,
|
||||
admin_other=800000.0, dep=0.0)
|
||||
_month_rows(db, row)
|
||||
db.flush()
|
||||
r = D.run_derive(db, ENTITY, "2026-08", None, write_kpi=False)
|
||||
assert abs(r["rebate_actual_pct"] - 50.0) < 0.005, r
|
||||
# admin_avg = 80万/8个月 = 10 万 ⇒ 10/0.5 = 20.0 万(常量口径会是 10/0.1539 = 65.0 万)
|
||||
assert abs(r["values"]["F_BEP_REVENUE"] - 20.0) < 0.05, r["values"]
|
||||
assert abs(r["values"]["F_BEP_REVENUE"] - 65.0) > 0.5, r["values"]
|
||||
|
||||
|
||||
# ── 二、派生器:fail-closed(缺数/为 0 不出值、不回落常量)─────────────────
|
||||
|
||||
def test_deriver_fail_closed_when_rebate_missing(db):
|
||||
_kpi_defs(db)
|
||||
r07 = dict(M07)
|
||||
r07["rebate"] = None # 全年 571103 完全缺数
|
||||
r08 = dict(M08)
|
||||
r08["rebate"] = None
|
||||
_month_rows(db, r07)
|
||||
_month_rows(db, r08)
|
||||
db.flush()
|
||||
r = D.run_derive(db, ENTITY, "2026-08", None, write_kpi=True)
|
||||
assert r["data_status"] == "pending", r
|
||||
assert r["rebate_actual_pct"] is None, r
|
||||
assert "渠补率不可得" in r["reason"], r
|
||||
rows = _derived_rows(db, "2026-08")
|
||||
assert set(rows) == {c for c, *_ in KPI_CODES}, sorted(rows)
|
||||
for code, v in rows.items():
|
||||
assert v.actual_value is None, (code, v.actual_value) # 不出保本值
|
||||
assert v.data_status == "pending", (code, v.data_status)
|
||||
assert "渠补率不可得" in (v.remark or ""), (code, v.remark)
|
||||
assert rows["F_BEP_REVENUE"].actual_value != 167.5 # 未回落常量口径
|
||||
|
||||
|
||||
def test_deriver_fail_closed_when_rebate_zero(db):
|
||||
"""571103 存在但合计为 0 ⇒ 同样 fail-closed(不得静默回落 84.61)"""
|
||||
_kpi_defs(db)
|
||||
r07 = dict(M07)
|
||||
r07["rebate"] = 0.0 # 显式 0 行
|
||||
r08 = dict(M08)
|
||||
r08["rebate"] = 0.0
|
||||
_month_rows(db, r07)
|
||||
_month_rows(db, r08)
|
||||
db.flush()
|
||||
r = D.run_derive(db, ENTITY, "2026-08", None, write_kpi=True)
|
||||
assert r["data_status"] == "pending", r
|
||||
assert r["rebate_sum_w"] == 0.0, r
|
||||
rows = _derived_rows(db, "2026-08")
|
||||
assert rows["F_BEP_REVENUE"].actual_value is None
|
||||
assert rows["F_BEP_REVENUE"].data_status == "pending"
|
||||
|
||||
|
||||
# ── 三、卡面 /bep-card:rebate_pct 读派生值 + book/cash 同源 ───────────────
|
||||
|
||||
def test_card_reads_derived_values_and_rebate_pct(db):
|
||||
_fixture_01_08(db)
|
||||
D.run_derive(db, ENTITY, "2026-08", None, write_kpi=True)
|
||||
|
||||
card = get_bep_card(db=db, entity_id=ENTITY)
|
||||
assert card["available"] is True, card
|
||||
assert card["period"] == "2026-08", card
|
||||
assert abs(card["inputs"]["rebate_pct"] - 85.49) < 0.005, card["inputs"]
|
||||
assert card["inputs"]["rebate_policy_pct"] == 84.61, card["inputs"]
|
||||
assert abs(card["inputs"]["cm_pct"] - 14.51) < 0.005, card["inputs"]
|
||||
assert abs(card["book"]["bep_revenue"] - 177.6) < 0.05, card["book"]
|
||||
assert abs(card["book"]["cover"] - 56.9) < 0.05, card["book"]
|
||||
assert abs(card["cash"]["bep_revenue"] - 125.8) < 0.05, card["cash"]
|
||||
assert abs(card["cash"]["cover"] - 80.3) < 0.05, card["cash"]
|
||||
assert card["data_source"] == "derived"
|
||||
|
||||
|
||||
def test_card_book_and_cash_both_read_derived_rows(db):
|
||||
"""book / cash 同源回归:直接改库内派生值,两条路径都必须随之变(不再实时复算 voucher)"""
|
||||
_fixture_01_08(db)
|
||||
D.run_derive(db, ENTITY, "2026-08", None, write_kpi=True)
|
||||
first = get_bep_card(db=db, entity_id=ENTITY)
|
||||
assert abs(first["book"]["bep_revenue"] - 177.6) < 0.05
|
||||
assert abs(first["cash"]["bep_revenue"] - 125.8) < 0.05
|
||||
|
||||
# 临时改库内派生值(夹具库,非生产)——两路径应同步跟随
|
||||
for code, new_val in (("F_BEP_REVENUE", 999.9), ("F_BEP_REVENUE_CASH", 888.8),
|
||||
("F_BEP_REBATE", 60.0)):
|
||||
kpi = db.query(KPIDefinition).filter(KPIDefinition.entity_id == ENTITY,
|
||||
KPIDefinition.kpi_code == code).first()
|
||||
row = db.query(KPIValue).filter(KPIValue.kpi_id == kpi.id,
|
||||
KPIValue.period == "2026-08",
|
||||
KPIValue.source_type == "derived").first()
|
||||
row.actual_value = new_val
|
||||
db.commit()
|
||||
|
||||
second = get_bep_card(db=db, entity_id=ENTITY)
|
||||
assert abs(second["book"]["bep_revenue"] - 999.9) < 0.001, second["book"]
|
||||
assert abs(second["cash"]["bep_revenue"] - 888.8) < 0.001, second["cash"]
|
||||
assert abs(second["inputs"]["rebate_pct"] - 60.0) < 0.001, second["inputs"]
|
||||
assert abs(second["inputs"]["cm_pct"] - 40.0) < 0.001, second["inputs"]
|
||||
# 反证:实时复算渠道(voucher 现值 177.6)不得再决定卡面值
|
||||
assert abs(second["book"]["bep_revenue"] - 177.6) > 1.0
|
||||
|
||||
|
||||
def test_card_fail_closed_when_derived_rebate_absent(db):
|
||||
"""派生渠补率不可得 ⇒ 卡面不出保本值(available=False),不回落常量、不跨期回落"""
|
||||
_kpi_defs(db)
|
||||
_month_rows(db, M07)
|
||||
_month_rows(db, M08)
|
||||
db.flush()
|
||||
# 1) 07 期正常派生(当时 571103 数据齐全)
|
||||
r07 = D.run_derive(db, ENTITY, "2026-07", None, write_kpi=True)
|
||||
assert r07["data_status"] == "ok"
|
||||
# 2) 全年 571103 缺数 ⇒ 08 期 fail-closed(已落 07 值不动)
|
||||
db.query(VoucherDetail).filter(
|
||||
VoucherDetail.entity_id == ENTITY,
|
||||
VoucherDetail.subject_code == "571103",
|
||||
).delete(synchronize_session=False)
|
||||
db.commit()
|
||||
r08 = D.run_derive(db, ENTITY, "2026-08", None, write_kpi=True)
|
||||
assert r08["data_status"] == "pending", r08
|
||||
|
||||
card = get_bep_card(db=db, entity_id=ENTITY)
|
||||
# 08 期六条不齐(渠补率 NULL)⇒ 卡面不得跨期回落到 07 期,也不得回落常量
|
||||
assert card["available"] is False, card
|
||||
assert card["period"] == "2026-08", card
|
||||
assert "渠补率" in card["message"], card
|
||||
assert card.get("rebate_policy_pct") == 84.61, card
|
||||
assert "book" not in card
|
||||
|
||||
|
||||
# ── 四、验收探针(自测判别力):同一用例在改前 / 改后两树上跑 ────────────────
|
||||
# 只依赖两版共有的 API(run_derive 返回的 values / get_bep_card),故可在 HEAD 快照树上复跑:
|
||||
# 改前(政策常量 84.61%)⇒ FAIL,且失败信息里出现 167.5 / 118.7
|
||||
# 改后(实测 85.49%) ⇒ PASS,输出 177.6 / 125.8 / 56.9 / 80.3 / rebate_pct 85.49
|
||||
def test_acceptance_probe_2026_08(db):
|
||||
"""验收探针:2026-08 口径数值(财务复算预告逐项比对)"""
|
||||
_fixture_01_08(db)
|
||||
r = D.run_derive(db, ENTITY, "2026-08", None, write_kpi=True)
|
||||
vals = r.get("values", {})
|
||||
card = get_bep_card(db=db, entity_id=ENTITY)
|
||||
obs = {
|
||||
"deriver.F_BEP_REVENUE": vals.get("F_BEP_REVENUE"),
|
||||
"deriver.F_BEP_REVENUE_CASH": vals.get("F_BEP_REVENUE_CASH"),
|
||||
"deriver.F_BEP_COVER": vals.get("F_BEP_COVER"),
|
||||
"deriver.F_BEP_COVER_CASH": vals.get("F_BEP_COVER_CASH"),
|
||||
"deriver.F_BEP_REBATE": vals.get("F_BEP_REBATE"),
|
||||
"card.available": card.get("available"),
|
||||
"card.inputs.rebate_pct": (card.get("inputs") or {}).get("rebate_pct"),
|
||||
"card.inputs.rebate_policy_pct": (card.get("inputs") or {}).get("rebate_policy_pct"),
|
||||
"card.book.bep_revenue": (card.get("book") or {}).get("bep_revenue"),
|
||||
"card.book.cover": (card.get("book") or {}).get("cover"),
|
||||
"card.cash.bep_revenue": (card.get("cash") or {}).get("bep_revenue"),
|
||||
"card.cash.cover": (card.get("cash") or {}).get("cover"),
|
||||
}
|
||||
print("\n[ACCEPTANCE-PROBE] " + json.dumps(obs, ensure_ascii=False))
|
||||
checks = [
|
||||
("deriver.F_BEP_REVENUE", obs["deriver.F_BEP_REVENUE"], 177.6, 0.2),
|
||||
("deriver.F_BEP_REVENUE_CASH", obs["deriver.F_BEP_REVENUE_CASH"], 125.8, 0.2),
|
||||
("deriver.F_BEP_COVER", obs["deriver.F_BEP_COVER"], 56.9, 0.3),
|
||||
("deriver.F_BEP_COVER_CASH", obs["deriver.F_BEP_COVER_CASH"], 80.3, 0.3),
|
||||
("card.book.bep_revenue", obs["card.book.bep_revenue"], 177.6, 0.2),
|
||||
("card.cash.bep_revenue", obs["card.cash.bep_revenue"], 125.8, 0.2),
|
||||
("card.inputs.rebate_pct", obs["card.inputs.rebate_pct"], 85.49, 0.02),
|
||||
]
|
||||
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}"
|
||||
Reference in New Issue
Block a user