空运人工价锁舱现拉补全舱位并拦清缺参;成交/报价填表与工单陆运费率展示同步收紧。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -421,6 +421,21 @@ class AirGroupCollabTests(unittest.TestCase):
|
||||
classify_air_group_text("@询价小助手 WO202609160002", allow_network=False),
|
||||
INTENT_ACTIVATE,
|
||||
)
|
||||
# 话术前缀「工单编号」不能当成报价(WO026 复现:误走 product_quote)。
|
||||
self.assertEqual(
|
||||
classify_air_group_text(
|
||||
"工单编号WO202610070026@询价小助手",
|
||||
allow_network=False,
|
||||
),
|
||||
INTENT_ACTIVATE,
|
||||
)
|
||||
self.assertEqual(
|
||||
classify_air_group_text(
|
||||
"工单编号 WO202610060026@询价小助手",
|
||||
allow_network=False,
|
||||
),
|
||||
INTENT_ACTIVATE,
|
||||
)
|
||||
from unittest.mock import patch
|
||||
|
||||
with patch(
|
||||
@@ -434,6 +449,13 @@ class AirGroupCollabTests(unittest.TestCase):
|
||||
),
|
||||
INTENT_ACTIVATE,
|
||||
)
|
||||
self.assertEqual(
|
||||
classify_air_group_text(
|
||||
"工单编号WO202610070026@询价小助手",
|
||||
allow_network=True,
|
||||
),
|
||||
INTENT_ACTIVATE,
|
||||
)
|
||||
|
||||
def test_dispatch_aibot_goes_air(self) -> None:
|
||||
from agent.routing.dispatch import dispatch_inbound as _disp
|
||||
@@ -906,7 +928,8 @@ class AirGroupCollabTests(unittest.TestCase):
|
||||
self.assertFalse(any("AIR-OPT-01" in t for t in self._texts()))
|
||||
self.assertFalse(any("执行成功" in t for t in self._texts()))
|
||||
|
||||
def test_manual_quote_without_quote_id_can_lock(self) -> None:
|
||||
def test_manual_quote_without_option_asks_fields(self) -> None:
|
||||
"""纯人工价且口令无 AIR-OPT:交主账后仍缺 → lock_incomplete(不空壳成功)。"""
|
||||
wo = self._create_air(
|
||||
quote={
|
||||
"total": "100",
|
||||
@@ -921,6 +944,33 @@ class AirGroupCollabTests(unittest.TestCase):
|
||||
ledger=self.ledger,
|
||||
injected_intent="air_lock",
|
||||
)
|
||||
self.assertEqual(phase, "lock_incomplete")
|
||||
joined = "\n".join(self._texts())
|
||||
self.assertIn("舱位编号", joined)
|
||||
self.assertFalse(any("执行成功" in t for t in self._texts()))
|
||||
|
||||
def test_manual_quote_with_option_airline_can_lock(self) -> None:
|
||||
"""人工价补齐舱位+航司+航班后可锁;计费重用工单毛重兜底。"""
|
||||
wo = self._create_air(
|
||||
quote={
|
||||
"total": "100",
|
||||
"source": "manual",
|
||||
"source_label": "产品报价",
|
||||
"currency": "CNY",
|
||||
},
|
||||
status="已报价",
|
||||
)
|
||||
phase = handle_air_group(
|
||||
self._msg(
|
||||
sender="air1",
|
||||
text=f"{wo}锁舱,锁AIR-OPT01 CZ 航班CZ327",
|
||||
chat="wr_air_lock_manual2",
|
||||
mid="lm2",
|
||||
),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
injected_intent="air_lock",
|
||||
)
|
||||
self.assertEqual(phase, "lock_ok")
|
||||
self.assertTrue(any("执行成功" in t for t in self._texts()))
|
||||
|
||||
@@ -1348,7 +1398,7 @@ class AirGroupCollabTests(unittest.TestCase):
|
||||
|
||||
def test_lock_without_quote_still_shows_tms_reason(self) -> None:
|
||||
"""TMS 拒绝锁舱时原样回失败原因,不要改写成选项不匹配。"""
|
||||
wo = self._create_air(quote={}, status="询价中")
|
||||
wo = self._create_air()
|
||||
self.ledger._cabin_lock_result = {
|
||||
"ok": False,
|
||||
"error": "lock_fail",
|
||||
|
||||
@@ -144,10 +144,75 @@ class TestDealOutcome(unittest.TestCase):
|
||||
view = self.ledger.get_for_agent(work_order_no=self.wo, sender_id="sales1")
|
||||
self.assertEqual(view.get("wait_phase"), "done")
|
||||
|
||||
def test_lost_from_inquiring_promotes_quoted_first(self) -> None:
|
||||
"""主账仍询价中时点未成交:先补已报价再入未成交,禁止直接失败。"""
|
||||
|
||||
class _StrictSix:
|
||||
def __init__(self) -> None:
|
||||
self.status = "询价中"
|
||||
self.lost_reason = ""
|
||||
self.event_wait_kind = ""
|
||||
|
||||
def transition(self, *, work_order_no, to_status, **kwargs):
|
||||
_ = work_order_no
|
||||
allowed = {
|
||||
"询价中": {"已报价", "协商中", "已关闭"},
|
||||
"已报价": {"协商中", "已成交", "未成交", "已关闭"},
|
||||
}
|
||||
if to_status not in allowed.get(self.status, set()):
|
||||
# 模拟 http_ledger 在 500 时只回 http_500(丢失正文)
|
||||
return {"ok": False, "error": "http_500"}
|
||||
self.status = to_status
|
||||
if to_status == "未成交" and kwargs.get("lost_reason"):
|
||||
self.lost_reason = str(kwargs.get("lost_reason") or "")
|
||||
return {"ok": True, "status": self.status}
|
||||
|
||||
def patch_lost_reason(self, *, work_order_no, lost_reason):
|
||||
_ = work_order_no
|
||||
self.lost_reason = lost_reason
|
||||
return {"ok": True}
|
||||
|
||||
def get_for_agent(self, *, work_order_no, sender_id):
|
||||
_ = (work_order_no, sender_id)
|
||||
return {"status": self.status, "lost_reason": self.lost_reason}
|
||||
|
||||
ledger = _StrictSix()
|
||||
out = finish_deal_outcome(
|
||||
ledger=ledger,
|
||||
work_order_no="WO202610070023",
|
||||
outcome="未成交",
|
||||
facts={},
|
||||
quote={},
|
||||
business_line="AIR",
|
||||
)
|
||||
self.assertTrue(out.get("ok"), out)
|
||||
self.assertEqual(ledger.status, "未成交")
|
||||
self.assertEqual(out.get("phase"), "wait_lost_reason")
|
||||
|
||||
def test_lost_reason_ack_repeats_user_text(self) -> None:
|
||||
self.assertEqual(copy.lost_reason_ack("客户不要了"), "已记下未成交原因:客户不要了。")
|
||||
self.assertEqual(copy.lost_reason_ack("价格高。"), "已记下未成交原因:价格高。")
|
||||
|
||||
def test_lost_reason_ack_fills_ticket_no_from_bot_reply(self) -> None:
|
||||
"""后台文案带 {{ticket_no}} 时必须填实值,禁止原样打出占位符。"""
|
||||
from agent.policy import bot_reply
|
||||
|
||||
bot_reply.override_catalog(
|
||||
{
|
||||
"lost.reason_ack": (
|
||||
"已将未成交原因“{{lost_reason}}”记录在后台,"
|
||||
"工单{{ticket_no}}已关闭,智能体不再跟进该工单,请悉知。"
|
||||
)
|
||||
}
|
||||
)
|
||||
try:
|
||||
text = copy.lost_reason_ack("换时间", work_order_no="WO202610070018")
|
||||
self.assertIn("WO202610070018", text)
|
||||
self.assertNotIn("{{ticket_no}}", text)
|
||||
self.assertIn("换时间", text)
|
||||
finally:
|
||||
bot_reply.override_catalog(None)
|
||||
|
||||
def test_harvest_inline_lost_reason(self) -> None:
|
||||
self.assertEqual(harvest_inline_lost_reason("未成交,原因客户不想要了"), "客户不想要了")
|
||||
self.assertEqual(
|
||||
|
||||
@@ -1341,6 +1341,28 @@ class SeaGroupCollabFieldTests(unittest.TestCase):
|
||||
self.assertEqual(str(rows["海运费"].get("amount") or ""), "100.00")
|
||||
self.assertEqual(str(rows["燃油附加费"].get("amount") or ""), "15.00")
|
||||
|
||||
def test_land_weight_band_unit_price_and_rates(self) -> None:
|
||||
"""陆运集拼:2-500KG 单价 + 燃油附加费率/税率,不能把档下限 2 当成燃油附加费。"""
|
||||
from agent.llm.extract_text import extract_product_quote
|
||||
|
||||
quote = extract_product_quote(
|
||||
"2-500KG 单价CNY990,燃油附加费率1%,税率6%"
|
||||
)
|
||||
self.assertEqual(str(quote.get("total") or ""), "990")
|
||||
self.assertEqual(str(quote.get("fuel_surcharge_rate") or ""), "1%")
|
||||
self.assertEqual(str(quote.get("tax_rate") or ""), "6%")
|
||||
tiers = list(quote.get("weightPrices") or [])
|
||||
self.assertEqual(len(tiers), 1)
|
||||
self.assertEqual(int(tiers[0].get("min") or 0), 2)
|
||||
self.assertEqual(int(tiers[0].get("max") or 0), 500)
|
||||
self.assertEqual(str(tiers[0].get("price") or ""), "990")
|
||||
rows = {str(x.get("name") or ""): x for x in (quote.get("fee_rows") or [])}
|
||||
self.assertIn("2-500KG", rows)
|
||||
self.assertEqual(str(rows["2-500KG"].get("amount") or ""), "990")
|
||||
self.assertNotEqual(str(rows.get("燃油附加费", {}).get("amount") or ""), "2")
|
||||
self.assertEqual(str(rows.get("燃油附加费率", {}).get("amount") or ""), "1%")
|
||||
self.assertEqual(str(rows.get("税率", {}).get("amount") or ""), "6%")
|
||||
|
||||
def test_air_unit_qty_amount_keeps_air_freight_name(self) -> None:
|
||||
"""空运费 单价/数量/金额:费用名是空运费,金额用「金额」不是单价。"""
|
||||
from agent.llm.extract_text import extract_product_quote
|
||||
|
||||
@@ -477,22 +477,45 @@ class TestConfirmSystemException(unittest.TestCase):
|
||||
self.assertTrue("USD" in joined or "拉产品" in joined or "已报价" in joined)
|
||||
|
||||
def test_admin_retry_cabin_lock_runs_lock(self) -> None:
|
||||
# 空运单 + 可锁报价;重试必须带上 operator_name / airline。
|
||||
air = self.ledger.create_ticket(
|
||||
sender_id="sales1",
|
||||
business_line="AIR",
|
||||
facts={"起运地": "上海", "目的地": "洛杉矶", "毛重": "100"},
|
||||
)
|
||||
air_wo = str(air.get("work_order_no") or "")
|
||||
self.ledger.upsert_quote(
|
||||
work_order_no=air_wo,
|
||||
quote={
|
||||
"total": "800",
|
||||
"source": "TMS",
|
||||
"quoteId": "QT-AIR-1",
|
||||
"airOptions": [{"optionNo": "AIR-OPT-01", "airlineCode": "CZ"}],
|
||||
},
|
||||
to_status="已报价",
|
||||
)
|
||||
self.ledger.mark_system_exception(
|
||||
work_order_no=self.wo,
|
||||
work_order_no=air_wo,
|
||||
system_exception=TYPE_TMS,
|
||||
system_exception_reason="空运锁舱接口调用失败",
|
||||
step=STEP_TMS_CABIN,
|
||||
payload={
|
||||
"conversation_kind": "group",
|
||||
"conversation_target": "chat-air",
|
||||
"extra": {"option_no": "AIR-OPT-01", "instruction": "锁舱"},
|
||||
"extra": {
|
||||
"option_no": "AIR-OPT-01",
|
||||
"instruction": "锁舱",
|
||||
"operator_name": "HangXian",
|
||||
"airline": "CZ",
|
||||
},
|
||||
},
|
||||
)
|
||||
hits = {"n": 0}
|
||||
hits = {"n": 0, "kwargs": {}}
|
||||
orig_lock = self.ledger.lock_cabin
|
||||
|
||||
def counted(**kwargs):
|
||||
hits["n"] += 1
|
||||
hits["kwargs"] = dict(kwargs)
|
||||
return orig_lock(**kwargs)
|
||||
|
||||
self.ledger.lock_cabin = counted # type: ignore[method-assign]
|
||||
@@ -504,12 +527,14 @@ class TestConfirmSystemException(unittest.TestCase):
|
||||
orig = se._send_to_conversation
|
||||
se._send_to_conversation = lambda **kwargs: talks.append(str(kwargs.get("text") or ""))
|
||||
try:
|
||||
out = run_admin_retry(self.wo, ledger=self.ledger)
|
||||
out = run_admin_retry(air_wo, ledger=self.ledger)
|
||||
finally:
|
||||
se._send_to_conversation = orig
|
||||
self.assertTrue(out.get("ok"))
|
||||
self.assertGreaterEqual(hits["n"], 1)
|
||||
ticket = self.ledger.get_ticket(work_order_no=self.wo)
|
||||
self.assertEqual(hits["kwargs"].get("operator_name"), "HangXian")
|
||||
self.assertEqual(hits["kwargs"].get("airline"), "CZ")
|
||||
ticket = self.ledger.get_ticket(work_order_no=air_wo)
|
||||
self.assertFalse(ticket_is_paused(ticket))
|
||||
self.assertTrue(any("空运舱位接口已恢复" in x for x in talks))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user