Files
inquiry_robot/inquiry-agent/tests/test_deal_outcome.py
T
2026-09-21 18:28:35 +08:00

289 lines
11 KiB
Python

"""
成交三态:入账、推负责人、收下未成交原因、协商中不停会话。
"""
from __future__ import annotations
import unittest
from agent.ledger.memory_ledger import MemoryLedger
from agent.policy import inquiry_copy as copy
from agent.policy.air_text_flow import AirTextInquiryFlow, FlowSession
from agent.policy.deal_outcome import capture_lost_reason, finish_deal_outcome
from agent.policy.sea_text_flow import SeaTextInquiryFlow
class TestDealOutcome(unittest.TestCase):
def setUp(self) -> None:
self.ledger = MemoryLedger()
created = self.ledger.create_ticket(
sender_id="sales1",
business_line="SEA",
facts={"起运港": "上海", "目的港": "洛杉矶", "品名": "衣服"},
)
self.wo = str(created.get("work_order_no") or "")
self.ledger.transition(work_order_no=self.wo, to_status="已报价")
self.ledger.upsert_staff(
{
"name": "海运负责人",
"wecomId": "lead-sea",
"roleCode": "sea",
"routes": "海运全线",
"isLeader": 1,
"status": "active",
}
)
def test_deal_notifies_leaders(self) -> None:
queued: list[dict] = []
def enqueue(*, touser, content, dedupe_key, payload):
queued.append(
{
"touser": touser,
"content": content,
"dedupe_key": dedupe_key,
"payload": payload,
}
)
return True, "ok"
out = finish_deal_outcome(
ledger=self.ledger,
work_order_no=self.wo,
outcome="已成交",
facts={"起运港": "上海", "目的港": "洛杉矶"},
quote={"total": "1000", "currency": "USD"},
business_line="SEA",
sales_name="吴吉林",
enqueue=enqueue,
notify_ready=True,
)
self.assertTrue(out.get("ok"))
self.assertEqual(out.get("phase"), "done")
self.assertIn("海运负责人", str(out.get("notify_summary") or ""))
self.assertEqual(queued[0]["touser"], "lead-sea")
self.assertEqual(queued[0]["payload"].get("wecom_app"), "notify")
ticket = self.ledger.get_ticket(work_order_no=self.wo)
self.assertEqual(ticket.status, "已成交")
self.assertEqual(ticket.next_followup_at, "")
self.assertEqual(ticket.event_wait_kind, "")
def test_deal_notify_card_omits_sales_label(self) -> None:
"""成交通知不要「销售」二字:标题副标和正文都不写。"""
text = copy.deal_notify_card(
work_order_no="WO202609180008",
sales_name="吴吉林",
facts={"起运地": "泉州", "目的地": "南京", "线路类别": "国内城配/拖车/打包"},
quote={},
business_line="LAND",
)
self.assertNotIn("销售", text)
payload = copy.deal_notify_wecom_payload(
work_order_no="WO202609180008",
sales_name="吴吉林",
facts={"起运地": "泉州", "目的地": "南京", "线路类别": "国内城配/拖车/打包"},
quote={},
business_line="LAND",
)
title = (payload.get("template_card") or {}).get("main_title") or {}
blob = str(title.get("title") or "") + str(title.get("desc") or "")
self.assertNotIn("销售", blob)
self.assertNotIn("desc", title)
def test_deal_without_leader_still_closes(self) -> None:
self.ledger.upsert_staff(
{
"name": "不是负责人",
"wecomId": "lead-sea",
"roleCode": "sea",
"routes": "海运全线",
"isLeader": 0,
"status": "active",
}
)
out = finish_deal_outcome(
ledger=self.ledger,
work_order_no=self.wo,
outcome="已成交",
facts={},
quote={},
business_line="SEA",
notify_ready=True,
)
self.assertEqual(out.get("notify_summary"), "无匹配负责人")
self.assertEqual(self.ledger.get_ticket(work_order_no=self.wo).status, "已成交")
def test_lost_waits_for_reason(self) -> None:
out = finish_deal_outcome(
ledger=self.ledger,
work_order_no=self.wo,
outcome="未成交",
facts={},
quote={},
business_line="SEA",
)
self.assertEqual(out.get("phase"), "wait_lost_reason")
ticket = self.ledger.get_ticket(work_order_no=self.wo)
self.assertEqual(ticket.status, "未成交")
self.assertEqual(ticket.lost_reason, "")
self.assertEqual(ticket.event_wait_kind, "lost_reason")
view = self.ledger.get_for_agent(work_order_no=self.wo, sender_id="sales1")
self.assertEqual(view.get("wait_phase"), "wait_lost_reason")
captured = capture_lost_reason(
ledger=self.ledger, work_order_no=self.wo, reason="客人比价了"
)
self.assertTrue(captured.get("ok"))
ticket = self.ledger.get_ticket(work_order_no=self.wo)
self.assertEqual(ticket.lost_reason, "客人比价了")
self.assertEqual(ticket.event_wait_kind, "")
view = self.ledger.get_for_agent(work_order_no=self.wo, sender_id="sales1")
self.assertEqual(view.get("wait_phase"), "done")
def test_lost_reason_ack_repeats_user_text(self) -> None:
self.assertEqual(copy.lost_reason_ack("客户不要了"), "已记下未成交原因:客户不要了。")
self.assertEqual(copy.lost_reason_ack("价格高。"), "已记下未成交原因:价格高。")
def test_negotiate_ack_names_followup_day(self) -> None:
text = copy.deal_ack("协商中", "WO202609190007", "2026-09-24 09:00:00")
self.assertEqual(
text,
"已记录为「协商中」。已顺延三个工作日,9月24日的9:00再次发起成交状态询问。",
)
self.assertNotIn("跟进周期已顺延", text)
def test_negotiate_keeps_wait_deal(self) -> None:
self.ledger.start_event_wait(work_order_no=self.wo, wait_kind="deal")
out = finish_deal_outcome(
ledger=self.ledger,
work_order_no=self.wo,
outcome="协商中",
facts={},
quote={},
business_line="SEA",
)
self.assertEqual(out.get("phase"), "wait_deal")
ticket = self.ledger.get_ticket(work_order_no=self.wo)
self.assertEqual(ticket.status, "协商中")
self.assertEqual(ticket.event_wait_kind, "")
self.assertTrue(ticket.next_followup_at)
view = self.ledger.get_for_agent(work_order_no=self.wo, sender_id="sales1")
self.assertEqual(view.get("wait_phase"), "wait_deal")
again = finish_deal_outcome(
ledger=self.ledger,
work_order_no=self.wo,
outcome="协商中",
facts={},
quote={},
business_line="SEA",
)
self.assertEqual(again.get("phase"), "wait_deal")
self.assertEqual(self.ledger.get_ticket(work_order_no=self.wo).status, "协商中")
self.assertTrue(again.get("next_followup_at"))
def test_private_next_text_is_lost_reason(self) -> None:
flow = AirTextInquiryFlow(self.ledger)
sess = FlowSession(
sender_id="sales1",
thread_id="sales1",
phase="wait_deal",
business_line="SEA",
work_order_no=self.wo,
allowed=(copy.BTN_DEAL, copy.BTN_LOST, copy.BTN_NEGOTIATE),
)
flow._save(sess)
replies: list[str] = []
phase = flow.on_button(
sender_id="sales1",
action=copy.BTN_LOST,
reply=lambda t, extra=None: replies.append(t),
)
self.assertEqual(phase, "wait_lost_reason")
phase = flow.on_text(
sender_id="sales1",
text="价格高",
reply=lambda t, extra=None: replies.append(t),
)
self.assertEqual(phase, "done")
self.assertEqual(self.ledger.get_ticket(work_order_no=self.wo).lost_reason, "价格高")
self.assertTrue(any(t == "已记下未成交原因:价格高。" for t in replies))
def test_sea_private_next_text_is_lost_reason(self) -> None:
flow = SeaTextInquiryFlow(self.ledger)
flow._save(
FlowSession(
sender_id="sales1",
thread_id="sales1",
phase="wait_deal",
business_line="SEA",
work_order_no=self.wo,
allowed=(copy.BTN_DEAL, copy.BTN_LOST, copy.BTN_NEGOTIATE),
)
)
replies: list[str] = []
phase = flow.on_button(
sender_id="sales1",
action=copy.BTN_LOST,
reply=lambda t, extra=None: replies.append(t),
)
self.assertEqual(phase, "wait_lost_reason")
phase = flow.on_text(
sender_id="sales1",
text="客人比价了",
reply=lambda t, extra=None: replies.append(t),
)
self.assertEqual(phase, "done")
self.assertEqual(self.ledger.get_ticket(work_order_no=self.wo).lost_reason, "客人比价了")
def test_claim_due_negotiate_clears_schedule(self) -> None:
finish_deal_outcome(
ledger=self.ledger,
work_order_no=self.wo,
outcome="协商中",
facts={},
quote={},
business_line="SEA",
)
ticket = self.ledger.get_ticket(work_order_no=self.wo)
ticket.next_followup_at = "2020-01-01 09:00:00"
rows = self.ledger.claim_due_negotiate()
self.assertEqual(len(rows), 1)
self.assertEqual(rows[0]["work_order_no"], self.wo)
self.assertEqual(self.ledger.get_ticket(work_order_no=self.wo).next_followup_at, "")
def test_reask_starts_deal_wait(self) -> None:
"""协商再问发出后,重新盯销售成交跟进。"""
import importlib.util
import os
path = os.path.join(
os.path.dirname(__file__), "..", "agent", "jobs", "deal_followup_poll.py"
)
spec = importlib.util.spec_from_file_location("deal_followup_poll_ee", path)
self.assertIsNotNone(spec)
self.assertIsNotNone(spec.loader)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
class Cfg:
wecom_corp_id = ""
wecom_secret = ""
wecom_agent_id = "0"
mod._send_group_reask = lambda **kwargs: True
ok = mod._reask_one(
{
"work_order_no": self.wo,
"collab_chat_id": "wr1",
"sales_name": "吴吉林",
"facts": {},
"quote": {},
"business_line": "SEA",
},
settings=Cfg(),
ledger=self.ledger,
)
self.assertTrue(ok)
ticket = self.ledger.get_ticket(work_order_no=self.wo)
self.assertEqual(ticket.event_wait_kind, "deal")