"""公式纯函数测试。 守的是:算钱公式的精确性与边界 —— 一旦按 float 或字面量截断计算, 360/361 天分界、4/360 斜率、27 天回款示例会全部错位,直接影响工资金额。 断言一律用 Fraction/Decimal 精确值,禁近似比较。 """ from datetime import date from decimal import Decimal from fractions import Fraction from app.services.formula import ( apply_barter, clamp_weight_r, converted_profit, manager_team_share, monthly_salary, payment_days, payment_weight, performance_amount, probation_annual_note, profit_rate, salesperson_rate, start_payment_ok, ) # 与迁移种子一致的业务员参数(存储口径:start/floor 为 ‰ 数,slope 为 ‰/天) SP_START = Fraction(10) SP_SLOPE = Fraction(4, 360) SP_FLOOR = Fraction(4) SP_BOUNDARY = 360 def _sp_rate(days: int) -> Fraction: return salesperson_rate( days, start=SP_START, slope=SP_SLOPE, floor=SP_FLOOR, boundary_days=SP_BOUNDARY ) class TestPaymentDays: """回款天数:发货日首日计第 1 天(对照表 #1;需规「26 天」示例作废)。""" def test_发货日当天回款为第1天(self) -> None: d = date(2026, 1, 1) assert payment_days(d, d) == 1 def test_12月15日发货1月10日回款为27天(self) -> None: assert payment_days(date(2025, 12, 15), date(2026, 1, 10)) == 27 def test_跨年按自然日连续累计(self) -> None: assert payment_days(date(2025, 12, 31), date(2026, 1, 1)) == 2 class TestSalespersonRate: """业务员计提比例:1~360 天线性递减、≥361 天固定下限(对照表 #4)。""" def test_第360天恰为千分之6_线性段终点(self) -> None: assert _sp_rate(360) == Fraction(6, 1000) def test_第361天固定下限千分之4_分界线(self) -> None: # 公式外推为 5.989‰,业务确认「360 为分界线,360 以后按千分之四」 assert _sp_rate(361) == Fraction(4, 1000) def test_第1天按公式为899比90000_非取整为10(self) -> None: # 需规图示「第 1 天对应 10‰」为上限参考;公式值 = (10 − 4/360)/1000 = 899/90000 assert _sp_rate(1) == Fraction(899, 90000) def test_第180天线性段中点千分之8(self) -> None: assert _sp_rate(180) == Fraction(8, 1000) def test_返回值为纯比例_非千分数(self) -> None: # 绩效 = 折算额 × 比例,比例必须是 0.006 而非 6 assert _sp_rate(360) == Fraction(6, 1000) assert _sp_rate(360) != Fraction(6) def test_负天数钳制为0天_按起始比例计提(self) -> None: """先款后货(payment_days 为负)不线性外插:钳到 0 天取起始比例。 修复前 days=-5 会算出 (10 + 5×4/360)‰ —— 高于起始值,多算绩效。 """ assert _sp_rate(-5) == _sp_rate(0) assert _sp_rate(-1) == Fraction(10, 1000) class TestProfitRate: """毛利润率:不含运费口径(对照表 #2),毛利润额 / 销售额。""" def test_毛利润率_不含运费(self) -> None: assert profit_rate(Decimal("45"), Decimal("100")) == Fraction(9, 20) def test_销售额为零_返回零_防除零(self) -> None: assert profit_rate(Decimal("0"), Decimal("0")) == Fraction(0) class TestConvertedProfit: """折算毛利润额(化简式):销售额 ×(毛利润率 − 阈值)(对照表 #12/#13)。""" def test_化简式结果_市政水利30点阈值(self) -> None: # 100 × (9/20 − 30/100) = 15 assert converted_profit(Decimal("100"), Fraction(9, 20), Fraction(30)) == Decimal("15") def test_化简式与定义式等价(self) -> None: # 定义式:毛利润额 / 毛利润率 × (毛利润率 − 阈值) = 45 / (9/20) × (9/20 − 3/10) = 15 revenue, profit = Decimal("100"), Decimal("45") rate = profit_rate(profit, revenue) assert converted_profit(revenue, rate, Fraction(30)) == Decimal("15") def test_毛利润额为零_折算为零_不除零(self) -> None: assert converted_profit(Decimal("0"), Fraction(0), Fraction(30)) == Decimal("0") def test_毛利润率低于阈值_折算为负_不归零(self) -> None: # 100 × (1/10 − 3/10) = -20(业务确认负值正常计算) assert converted_profit(Decimal("100"), Fraction(1, 10), Fraction(30)) == Decimal("-20") class TestPerformanceAmount: """绩效 = 折算毛利润额 × 计提比例(乘法方向,对照表 #3)。""" def test_乘法方向_折算额乘比例(self) -> None: # 15000 × 97/10000 = 145.5 assert performance_amount(Decimal("15000"), Fraction(97, 10000)) == Decimal("145.5") def test_负折算额_绩效为负不归零(self) -> None: assert performance_amount(Decimal("-20"), Fraction(6, 1000)) == Decimal("-0.12") class TestModifiers: """修饰器:以物抵顶、支付权重 R×R、起付判定(对照表 #21/#22/#24)。""" def test_以物抵顶_按80点折算(self) -> None: assert apply_barter(Decimal("145.5"), Fraction(80)) == Decimal("116.4") def test_支付权重_R为60时0点36(self) -> None: assert payment_weight(60) == Fraction(36, 100) def test_支付权重_R为100时全额(self) -> None: assert payment_weight(100) == Fraction(1) def test_起付判定_达到60点(self) -> None: assert start_payment_ok(Decimal("60000"), Decimal("100000"), Fraction(60)) is True def test_起付判定_未达到60点(self) -> None: assert start_payment_ok(Decimal("59999"), Decimal("100000"), Fraction(60)) is False class TestClampWeightR: """R 钳制:向下取整 + [0,100] 封顶封底(从预览内联提取的共用实现)。""" def test_向下取整(self) -> None: assert clamp_weight_r(Decimal("5999.99"), Decimal("10000")) == 59 def test_封顶100_封底0(self) -> None: assert clamp_weight_r(Decimal("20000"), Decimal("10000")) == 100 assert clamp_weight_r(Decimal("0"), Decimal("10000")) == 0 def test_销售额0_防御0(self) -> None: assert clamp_weight_r(Decimal("100"), Decimal("0")) == 0 class TestMonthlySalary: """月工资分岗口径三档(对照表 #8)。""" def test_INCLUDE_绩效加基本(self) -> None: assert monthly_salary(Decimal("1000"), Decimal("3000"), "INCLUDE") == Decimal("4000") def test_DEDUCT_绩效减基本_可为负(self) -> None: assert monthly_salary(Decimal("1000"), Decimal("3000"), "DEDUCT") == Decimal("-2000") def test_EXCLUDE_仅绩效(self) -> None: assert monthly_salary(Decimal("1000"), Decimal("3000"), "EXCLUDE") == Decimal("1000") def test_基本缺失按0(self) -> None: assert monthly_salary(Decimal("1000"), None, "INCLUDE") == Decimal("1000") def test_未知口径报错(self) -> None: import pytest with pytest.raises(ValueError): monthly_salary(Decimal("1"), Decimal("1"), "UNKNOWN") class TestProbationAnnualNote: """试用期年累计说明:超阈值才有 note,金额精确到分。""" def test_未超阈值_无说明(self) -> None: assert probation_annual_note(Decimal("18000"), Decimal("18000")) is None assert probation_annual_note(Decimal("100"), Decimal("18000")) is None def test_超额_给出精确差额(self) -> None: note = probation_annual_note(Decimal("20000.5"), Decimal("18000")) assert note is not None and "2000.50" in note class TestManagerTeamShare: """经理 12.5%+5% 聚合(#12 暂口径:基数不含经理个人户业绩——由调用方 传入的基数集合保证,本函数只做乘法与求和)。""" def test_两段乘加_精确分数(self) -> None: # 入参为百分数(与种子一致:12.5 表示 12.5%):10000×12.5%=1250、50000×5%=2500 got = manager_team_share( Decimal("10000"), Decimal("50000"), team_pct=Fraction(25, 2), company_pct=Fraction(5) ) assert got == Decimal("3750") def test_零基数_零结果(self) -> None: assert manager_team_share( Decimal("0"), Decimal("0"), team_pct=Fraction(0), company_pct=Fraction(0) ) == Decimal("0")