Files
inquiry_robot/inquiry-agent/tests/test_air_route_options.py
T
jillion886andCursor cfbce2fd69 落地空运单聊询价字段与线路选择。
必填核对后再建单;多条线路走 H5 点选;费用只展示该线路 TMS 回包,不套用整单空运费。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 15:02:32 +08:00

261 lines
10 KiB
Python

"""
空运线路报价:一条直接展示,多条要选;航线带中转;费用取该线路回包。
"""
from __future__ import annotations
import json
import os
import sys
import unittest
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if ROOT not in sys.path:
sys.path.insert(0, ROOT)
from agent.ledger.memory_ledger import MemoryLedger
from agent.policy import inquiry_copy as copy
from agent.policy.air_text_flow import AirTextInquiryFlow
from agent.channel.h5.air_route_page import render_air_route_page
from agent.schema.air_route_options import (
apply_selected_option,
needs_route_pick,
option_fee_lines,
parse_air_options,
project_selected_quote,
route_codes,
route_line,
)
class AirRouteOptionsTests(unittest.TestCase):
def test_single_with_transfer_line(self) -> None:
quote = {
"airOptions": [
{
"originAirportCode": "CAN",
"transferAirportCode": "SHA",
"destinationAirportCode": "LAX",
"salePrice": 110,
"currency": "CNY",
"departureCharges": [
{"chargeName": "运输费", "currency": "CNY", "unitPrice": 110}
],
}
]
}
opts = parse_air_options(quote)
self.assertEqual(len(opts), 1)
self.assertFalse(needs_route_pick(quote))
self.assertEqual(route_line(opts[0], {}), "广州-上海-洛杉矶")
self.assertEqual(route_codes(opts[0], {}), "CAN-SHA-LAX")
fees = option_fee_lines(opts[0], quote)
self.assertEqual(fees[0][0], "运输费")
self.assertIn("110", fees[0][1])
def test_multi_needs_pick(self) -> None:
quote = {
"isMulti": "2",
"airOptions": [
{"originAirportCode": "ZUH", "transferAirportCode": "COC", "destinationAirportCode": "CRK", "salePrice": 183370},
{"originAirportCode": "ZUH", "transferAirportCode": "ABC", "destinationAirportCode": "CRK", "salePrice": 193370},
],
}
self.assertTrue(needs_route_pick(quote))
self.assertEqual(len(parse_air_options(quote)), 2)
def test_select_writes_origin(self) -> None:
option = {"originAirportCode": "SZX", "destinationAirportCode": "KUL"}
facts = apply_selected_option({"目的港": "吉隆坡"}, option)
self.assertEqual(facts["起运港"], "深圳")
self.assertEqual(facts["originCode"], "SZX")
def test_multi_quote_opens_h5_then_select_writes_origin(self) -> None:
flow = AirTextInquiryFlow(MemoryLedger())
replies: list[str] = []
extras: list[dict] = []
def reply(text: str, extra=None) -> None:
replies.append(text)
extras.append(dict(extra or {}))
facts = {
"目的港": "克拉克",
"品名": "茶叶",
"件数": "10",
"毛重": "100",
"体积": "2",
"包装方式": "托盘",
"tms_multi": "1",
}
phase = flow.on_text(
sender_id="u_multi",
text="空运",
reply=reply,
injected_facts=facts,
injected_mode="AIR",
)
self.assertEqual(phase, "wait_confirm")
phase = flow.on_text(sender_id="u_multi", text="确定", reply=reply)
self.assertEqual(phase, "wait_air_route")
self.assertTrue(any("已查到" in x and "条线路" in x for x in replies))
phase = flow.select_air_route(sender_id="u_multi", index=1, reply=reply)
self.assertEqual(phase, "wait_collab")
sess = flow.session_of("u_multi")
self.assertEqual(sess.facts.get("起运港"), "深圳")
self.assertIn("SZX-CRK", "\n".join(replies[-2:]))
def test_reselect_after_adopt_voids_old_file(self) -> None:
"""采用后再改选:旧报价单作废,回到方案卡重新采用。"""
ledger = MemoryLedger()
flow = AirTextInquiryFlow(ledger)
replies: list[str] = []
def reply(text: str, extra=None) -> None:
replies.append(text)
_ = extra
facts = {
"目的港": "克拉克",
"品名": "茶叶",
"件数": "10",
"毛重": "100",
"体积": "2",
"包装方式": "托盘",
"tms_multi": "1",
}
phase = flow.on_text(
sender_id="u_void",
text="空运",
reply=reply,
injected_facts=facts,
injected_mode="AIR",
)
self.assertEqual(phase, "wait_confirm")
self.assertEqual(
flow.on_text(sender_id="u_void", text="确定", reply=reply),
"wait_air_route",
)
self.assertEqual(flow.select_air_route(sender_id="u_void", index=0, reply=reply), "wait_collab")
self.assertEqual(
flow.on_button(sender_id="u_void", action=copy.BTN_SKIP_COLLAB, reply=reply),
"wait_adopt",
)
self.assertEqual(
flow.on_button(sender_id="u_void", action=copy.BTN_ADOPT_EXCEL, reply=reply),
"wait_deal",
)
wo = flow.session_of("u_void").work_order_no
self.assertTrue(ledger.get_ticket(work_order_no=wo).quote_file_ready)
phase = flow.select_air_route(sender_id="u_void", index=1, reply=reply)
self.assertEqual(phase, "wait_adopt")
self.assertFalse(ledger.get_ticket(work_order_no=wo).quote_file_ready)
view = ledger.get_for_agent(work_order_no=wo, sender_id="u_void")
self.assertEqual(view.get("wait_phase"), "wait_adopt")
self.assertEqual(flow.session_of("u_void").facts.get("起运港"), "深圳")
self.assertTrue(any("报价方案" in x or "生成Excel" in x for x in replies[-3:]))
def test_empty_route_fees_do_not_borrow_other_quote_lines(self) -> None:
"""TMS 该线路费用列表为空时,禁止套整单或其他线路的空运费。"""
quote = {
"total": "183370.00",
"freight": "183370.00",
"currency": "CNY",
"fee_lines": [
{
"name": "空运费",
"amount": "183370.00",
"currency": "CNY",
"unit_price": "11.00",
}
],
"airOptions": [
{
"originAirportCode": "ZUH",
"transferAirportCode": "CHG",
"destinationAirportCode": "CRK",
"salePrice": 183370,
"currency": "CNY",
"departureCharges": [
{"chargeName": "运输费", "currency": "CNY", "unitPrice": 11},
{"chargeName": "提送货费", "currency": "CNY", "unitPrice": 6},
{"chargeName": "人工费", "currency": "CNY", "unitPrice": 123},
],
},
{
"originAirportCode": "SZX",
"transferAirportCode": "",
"destinationAirportCode": "CRK",
"salePrice": 200040,
"currency": "CNY",
"departureCharges": [],
},
],
}
opts = parse_air_options(quote)
zuh = option_fee_lines(opts[0], quote)
self.assertEqual([name for name, _ in zuh], ["运输费", "提送货费", "人工费"])
szx = option_fee_lines(opts[1], quote)
self.assertEqual(szx, [])
html = render_air_route_page(
{"work_order_no": "WO202609220009", "facts": {"目的港": "克拉克"}, "quote": quote}
)
self.assertIn("运输费", html)
self.assertIn("深圳-克拉克", html)
self.assertNotIn("空运费", html)
self.assertIn("200040", html)
self.assertIn("本线路未返回费用明细", html)
self.assertIn("直达", html)
self.assertIn("中转", html)
projected = project_selected_quote(quote, opts[1])
self.assertEqual(projected.get("fee_lines"), [])
self.assertIn("200040", str(projected.get("total") or ""))
self.assertEqual(copy.quote_fee_lines(projected), [])
detail = copy.sea_quote_detail_text(
work_order_no="WO202609220009",
quote=projected,
business_line="AIR",
)
self.assertNotIn("空运费", detail)
self.assertIn("总销售价", detail)
self.assertIn("200040", detail)
def test_h5_page_lists_route_and_fees(self) -> None:
from agent.channel.h5.air_route_page import render_air_route_page
from agent.channel.h5.air_route_store import build_air_route_url
html = render_air_route_page(
{
"work_order_no": "WO202609220001",
"facts": {"目的港": "克拉克"},
"quote": {
"airOptions": [
{
"originAirportCode": "ZUH",
"transferAirportCode": "CAN",
"destinationAirportCode": "CRK",
"salePrice": 183370,
"currency": "CNY",
"departureCharges": [
{"chargeName": "运输费", "currency": "CNY", "unitPrice": 180000}
],
}
]
},
}
)
self.assertIn("珠海-广州-克拉克", html)
self.assertIn("运输费", html)
self.assertIn("总销售价", html)
self.assertIn("class='opt'", html)
self.assertIn("name='index'", html)
self.assertNotIn("userid", html.lower())
self.assertNotIn("sender_id", html)
url = build_air_route_url("tok-abc")
self.assertIn("/air-routes/tok-abc", url)
self.assertNotIn("userid", url)
if __name__ == "__main__":
unittest.main()