Files
浅念 9dc22cd1e6 feat(management): 生命周期判定纯函数——距今天数口径/7天新客/30天沉睡/历史门槛5条
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>
2026-09-09 13:37:22 +08:00

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