""" 空运线路报价:一条直接展示,多条要选;航线带中转;费用取该线路回包。 """ 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_group_air_quote, 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_group_air_quote_projects_route_fees_and_drops_total_alias(self) -> None: """群摘要投影:该线路分项留下,空运费=总价丢掉。""" quote = { "total": "183370.0", "freight": "183370.0", "currency": "CNY", "fee_lines": [ {"name": "空运费", "amount": "183370.00", "currency": "CNY"}, ], "airOptions": [ { "originAirportCode": "ZUH", "transferAirportCode": "CHG", "destinationAirportCode": "CRK", "salePrice": 183370, "currency": "CNY", "departureCharges": [ {"chargeName": "运输费", "currency": "CNY", "unitPrice": 11.0}, {"chargeName": "提送货费", "currency": "CNY", "unitPrice": 6.0}, {"chargeName": "人工费", "currency": "CNY", "unitPrice": 123.0}, ], } ], } projected = project_group_air_quote(quote, {"originCode": "ZUH"}) names = [str(x.get("name") or "") for x in (projected.get("fee_lines") or [])] self.assertEqual(names, ["运输费", "提送货费", "人工费"]) self.assertNotIn("空运费", names) self.assertEqual(copy.quote_fee_lines(projected)[0][0], "运输费") def test_air_detail_text_hides_eta_keeps_valid_until(self) -> None: """空运没有时效;价格明细只留报价有效期。""" text = copy.sea_quote_detail_text( work_order_no="WO202609220013", quote={ "source_label": "TMS标准报价", "selected_air_option": { "originAirportCode": "NKG", "destinationAirportCode": "KUL", }, "fee_lines": [ {"name": "增点费", "amount": "100.0", "currency": "CNY"}, {"name": "拆板费", "amount": "100.0", "currency": "USD"}, ], "total": "5001.0", "currency": "CNY", "eta": "3天", "valid_until": "2026-09-30", }, business_line="AIR", ) self.assertEqual( text, "\n".join( [ "TMS标准报价", "WO202609220013价格明细", "NKG-KUL", "增点费:CNY 100.0", "拆板费:USD 100.0", "总销售价:CNY 5001.0", "报价有效期:2026-09-30", ] ), ) self.assertNotIn("时效", text) 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()