Files
inquiry_robot/inquiry-agent/tests/test_deal_followup.py
T

231 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
成交跟进:工作日顺延、线路对负责人、未成交收下原因(不打外网)。
"""
from __future__ import annotations
import unittest
from datetime import date, datetime
from zoneinfo import ZoneInfo
from agent.policy.deal_followup import (
SHANGHAI,
add_workday_hours,
air_route_keys,
air_staff_route_matches,
is_workday,
leader_matches,
next_followup_at,
shift_workdays,
workday_elapsed_hours,
)
class TestWorkdays(unittest.TestCase):
def test_skip_weekend(self) -> None:
# 普通周末仍跳过。2026-11-13 周五 → 16、17、18 三个工作日,第四个是 19 日。
self.assertEqual(shift_workdays(date(2026, 11, 13), 3), date(2026, 11, 18))
self.assertEqual(shift_workdays(date(2026, 11, 13), 4), date(2026, 11, 19))
def test_sept20_makeup_pulls_followup_forward(self) -> None:
# 2026-09-18 周五。9 月 20 日周日调休上班,第四个工作日是 23 日,不是 24 日。
self.assertEqual(shift_workdays(date(2026, 9, 18), 4), date(2026, 9, 23))
def test_skip_national_day(self) -> None:
# 2026-09-30 周三。10 月 1–7 日放假,8 日上班,10 日周六调休。
# 节后第 1 个工作日 8 日,第 4 个是 12 日周一。
fourth = shift_workdays(date(2026, 9, 30), 4)
self.assertEqual(fourth, date(2026, 10, 12))
def test_oct8_works_and_sept20_is_makeup(self) -> None:
# 国办发明电〔2025〕7号:10 月 8 日正常上班,9 月 20 日周日调休上班。
self.assertTrue(is_workday(date(2026, 10, 8)))
self.assertTrue(is_workday(date(2026, 9, 20)))
self.assertFalse(is_workday(date(2026, 10, 7)))
def test_next_followup_48_work_hours_skips_weekend(self) -> None:
# 周五 15:30 + 48 工作小时 → 跳过周末 → 周二 15:30
when = next_followup_at(
datetime(2026, 11, 13, 15, 30, tzinfo=SHANGHAI), hours=48
)
self.assertEqual(when.tzinfo, SHANGHAI)
self.assertEqual(when, datetime(2026, 11, 17, 15, 30, tzinfo=SHANGHAI))
def test_add_workday_hours_from_rest_day(self) -> None:
# 周六起点:从下周一 00:00 起算 10 小时 → 周一 10:00
when = add_workday_hours(datetime(2026, 11, 14, 10, 0, tzinfo=SHANGHAI), 10)
self.assertEqual(when, datetime(2026, 11, 16, 10, 0, tzinfo=SHANGHAI))
def test_skip_weekend_keep_weekday_night(self) -> None:
start = datetime(2026, 11, 13, 17, 0, tzinfo=SHANGHAI) # 周五
now = datetime(2026, 11, 16, 17, 0, tzinfo=SHANGHAI) # 周一
# 周五 17–24 = 7h,周末不算,周一 0–17 = 17h
self.assertAlmostEqual(workday_elapsed_hours(start, now), 24.0, places=4)
def test_rest_day_start_counts_from_next_workday(self) -> None:
start = datetime(2026, 11, 14, 10, 0, tzinfo=SHANGHAI) # 周六
now = datetime(2026, 11, 16, 10, 0, tzinfo=SHANGHAI) # 周一
self.assertAlmostEqual(workday_elapsed_hours(start, now), 10.0, places=4)
def test_fractional_quarter_hour(self) -> None:
start = datetime(2026, 9, 18, 9, 0, tzinfo=SHANGHAI)
now = datetime(2026, 9, 18, 9, 15, tzinfo=SHANGHAI)
self.assertAlmostEqual(workday_elapsed_hours(start, now), 0.25, places=4)
class TestAirRouteKeys(unittest.TestCase):
def test_pair_and_dest(self) -> None:
keys = air_route_keys({"起运港": "SZX", "目的港": "KUL"})
self.assertIn("SZX-KUL", keys)
self.assertIn("KUL", keys)
def test_chinese_city_projects_iata(self) -> None:
keys = air_route_keys({"起运港": "深圳", "目的港": "吉隆坡"})
self.assertIn("SZX-KUL", keys)
self.assertIn("KUL", keys)
class TestLeaderMatch(unittest.TestCase):
def test_sea_all_line(self) -> None:
self.assertTrue(
leader_matches(
role_code="sea",
routes="海运全线",
is_leader=True,
business_line="SEA",
facts={},
)
)
self.assertFalse(
leader_matches(
role_code="sea",
routes="海运全线",
is_leader=False,
business_line="SEA",
facts={},
)
)
def test_land_route_category(self) -> None:
self.assertTrue(
leader_matches(
role_code="land",
routes="国内长途/零担,东南亚",
is_leader=True,
business_line="LAND",
facts={"线路类别": "国内长途/零担"},
)
)
self.assertFalse(
leader_matches(
role_code="land",
routes="东南亚",
is_leader=True,
business_line="LAND",
facts={"线路类别": "国内长途/零担"},
)
)
def test_air_pair_dest_and_all(self) -> None:
facts = {"起运港": "SZX", "目的港": "KUL"}
self.assertTrue(
leader_matches(
role_code="air",
routes="SZX-KUL",
is_leader=True,
business_line="AIR",
facts=facts,
)
)
self.assertTrue(
leader_matches(
role_code="air",
routes="KUL",
is_leader=True,
business_line="AIR",
facts=facts,
)
)
self.assertTrue(
leader_matches(
role_code="air",
routes="全线",
is_leader=True,
business_line="AIR",
facts=facts,
)
)
# 后台现网选项是「空运全线」,不能只认旧词「全线」
self.assertTrue(
leader_matches(
role_code="air",
routes="空运全线",
is_leader=True,
business_line="AIR",
facts=facts,
)
)
self.assertTrue(
leader_matches(
role_code="land",
routes="陆运全线",
is_leader=True,
business_line="LAND",
facts={"线路类别": "东南亚"},
)
)
self.assertFalse(
leader_matches(
role_code="air",
routes="外采-空运",
is_leader=True,
business_line="AIR",
facts=facts,
)
)
self.assertTrue(
leader_matches(
role_code="air",
routes="外采-空运",
is_leader=True,
business_line="AIR",
facts={**facts, "外采": "1"},
)
)
class TestAirStaffRouteMatches(unittest.TestCase):
"""群 @ 航线:按负责线路过滤,不要求 isLeader。"""
def test_ehu_ord_pair_and_all_line(self) -> None:
facts = {"起运港": "EHU", "目的港": "ORD"}
self.assertTrue(air_staff_route_matches(routes="EHU-ORD", facts=facts))
self.assertTrue(air_staff_route_matches(routes="空运全线", facts=facts))
self.assertFalse(air_staff_route_matches(routes="CGO-KUL,SZX-KUL", facts=facts))
self.assertFalse(air_staff_route_matches(routes="HKG-KUL,NKG-KUL", facts=facts))
def test_dest_only_matches_pair_suffix(self) -> None:
facts = {"目的港": "KUL"}
self.assertTrue(air_staff_route_matches(routes="CGO-KUL,SZX-KUL", facts=facts))
self.assertTrue(air_staff_route_matches(routes="HKG-KUL,NKG-KUL", facts=facts))
self.assertFalse(air_staff_route_matches(routes="EHU-ORD", facts=facts))
self.assertFalse(air_staff_route_matches(routes="CGO-BLR,CGO-MAA", facts=facts))
def test_non_leader_still_matches(self) -> None:
# 群 @ 与成交推负责人不同:非负责人只要线路对也要 @
facts = {"起运港": "EHU", "目的港": "ORD"}
self.assertTrue(air_staff_route_matches(routes="EHU-ORD", facts=facts))
self.assertFalse(
leader_matches(
role_code="air",
routes="EHU-ORD",
is_leader=False,
business_line="AIR",
facts=facts,
)
)
if __name__ == "__main__":
unittest.main()