"""Tests for GET /api/staff-chat-stats — 微信员工 5 数字 + 客户明细(本地 message 聚合)。""" from __future__ import annotations from sqlalchemy.ext.asyncio import AsyncSession from app.db.models import Customer, Message OWNER = "wx_owner_1" WIN_START = 1_700_000_000_000 # 固定 ms-epoch,确定性 WIN_END = WIN_START + 3 * 86_400_000 DAY = 86_400_000 async def _seed(session: AsyncSession) -> None: session.add_all( [ # 客户 A:跨 2 天双向 → effective + follow,员工最后发 Message(msg_id="a1", channel="wechat", owner_account_id=OWNER, talker_id="t_a", room_id=None, is_send=True, timestamp=WIN_START, type="text", type_raw=1, text="x"), Message(msg_id="a2", channel="wechat", owner_account_id=OWNER, talker_id="t_a", room_id=None, is_send=False, timestamp=WIN_START + DAY, type="text", type_raw=1, text="y"), Message(msg_id="a3", channel="wechat", owner_account_id=OWNER, talker_id="t_a", room_id=None, is_send=True, timestamp=WIN_START + DAY + 1000, type="text", type_raw=1, text="z"), # 客户 B:仅接收 → 非 effective,客户最后发(未回复) Message(msg_id="b1", channel="wechat", owner_account_id=OWNER, talker_id="t_b", room_id=None, is_send=False, timestamp=WIN_START + 2000, type="text", type_raw=1, text="hi"), # 客户 C:单条员工发 → 员工最后发,1 天 Message(msg_id="c1", channel="wechat", owner_account_id=OWNER, talker_id="t_c", room_id=None, is_send=True, timestamp=WIN_START + 3000, type="text", type_raw=1, text="c"), # 群消息(room_id 非空)——必须被全部排除 Message(msg_id="g1", channel="wechat", owner_account_id=OWNER, talker_id="t_grp_member", room_id="room123@chatroom", is_send=True, timestamp=WIN_START + 4000, type="text", type_raw=1, text="g"), # 窗口外私聊——必须排除(不污染 t_a 统计) Message(msg_id="o1", channel="wechat", owner_account_id=OWNER, talker_id="t_a", room_id=None, is_send=True, timestamp=WIN_END + 1000, type="text", type_raw=1, text="future"), # 其他 owner——必须排除 Message(msg_id="x1", channel="wechat", owner_account_id="wx_other", talker_id="t_a", room_id=None, is_send=True, timestamp=WIN_START + 5000, type="text", type_raw=1, text="other"), Customer(friend_id="t_a", channel="wechat", owner_account_id=OWNER, friend_nickname="Alice", friend_remark="Alice备注", real_name=None), Customer(friend_id="t_b", channel="wechat", owner_account_id=OWNER, friend_nickname="Bob", friend_remark=None, real_name=None), # t_c 无 customer 行 → 昵称 fallback 为 talker_id "t_c" ] ) await session.commit() async def test_summary_five_numbers(make_client): client = await make_client(_seed) r = await client.get( "/api/staff-chat-stats", params={"owner_account_id": OWNER, "start": WIN_START, "end": WIN_END}, ) assert r.status_code == 200 s = r.json()["summary"] assert s == { "chat_count": 3, # t_a, t_b, t_c(群/窗外/他owner 全排除) "effective_count": 1, # 仅 t_a 双向 "follow_count": 1, # 仅 t_a 跨 2 天 "last_reply_count": 2, # t_a, t_c 员工最后发 "no_reply_count": 1, # t_b 客户最后发 } async def test_customers_fields_and_names(make_client): client = await make_client(_seed) r = await client.get( "/api/staff-chat-stats", params={"owner_account_id": OWNER, "start": WIN_START, "end": WIN_END}, ) by_id = {c["talker_id"]: c for c in r.json()["customers"]} assert set(by_id) == {"t_a", "t_b", "t_c"} # t_a: send=2 recv=1 days=2 last_is_send=1 last_time=a3 assert by_id["t_a"]["friend_name"] == "Alice备注" # remark 优先 assert by_id["t_a"]["send_count"] == 2 assert by_id["t_a"]["receive_count"] == 1 assert by_id["t_a"]["interaction_days"] == 2 assert by_id["t_a"]["last_is_send"] == 1 assert by_id["t_a"]["last_time"] == WIN_START + DAY + 1000 # t_b assert by_id["t_b"]["friend_name"] == "Bob" assert by_id["t_b"]["send_count"] == 0 assert by_id["t_b"]["receive_count"] == 1 assert by_id["t_b"]["interaction_days"] == 1 assert by_id["t_b"]["last_is_send"] == 0 # t_c 无 customer 行 → fallback assert by_id["t_c"]["friend_name"] == "t_c" assert by_id["t_c"]["last_is_send"] == 1 async def test_group_messages_excluded(make_client): client = await make_client(_seed) r = await client.get( "/api/staff-chat-stats", params={"owner_account_id": OWNER, "start": WIN_START, "end": WIN_END}, ) ids = {c["talker_id"] for c in r.json()["customers"]} assert "t_grp_member" not in ids # 群消息被 room_id 过滤 async def test_date_window_and_owner_filter(make_client): """窗外消息与他 owner 消息不污染统计(t_a 未被 o1/x1 抬高)。""" client = await make_client(_seed) r = await client.get( "/api/staff-chat-stats", params={"owner_account_id": OWNER, "start": WIN_START, "end": WIN_END}, ) by_id = {c["talker_id"]: c for c in r.json()["customers"]} assert by_id["t_a"]["send_count"] == 2 # 非 4(a1+a3,排除 o1+x1) assert by_id["t_a"]["last_time"] == WIN_START + DAY + 1000 # 非 o1 async def test_empty_when_no_messages(make_client): async def _empty(session: AsyncSession) -> None: await session.commit() client = await make_client(_empty) r = await client.get( "/api/staff-chat-stats", params={"owner_account_id": OWNER, "start": WIN_START, "end": WIN_END}, ) assert r.status_code == 200 body = r.json() assert body["customers"] == [] assert body["summary"] == { "chat_count": 0, "effective_count": 0, "follow_count": 0, "last_reply_count": 0, "no_reply_count": 0, } async def test_default_window_returns_ok(make_client): """省略 start/end 走默认近 3 天;2023 固定种子在默认窗外 → 空集,仅验结构。""" client = await make_client(_seed) r = await client.get("/api/staff-chat-stats", params={"owner_account_id": OWNER}) assert r.status_code == 200 assert "summary" in r.json() and "customers" in r.json() async def test_last_is_send_none_excluded_from_rates(make_client): """最后一条私聊 is_send IS NULL → last_is_send=None,计入 chat_count/ interaction_days,但被 ==1/==0 同时排除(两率都不计)。""" async def seed(session: AsyncSession) -> None: session.add( Message( msg_id="n1", channel="wechat", owner_account_id=OWNER, talker_id="t_n", room_id=None, is_send=None, timestamp=WIN_START, type="text", type_raw=1, text="unknown-dir", ) ) await session.commit() client = await make_client(seed) r = await client.get( "/api/staff-chat-stats", params={"owner_account_id": OWNER, "start": WIN_START, "end": WIN_END}, ) assert r.status_code == 200 body = r.json() customers = body["customers"] assert len(customers) == 1 c = customers[0] assert c["talker_id"] == "t_n" assert c["last_is_send"] is None assert c["send_count"] == 0 assert c["receive_count"] == 0 assert c["interaction_days"] == 1 assert body["summary"] == { "chat_count": 1, "effective_count": 0, "follow_count": 0, "last_reply_count": 0, "no_reply_count": 0, }