7b62c2f8b4
## H5 底部导航修复 (Bug #10) - 精简 App.vue,移除重复 tabbar,仅保留全局样式 - uni-page 设置 height: calc(100% - 50px) + overflow-y: auto - 内容区域精确停在底部导航上方,独立滚动不再叠加 - 恢复 custom-tab-bar 组件 ## 项目进度文档 - PROGRESS.md 更新至 10 个 Bug 修复 - 新增 H5 底部导航修复记录 - 新增历史变更条目
274 lines
9.1 KiB
Python
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
|