补齐成交跟进:按线路推成交通知、收下未成交原因、协商中按工作日再问。
成交走询价消息服务;陆运/海运私聊下一句能记下原因;协商中回执写明顺延三个工作日和下次 9:00。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
"""
|
||||
成交三态:入账、推负责人、收下未成交原因、协商中不停会话。
|
||||
"""
|
||||
|
||||
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, "")
|
||||
|
||||
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, "")
|
||||
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, "客人比价了")
|
||||
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:
|
||||
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.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, "")
|
||||
Reference in New Issue
Block a user