Files
trade-assistant/backend/tests/test_customer_health.py
TradeMate Dev 7b62c2f8b4 feat: 修复 H5 底部导航覆盖 + 更新项目进度文档
## H5 底部导航修复 (Bug #10)
- 精简 App.vue,移除重复 tabbar,仅保留全局样式
- uni-page 设置 height: calc(100% - 50px) + overflow-y: auto
- 内容区域精确停在底部导航上方,独立滚动不再叠加
- 恢复 custom-tab-bar 组件

## 项目进度文档
- PROGRESS.md 更新至 10 个 Bug 修复
- 新增 H5 底部导航修复记录
- 新增历史变更条目
2026-05-12 20:24:42 +08:00

274 lines
9.1 KiB
Python

import pytest
from datetime import datetime, timedelta
from app.services.customer_health import CustomerHealthService
class TestSilenceScore:
def test_no_contact_returns_max_days(self):
assert CustomerHealthService.silence_days(None) == 999
def test_recent_contact_returns_0_days(self):
now = datetime.utcnow()
assert CustomerHealthService.silence_days(now) == 0
def test_3_days_ago(self):
dt = datetime.utcnow() - timedelta(days=3)
assert CustomerHealthService.silence_days(dt) == 3
def test_silence_score_0_days(self):
assert CustomerHealthService.calculate_silence_score(datetime.utcnow()) == 100
def test_silence_score_7_days(self):
dt = datetime.utcnow() - timedelta(days=7)
score = CustomerHealthService.calculate_silence_score(dt)
assert score == 50
def test_silence_score_14_days(self):
dt = datetime.utcnow() - timedelta(days=14)
score = CustomerHealthService.calculate_silence_score(dt)
assert score == 0
def test_silence_score_21_days_clamped(self):
dt = datetime.utcnow() - timedelta(days=21)
score = CustomerHealthService.calculate_silence_score(dt)
assert score == 0
class TestStatusWeight:
def test_customer_status(self):
assert CustomerHealthService.status_weight("customer") == 100
def test_negotiating_status(self):
assert CustomerHealthService.status_weight("negotiating") == 70
def test_lead_status(self):
assert CustomerHealthService.status_weight("lead") == 40
def test_lost_status(self):
assert CustomerHealthService.status_weight("lost") == 10
def test_unknown_status_defaults(self):
assert CustomerHealthService.status_weight("unknown") == 40
def test_none_status_defaults(self):
assert CustomerHealthService.status_weight(None) == 40
class TestGrade:
def test_active_grade(self):
assert CustomerHealthService.grade(100) == "active"
assert CustomerHealthService.grade(80) == "active"
assert CustomerHealthService.grade(85) == "active"
def test_watch_grade(self):
assert CustomerHealthService.grade(79) == "watch"
assert CustomerHealthService.grade(50) == "watch"
assert CustomerHealthService.grade(65) == "watch"
def test_critical_grade(self):
assert CustomerHealthService.grade(49) == "critical"
assert CustomerHealthService.grade(0) == "critical"
assert CustomerHealthService.grade(30) == "critical"
class TestResponseScore:
def test_both_none(self):
r = CustomerHealthService.calc_response_score(None, None)
assert r["score"] == 50
assert r["trend"] == "stable"
def test_only_recent_exists(self):
r = CustomerHealthService.calc_response_score(2.0, None)
assert r["score"] == 90
assert r["trend"] == "stable"
def test_improving_faster_response(self):
r = CustomerHealthService.calc_response_score(2.0, 10.0)
assert r["score"] == 90
assert r["trend"] == "improving"
def test_declining_slower_response(self):
r = CustomerHealthService.calc_response_score(10.0, 2.0)
assert r["trend"] == "declining"
def test_fast_response_high_score(self):
r = CustomerHealthService.calc_response_score(0.5, 5.0)
assert r["score"] >= 95
assert r["trend"] == "improving"
def test_very_slow_response_low_score(self):
r = CustomerHealthService.calc_response_score(48.0, 2.0)
assert r["score"] == 0
assert r["trend"] == "declining"
class TestSentimentScore:
def test_empty_messages_neutral(self):
r = CustomerHealthService.calc_sentiment_score([])
assert r["score"] == 50
assert r["label"] == "neutral"
def test_positive_message(self):
r = CustomerHealthService.calc_sentiment_score(["yes I'm interested thanks"])
assert r["score"] == 80
assert r["label"] == "positive"
def test_negative_message(self):
r = CustomerHealthService.calc_sentiment_score(["no not interested too expensive"])
assert r["score"] == 20
assert r["label"] == "negative"
def test_mixed_messages_neutral(self):
r = CustomerHealthService.calc_sentiment_score(["good quality", "but price is high"])
assert r["score"] == 50
assert r["label"] == "neutral"
def test_more_positive_than_negative(self):
r = CustomerHealthService.calc_sentiment_score([
"great product",
"yes please proceed",
"but shipping is expensive",
])
assert r["score"] == 80
assert r["label"] == "positive"
class TestInquiryDepthScore:
def test_empty_messages(self):
r = CustomerHealthService.calc_inquiry_depth_score([])
assert r["score"] == 0
assert r["signal_count"] == 0
def test_no_signals(self):
r = CustomerHealthService.calc_inquiry_depth_score(["hello", "how are you"])
assert r["score"] == 0
def test_one_signal(self):
r = CustomerHealthService.calc_inquiry_depth_score(["what is your price"])
assert r["score"] == 50
assert r["signal_count"] >= 1
def test_multiple_signals(self):
r = CustomerHealthService.calc_inquiry_depth_score([
"what is your MOQ and FOB price",
"do you have certification",
"what is the lead time",
])
assert r["score"] >= 75
assert r["signal_count"] >= 3
def test_deduplicates_signals(self):
r = CustomerHealthService.calc_inquiry_depth_score([
"what is the price",
"please send price and MOQ",
])
assert r["signal_count"] == 2
class TestBusinessValueScore:
def test_zero_value(self):
r = CustomerHealthService.calc_business_value_score(0)
assert r["score"] == 0
def test_small_value(self):
r = CustomerHealthService.calc_business_value_score(500)
assert r["score"] == 20
def test_medium_value(self):
r = CustomerHealthService.calc_business_value_score(5000)
assert r["score"] == 40
def test_large_value(self):
r = CustomerHealthService.calc_business_value_score(50000)
assert r["score"] == 80
def test_very_large_value(self):
r = CustomerHealthService.calc_business_value_score(200000)
assert r["score"] == 100
class TestTotalScore:
def test_perfect_health(self):
dims = {
"response_trend": {"score": 100},
"sentiment": {"score": 100},
"inquiry_depth": {"score": 100},
"silence": {"score": 100},
"business_value": {"score": 100},
}
r = CustomerHealthService.calc_total_score(dims)
assert r["total_score"] == 100
assert r["grade"] == "active"
def test_zero_health(self):
dims = {
"response_trend": {"score": 0},
"sentiment": {"score": 0},
"inquiry_depth": {"score": 0},
"silence": {"score": 0},
"business_value": {"score": 0},
}
r = CustomerHealthService.calc_total_score(dims)
assert r["total_score"] == 0
assert r["grade"] == "critical"
def test_mid_health(self):
dims = {
"response_trend": {"score": 60},
"sentiment": {"score": 50},
"inquiry_depth": {"score": 50},
"silence": {"score": 40},
"business_value": {"score": 50},
}
r = CustomerHealthService.calc_total_score(dims)
assert 45 <= r["total_score"] <= 55
class TestSuggestion:
def test_active_suggestion(self):
s = CustomerHealthService.suggestion("active", 1, "lead")
assert "良好" in s
def test_watch_with_silence(self):
s = CustomerHealthService.suggestion("watch", 5, "lead")
assert "5天" in s
assert "跟进" in s
def test_watch_no_silence(self):
s = CustomerHealthService.suggestion("watch", 1, "lead")
assert "关注" in s
def test_critical_lead(self):
s = CustomerHealthService.suggestion("critical", 10, "lead")
assert "10天" in s
assert "跟进" in s
def test_critical_lost(self):
s = CustomerHealthService.suggestion("critical", 20, "lost")
assert "重新激活" in s
class TestHealthOverview:
def test_overview_empty(self):
overview = CustomerHealthService._calculate_overview_static([])
assert overview["total"] == 0
assert overview["active"] == 0
assert overview["watch"] == 0
assert overview["critical"] == 0
def test_overview_mixed(self, monkeypatch):
class Row:
def __init__(self, status, days_ago):
self.status = status
self.last_contact_at = datetime.utcnow() - timedelta(days=days_ago)
rows = [
Row("customer", 1),
Row("lead", 7),
Row("negotiating", 14),
Row("lost", 30),
]
overview = CustomerHealthService._calculate_overview_static(rows)
assert overview["total"] == 4
assert overview["active"] == 1