空运协同里手工改费用、有起运地、锁舱失败或缺参会被误判,报价单已出后再激活又重发模板。按销售侧口径收紧这些分支,并让价格卡和未成交回执带上真实工单号。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -160,7 +160,7 @@ class AirGroupCollabTests(unittest.TestCase):
|
||||
"source_label": "TMS 标准报价",
|
||||
"quoteId": "QT-AIR-1",
|
||||
"airOptions": [
|
||||
{"optionNo": "AIR-OPT-01", "airlineCode": "CZ", "airlineName": "南航"}
|
||||
{"optionNo": "AIR-OPT-01", "airlineCode": "CZ", "airlineName": "南航", "flightNo": "CZ327"}
|
||||
],
|
||||
}
|
||||
if quote:
|
||||
@@ -203,6 +203,67 @@ class AirGroupCollabTests(unittest.TestCase):
|
||||
self.assertEqual(classify_air_group_text("帮我锁舱 WO202609160001", allow_network=False), INTENT_LOCK)
|
||||
self.assertEqual(classify_air_group_text("WO202609160001释放舱位", allow_network=False), INTENT_RELEASE)
|
||||
|
||||
def test_airline_fee_wording_is_quote_not_adjust(self) -> None:
|
||||
"""航线写「空运费改为900」是手工报价,不能先判成调价。比例和改总价仍是调价。"""
|
||||
from agent.routing.air_group_intent import INTENT_ADJUST, INTENT_QUOTE
|
||||
|
||||
self.assertEqual(
|
||||
classify_air_group_text("空运费改为900USD", allow_network=False),
|
||||
INTENT_QUOTE,
|
||||
)
|
||||
self.assertEqual(
|
||||
classify_air_group_text("报关费改为300CNY", allow_network=False),
|
||||
INTENT_QUOTE,
|
||||
)
|
||||
self.assertEqual(
|
||||
classify_air_group_text("空运费 9900CNY", allow_network=False),
|
||||
INTENT_QUOTE,
|
||||
)
|
||||
self.assertEqual(
|
||||
classify_air_group_text("空运费加10%", allow_network=False),
|
||||
INTENT_ADJUST,
|
||||
)
|
||||
self.assertEqual(
|
||||
classify_air_group_text("总销售价改为180", allow_network=False),
|
||||
INTENT_ADJUST,
|
||||
)
|
||||
|
||||
def test_airline_rewritten_fee_does_not_say_only_sales(self) -> None:
|
||||
"""已有报价后,航线再写费用金额,不能提示只有销售能调价。"""
|
||||
wo = self._create_air()
|
||||
handle_air_group(
|
||||
self._msg(sender="air1", text=wo, chat="wr_air_1", mid="fee0"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
)
|
||||
self.groups.sent.clear()
|
||||
phase = handle_air_group(
|
||||
self._msg(sender="air1", text="空运费改为900USD", chat="wr_air_1", mid="fee1"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
)
|
||||
self.assertNotEqual(phase, "adjust_not_sales")
|
||||
self.assertNotIn("只有这张单的销售能调价", "\n".join(self._texts()))
|
||||
self.assertIn(phase, {"wait_tms_choice", "quoted"})
|
||||
|
||||
def test_sales_percent_still_adjusts(self) -> None:
|
||||
"""销售说加百分之几,仍是调价,不会被改成航线报价。"""
|
||||
wo = self._create_air()
|
||||
handle_air_group(
|
||||
self._msg(sender="sales1", text=wo, chat="wr_air_1", mid="adj0"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
)
|
||||
self.groups.sent.clear()
|
||||
phase = handle_air_group(
|
||||
self._msg(sender="sales1", text="空运费加10%", chat="wr_air_1", mid="adj1"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
)
|
||||
self.assertNotEqual(phase, "not_airline")
|
||||
self.assertNotIn("只有这张单的销售能调价", "\n".join(self._texts()))
|
||||
self.assertIn(phase, {"group_adjusted", "adjust_need_fee", "adjust_none", "adjust_need_lines"})
|
||||
|
||||
def test_air_group_brief_matches_sea_sections(self) -> None:
|
||||
text = copy.air_group_brief(
|
||||
work_order_no="WO202609160009",
|
||||
@@ -373,6 +434,51 @@ class AirGroupCollabTests(unittest.TestCase):
|
||||
self.assertIn("费用合计:CNY 200040", tms_block)
|
||||
self.assertEqual(tms_block.count("费用合计:"), 1)
|
||||
|
||||
def test_air_group_brief_keeps_sole_air_freight_line(self) -> None:
|
||||
"""TMS 只有空运费、金额等于总价时,群摘要必须写出这一行,不能只剩费用合计。"""
|
||||
tms = {
|
||||
"source": "TMS",
|
||||
"source_label": "TMS标准报价",
|
||||
"total": "1833.7",
|
||||
"freight": "1833.70",
|
||||
"currency": "CNY",
|
||||
"valid_until": "2026-10-21",
|
||||
"fee_lines": '[{"name":"空运费","amount":"CNY 1833.70"}]',
|
||||
"feeItems": [
|
||||
{
|
||||
"feeName": "空运费",
|
||||
"feeCode": "AIR_FREIGHT",
|
||||
"amount": 1833.70,
|
||||
"unitPrice": 11.00,
|
||||
"quantity": 166.7,
|
||||
"currency": "CNY",
|
||||
}
|
||||
],
|
||||
"airOptions": [
|
||||
{
|
||||
"originAirportCode": "CGO",
|
||||
"destinationAirportCode": "KUL",
|
||||
"salePrice": 1833.70,
|
||||
"currency": "CNY",
|
||||
"departureCharges": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
text = copy.air_group_brief(
|
||||
work_order_no="WO202610080020",
|
||||
facts={"起运地": "CGO", "目的地": "吉隆坡", "originCode": "CGO"},
|
||||
quote=tms,
|
||||
collab_facts={},
|
||||
has_price=True,
|
||||
tms_quote=tms,
|
||||
)
|
||||
tms_block = text.split("TMS报价:", 1)[1]
|
||||
self.assertIn("空运费:CNY 1833.70", tms_block)
|
||||
self.assertLess(tms_block.find("空运费:"), tms_block.find("费用合计:"))
|
||||
self.assertIn("费用合计:CNY 1833.7", tms_block)
|
||||
self.assertEqual(tms_block.count("费用合计:"), 1)
|
||||
self.assertEqual(tms_block.count("费用合计:"), 1)
|
||||
|
||||
def test_work_order_plus_lost_reason_is_deal_not_activate(self) -> None:
|
||||
"""WO@助手 未成交,原因… 必须当关单,不能只激活。"""
|
||||
from agent.routing.air_group_intent import INTENT_ACTIVATE, INTENT_LOST
|
||||
@@ -960,9 +1066,147 @@ class AirGroupCollabTests(unittest.TestCase):
|
||||
ledger=self.ledger,
|
||||
injected_intent="air_lock",
|
||||
)
|
||||
self.assertEqual(phase, "wait_lock_params")
|
||||
phase = handle_air_group(
|
||||
self._msg(
|
||||
sender="air1",
|
||||
text="AIR-OPT01 CZ 航班CZ327",
|
||||
chat="wr_air_lock_manual",
|
||||
mid="lm2",
|
||||
),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
injected_intent="air_pick_option",
|
||||
)
|
||||
self.assertEqual(phase, "lock_ok")
|
||||
self.assertTrue(any("执行成功" in t for t in self._texts()))
|
||||
|
||||
def test_lock_missing_params_asks_and_does_not_call_tms(self) -> None:
|
||||
"""缺舱位、航司、航班号时只请补,不锁舱、不报 TMS 失败、不通知 IT。"""
|
||||
wo = self._create_air(
|
||||
quote={
|
||||
"total": "CNY 1800",
|
||||
"source": "TMS",
|
||||
"source_label": "TMS 标准报价",
|
||||
"quoteId": "QT-AIR-23",
|
||||
}
|
||||
)
|
||||
calls: list[dict] = []
|
||||
orig = self.ledger.lock_cabin
|
||||
|
||||
def spy(**kwargs):
|
||||
calls.append(kwargs)
|
||||
return orig(**kwargs)
|
||||
|
||||
self.ledger.lock_cabin = spy # type: ignore
|
||||
phase = handle_air_group(
|
||||
self._msg(sender="air1", text=f"{wo}锁舱", chat="wr_air_lock_ask", mid="ask1"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
injected_intent="air_lock",
|
||||
)
|
||||
self.assertEqual(phase, "wait_lock_params")
|
||||
self.assertEqual(calls, [])
|
||||
asked = self._texts()[-1]
|
||||
self.assertEqual(
|
||||
asked,
|
||||
"锁舱参数不完整,缺少:舱位编号、航司、航班号。请补充后再锁,例如:AIR-OPT01,南航,航班CZ327。",
|
||||
)
|
||||
self.assertNotIn("已通知", asked)
|
||||
self.assertNotIn("锁舱失败", asked)
|
||||
phase = handle_air_group(
|
||||
self._msg(
|
||||
sender="air1",
|
||||
text="AIR-OPT01 CZ 航班CZ327",
|
||||
chat="wr_air_lock_ask",
|
||||
mid="ask2",
|
||||
),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
injected_intent="air_pick_option",
|
||||
)
|
||||
self.assertEqual(phase, "lock_ok")
|
||||
self.assertEqual(len(calls), 1)
|
||||
self.assertEqual(calls[0].get("option_no"), "AIR-OPT01")
|
||||
self.assertEqual(calls[0].get("airline"), "CZ")
|
||||
self.assertEqual(calls[0].get("flight_no"), "CZ327")
|
||||
|
||||
def test_lock_accepts_free_wording_not_only_sample_format(self) -> None:
|
||||
"""补舱位不限格式。点名舱位、航司、航班号,换顺序、用航司中文名,也要锁。"""
|
||||
wo = self._create_air(
|
||||
quote={
|
||||
"total": "CNY 1800",
|
||||
"source": "TMS",
|
||||
"source_label": "TMS 标准报价",
|
||||
"quoteId": "QT-AIR-24",
|
||||
}
|
||||
)
|
||||
calls: list[dict] = []
|
||||
orig = self.ledger.lock_cabin
|
||||
|
||||
def spy(**kwargs):
|
||||
calls.append(kwargs)
|
||||
return orig(**kwargs)
|
||||
|
||||
self.ledger.lock_cabin = spy # type: ignore
|
||||
phase = handle_air_group(
|
||||
self._msg(sender="air1", text=f"{wo}锁舱", chat="wr_air_lock_free", mid="free1"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
injected_intent="air_lock",
|
||||
)
|
||||
self.assertEqual(phase, "wait_lock_params")
|
||||
self.assertEqual(calls, [])
|
||||
phase = handle_air_group(
|
||||
self._msg(
|
||||
sender="air1",
|
||||
text="航班号:CZ327,航司南航,舱位编号是AIR-OPT01",
|
||||
chat="wr_air_lock_free",
|
||||
mid="free2",
|
||||
),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
)
|
||||
self.assertEqual(phase, "lock_ok")
|
||||
self.assertEqual(calls[0].get("option_no"), "AIR-OPT01")
|
||||
self.assertEqual(calls[0].get("airline"), "CZ")
|
||||
self.assertEqual(calls[0].get("flight_no"), "CZ327")
|
||||
|
||||
def test_lock_sentence_with_labeled_params_locks_immediately(self) -> None:
|
||||
"""同一句里已经说齐三项,直接锁,不再要求按示例格式重说。"""
|
||||
wo = self._create_air(
|
||||
quote={
|
||||
"total": "CNY 1800",
|
||||
"source": "manual",
|
||||
"source_label": "产品报价",
|
||||
}
|
||||
)
|
||||
calls: list[dict] = []
|
||||
orig = self.ledger.lock_cabin
|
||||
|
||||
def spy(**kwargs):
|
||||
calls.append(kwargs)
|
||||
return orig(**kwargs)
|
||||
|
||||
self.ledger.lock_cabin = spy # type: ignore
|
||||
phase = handle_air_group(
|
||||
self._msg(
|
||||
sender="air1",
|
||||
text=f"{wo}锁舱,舱位 AIR-OPT02,东方航空,航班号 MU5123",
|
||||
chat="wr_air_lock_once",
|
||||
mid="once1",
|
||||
),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
injected_intent="air_lock",
|
||||
)
|
||||
self.assertEqual(phase, "lock_ok")
|
||||
self.assertEqual(len(calls), 1)
|
||||
self.assertEqual(calls[0].get("option_no"), "AIR-OPT02")
|
||||
self.assertEqual(calls[0].get("airline"), "MU")
|
||||
self.assertEqual(calls[0].get("flight_no"), "MU5123")
|
||||
self.assertFalse(any("锁舱参数不完整" in t for t in self._texts()))
|
||||
|
||||
def test_lock_success_and_release_after_deal_blocked(self) -> None:
|
||||
wo = self._create_air()
|
||||
phase = handle_air_group(
|
||||
@@ -1049,8 +1293,8 @@ class AirGroupCollabTests(unittest.TestCase):
|
||||
"source": "TMS",
|
||||
"quoteId": "QT-AIR-2",
|
||||
"airOptions": [
|
||||
{"optionNo": "AIR-OPT-01", "airlineCode": "CZ"},
|
||||
{"optionNo": "AIR-OPT-02", "airlineCode": "MU"},
|
||||
{"optionNo": "AIR-OPT-01", "airlineCode": "CZ", "flightNo": "CZ327"},
|
||||
{"optionNo": "AIR-OPT-02", "airlineCode": "MU", "flightNo": "MU5123"},
|
||||
],
|
||||
}
|
||||
)
|
||||
@@ -1060,7 +1304,7 @@ class AirGroupCollabTests(unittest.TestCase):
|
||||
ledger=self.ledger,
|
||||
injected_intent="air_lock",
|
||||
)
|
||||
self.assertEqual(phase, "wait_lock_option")
|
||||
self.assertEqual(phase, "wait_lock_params")
|
||||
phase = handle_air_group(
|
||||
self._msg(sender="air1", text="AIR-OPT-02", chat="wr_air_1", mid="m2"),
|
||||
flow=self.flow,
|
||||
@@ -1155,6 +1399,31 @@ class AirGroupCollabTests(unittest.TestCase):
|
||||
self.assertIn("请确认成交状态", texts)
|
||||
self.assertFalse(any(x.get("kind") == "file" for x in added))
|
||||
|
||||
def test_reactivate_quoted_tms_file_is_deal_not_sheet(self) -> None:
|
||||
"""TMS 价的报价单已经发出、停在成交跟进:再发工单号只回成交跟进。"""
|
||||
wo = self._create_air(
|
||||
quote={
|
||||
"total": "CNY 3500.70",
|
||||
"source": "TMS",
|
||||
"source_label": "TMS 标准报价",
|
||||
"quoteId": "QT-AIR-FILE",
|
||||
"quoteFileIssued": True,
|
||||
"airOptions": [
|
||||
{"optionNo": "AIR-OPT-01", "airlineCode": "CZ", "flightNo": "CZ327"}
|
||||
],
|
||||
}
|
||||
)
|
||||
phase = handle_air_group(
|
||||
self._msg(sender="sales1", text=wo, chat="wr_air_1", mid="react-deal"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
)
|
||||
self.assertEqual(phase, "activated_deal")
|
||||
texts = "\n".join(self._texts())
|
||||
self.assertIn("请确认成交状态", texts)
|
||||
self.assertNotIn("询价字段:", texts)
|
||||
self.assertFalse(any(x.get("kind") == "file" for x in self.groups.sent))
|
||||
|
||||
def test_image_rejected(self) -> None:
|
||||
wo = self._create_air()
|
||||
handle_air_group(
|
||||
@@ -1385,6 +1654,38 @@ class AirGroupCollabTests(unittest.TestCase):
|
||||
self.assertNotIn("报价选项不存在", joined)
|
||||
self.assertNotIn("与工单不匹配", joined)
|
||||
|
||||
def test_lock_fail_does_not_block_airline_quote(self) -> None:
|
||||
"""锁舱失败后,航线再报手工价必须收下,不能回 TMS 接口异常。"""
|
||||
wo = self._create_air()
|
||||
handle_air_group(
|
||||
self._msg(sender="air1", text=wo, chat="wr_air_lock_quote", mid="lq0"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
)
|
||||
self.ledger._cabin_lock_result = {
|
||||
"ok": False,
|
||||
"error": "lock_fail",
|
||||
"failReason": "该航班日期不存在可锁定的配载记录",
|
||||
}
|
||||
phase = handle_air_group(
|
||||
self._msg(sender="air1", text=f"{wo}锁舱", chat="wr_air_lock_quote", mid="lq1"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
injected_intent="air_lock",
|
||||
)
|
||||
self.assertEqual(phase, "system_exception")
|
||||
self.groups.sent.clear()
|
||||
phase = handle_air_group(
|
||||
self._msg(sender="air1", text="空运费改为900USD", chat="wr_air_lock_quote", mid="lq2"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
injected_intent="product_quote",
|
||||
)
|
||||
self.assertNotEqual(phase, "system_exception_paused")
|
||||
joined = "\n".join(self._texts())
|
||||
self.assertNotIn("TMS接口异常", joined)
|
||||
self.assertIn(phase, {"wait_tms_choice", "quoted"})
|
||||
|
||||
def test_lock_without_quote_still_shows_tms_reason(self) -> None:
|
||||
"""TMS 拒绝锁舱时原样回失败原因,不要改写成选项不匹配。"""
|
||||
wo = self._create_air(quote={}, status="询价中")
|
||||
|
||||
Reference in New Issue
Block a user