9dc22cd1e6
TDD(批次A Task4):classify/day_add/days_between 纯函数,供 Task6 宽表 生命周期 4 列计算。口径已确认:新客=首次互动≤7 天(含当天共 7 天)、 cooling [7,30)、沉睡≥30 天且历史≥5 条(对齐 churn real min_msgs=5)、 回流=当日互动(last_day==as_of)且前次间隔>30 天(严格大于,恰好 30 天不算);未激活或历史不足的长期静默返回 None。analytics 现有页暂不 切换(v2 §5 口径差异另标),不动 636 既有测试。 模块为纯叶子节点,禁止 import app.storage(防 storage→analytics 成环)。 7 例测试覆盖:new 窗口边界(as_of-6 含/不含)、回流 35/9/31 天与恰好 30 天、last_day==as_of 门槛、6/7/29/30 天落位、历史门槛 4/5、 day_add 跨月。全量 643 绿。 Co-Authored-By: Claude Code <noreply@anthropic.com>
73 lines
2.6 KiB
Python
73 lines
2.6 KiB
Python
"""客户生命周期判定纯函数测试(批次A Task4)。
|
|
|
|
口径(v2 §1.1 #3 + v1 §21 默认,已用户确认):新客 7 天、沉睡 30 天、
|
|
历史≥5 条才算沉睡(对齐 churn real min_msgs=5)。analytics 现有页
|
|
(churn/客户资产)暂不切换(口径差异 v2 §5 标注),不动 636 既有测试。
|
|
"""
|
|
|
|
from app.analytics.lifecycle import DORMANT_DAYS, MIN_HISTORY_MSGS, NEW_DAYS, classify, day_add
|
|
|
|
|
|
def test_constants_locked_to_confirmed_defaults():
|
|
assert NEW_DAYS == 7 and DORMANT_DAYS == 30 and MIN_HISTORY_MSGS == 5
|
|
|
|
|
|
def test_new_customer():
|
|
# 首次沟通在 7 天窗口内
|
|
assert (
|
|
classify(first_day=20260901, last_day=20260903, prev_day=None, as_of=20260905, hist_msgs=2)
|
|
== "new"
|
|
)
|
|
# 窗口边界:as_of-6=20260830 含当天共 7 天 → new;再早一天 → 非 new
|
|
assert classify(20260830, 20260830, None, 20260905, 100) == "new"
|
|
assert classify(20260829, 20260829, None, 20260905, 100) == "cooling"
|
|
|
|
|
|
def test_reactivated_requires_gap_over_30d():
|
|
# 当日回流且前次间隔>30 天
|
|
assert (
|
|
classify(
|
|
first_day=20260101, last_day=20260905, prev_day=20260801, as_of=20260905, hist_msgs=50
|
|
)
|
|
== "reactivated"
|
|
)
|
|
# 间隔仅 9 天 → 只是 active
|
|
assert (
|
|
classify(
|
|
first_day=20260101, last_day=20260905, prev_day=20260827, as_of=20260905, hist_msgs=50
|
|
)
|
|
== "active"
|
|
)
|
|
# 间隔恰好 30 天 → 严格 >30 不算回流,落回 active
|
|
assert (
|
|
classify(
|
|
first_day=20260101, last_day=20260905, prev_day=20260806, as_of=20260905, hist_msgs=50
|
|
)
|
|
== "active"
|
|
)
|
|
# 回流必须当日互动:前次间隔 31 天(>30)但 last_day=as_of-1 → 不算回流,按 gap=1 落 active
|
|
assert classify(20260101, 20260904, 20260804, 20260905, 50) == "active"
|
|
|
|
|
|
def test_active_cooling_dormant_boundaries():
|
|
f, m = 20260101, 100
|
|
assert classify(f, 20260830, None, 20260905, m) == "active" # 6 天
|
|
assert classify(f, 20260829, None, 20260905, m) == "cooling" # 7 天
|
|
assert classify(f, 20260807, None, 20260905, m) == "cooling" # 29 天
|
|
assert classify(f, 20260806, None, 20260905, m) == "dormant" # 30 天
|
|
|
|
|
|
def test_dormant_requires_history():
|
|
# ≥30 天无互动但历史<5 条:不计入任何状态(None)
|
|
assert classify(20260101, 20260806, None, 20260905, 4) is None
|
|
assert classify(20260101, 20260806, None, 20260905, 5) == "dormant"
|
|
|
|
|
|
def test_no_interaction_returns_none():
|
|
assert classify(None, None, None, 20260905, 0) is None
|
|
|
|
|
|
def test_day_add():
|
|
assert day_add(20260831, 1) == 20260901
|
|
assert day_add(20260901, -7) == 20260825
|