from __future__ import annotations import argparse import json import sys import tempfile import unittest from datetime import date from decimal import Decimal from pathlib import Path from openpyxl import Workbook, load_workbook PROJECT_ROOT = Path(__file__).resolve().parents[1] SKILL_ROOT = PROJECT_ROOT / "opera-daily-channel-report" SCRIPTS = SKILL_ROOT / "scripts" PRICE_REFERENCE = SKILL_ROOT / "references" / "价格对照.xlsx" sys.path.insert(0, str(SCRIPTS)) import process_reports as core # noqa: E402 def pricing_record(company: str, rate_code: str, amount: str = "999"): return { "_SOURCE_INDEX": 1, "COMPANY_NAME": company, "RATE_CODE": rate_code, "EFFECTIVE_RATE_AMOUNT": Decimal(amount), "CONFIRMATION_NO": "TEST-CONFIRMATION", "NO_OF_ROOMS": 1, "NIGHTS": 1, "REAL PRICE": None, "TOTAL PRICE": None, } class PricingRuleTests(unittest.TestCase): def test_deterministic_keyword_company_matching(self): cases = { "T- LIAN TAI TRAVEL (T": "LIAN TAI", "T / Q.B.D. TRAVEL GROUP": "QBD", "T- Rainbow Holiday Service": "RAINBOW/AI", "T- FENG RUN TRAVEL": "FENGRUN", "T- HANATOUR TD CO., LTD": "HANA TOUR", "T / HANA TRAVEL": "HANA TOUR", "T- HONG TAI TRAVEL": "HONGTAI", "T / Guangzhou Go-Easy": "GUANGZHOU GO EASY", "Hong Yun": "HONGYUN", } for raw, expected in cases.items(): with self.subTest(raw=raw): self.assertEqual(core.price_company_key(raw), expected) def test_multiple_keyword_groups_fail_instead_of_guessing(self): with self.assertRaises(core.CompanyKeywordAmbiguity) as caught: core.price_company_key("HANA HONGTAI") self.assertEqual(caught.exception.matches, ("HANA TOUR", "HONGTAI")) record = pricing_record("HANA HONGTAI", "LBMS") with self.assertRaises(core.ProcessingFailure) as caught_price: core.apply_prices([record], {}) self.assertEqual(caught_price.exception.errors[0].code, "PRICE_COMPANY_AMBIGUOUS") with self.assertRaises(core.ProcessingFailure) as caught_route: core.route_standard("HANA HONGTAI", "LBMS") self.assertEqual(caught_route.exception.errors[0].code, "MONTHLY_COMPANY_AMBIGUOUS") def test_zero_price_requires_both_company_and_rate_conditions(self): records = [ pricing_record("T / Guangzhou Go-Easy", "LBSM", "12345"), pricing_record("T- Rainbow Holiday Service", "lbms", "67890"), ] core.apply_prices(records, {}) self.assertEqual([record["REAL PRICE"] for record in records], [Decimal(0), Decimal(0)]) self.assertEqual([record["TOTAL PRICE"] for record in records], [Decimal(0), Decimal(0)]) for company, rate_code in ( ("T- Guangzhou Go Easy", "GRPA1"), ("T- Rainbow Holiday Se", "LBW1"), ("T- HANATOUR TD CO., L", "LBMS"), ): with self.subTest(company=company, rate_code=rate_code): with self.assertRaises(core.ProcessingFailure) as caught: core.apply_prices([pricing_record(company, rate_code)], {}) self.assertEqual(caught.exception.errors[0].code, "PRICE_UNMATCHED") def test_latest_price_reference_is_normalized_on_load(self): price_map = core.load_price_map(PRICE_REFERENCE.resolve()) self.assertEqual(len(price_map), 73) rainbow_rules = [ total for (company, _rate, _amount), total in price_map.items() if company == "RAINBOW/AI" ] self.assertEqual(len(rainbow_rules), 9) self.assertTrue(all(total == Decimal(0) for total in rainbow_rules)) self.assertEqual(price_map[("RAINBOW/AI", "LBSM", Decimal("900"))], Decimal(0)) self.assertEqual(price_map[("HANA TOUR", "WHKR2100B", Decimal("2100"))], Decimal("2100")) self.assertEqual(price_map[("HONGTAI", "LBKB", Decimal("900"))], Decimal("1800")) self.assertEqual(price_map[("FENGRUN", "GLSPCB", Decimal("1800"))], Decimal("1800")) def test_duplicate_detection_uses_normalized_company_identity(self): with tempfile.TemporaryDirectory() as temp_dir: price_path = Path(temp_dir) / "duplicate.xlsx" workbook = Workbook() sheet = workbook.active sheet.title = "Sheet1" sheet.append(["COMPANY'S NAME", "RATE CODE", "Opera展示的价格", "总价"]) sheet.append(["T- HANATOUR TD CO., L", "WHKR2100B", 2100, 2100]) sheet.append(["HANA TOUR", "WHKR2100B", 2100, 2100]) workbook.save(price_path) workbook.close() with self.assertRaises(core.ProcessingFailure) as caught: core.load_price_map(price_path.resolve()) self.assertEqual(caught.exception.errors[0].code, "PRICE_DUPLICATE_KEY") def test_standard_route_uses_same_fuzzy_identity(self): cases = { ("T / Q.B.D. TRAVEL GROUP", "GRPA1"): "QBD", ("T- LIAN TAI TRAVEL", "LTLT"): "LIANTAI-FIT", ("T- LIAN TAI TRAVEL", "GRPA1"): "LIANTAI-GROUP", ("T- Rainbow Holiday Service", "LBW1"): "DY-AI-Easy-KB", ("T- FENG RUN TRAVEL", "GLSPCB"): "FENGRUN", ("T / Guangzhou Go-Easy", "GRPA1"): "DY-AI-Easy-KB", } for arguments, expected in cases.items(): with self.subTest(arguments=arguments): self.assertEqual(core.route_standard(*arguments), expected) def test_legacy_monthly_header_contract_is_rejected(self): template = SKILL_ROOT / "assets" / "channel-report-template.xlsx" workbook = load_workbook(template, data_only=False) try: workbook[core.KB_SHEET].cell(1, 20).value = None with self.assertRaises(core.ProcessingFailure) as caught: core.read_existing_monthly(workbook, date(2026, 7, 27)) self.assertIn( "MONTHLY_HEADER_MISMATCH", {error.code for error in caught.exception.errors}, ) finally: workbook.close() def test_existing_monthly_derived_values_are_revalidated(self): template = SKILL_ROOT / "assets" / "channel-report-template.xlsx" workbook = load_workbook(template, data_only=False) try: sheet = workbook[core.KB_SHEET] values = { "ARRIVAL": date(2026, 7, 26), "DEPARTURE": date(2026, 7, 27), "NIGHTS": 1, "ADULTS": 1, "CHILDREN": 0, "BLOCK_CODE": "", "NO_OF_ROOMS": 2, "COMPANY_NAME": "T- Rainbow Holiday Service", "CONFIRMATION_NO": "TEST-EXISTING", "DISP_ROOM_NO": "TEST-ROOM-EXISTING", "RATE_AMOUNT": 900, "FULL_NAME": "TEST GUEST EXISTING", "RES_COMMENT": "", "TRACE_TEXT": "", "PRODUCTS": "", "RATE_CODE": "LBMS", "ROOM_CATEGORY_LABEL": "TEST", "REAL PRICE": 0, "TOTAL PRICE": 1, core.KB_HEADER: 100, } for column, header in enumerate(core.KB_CHANNEL_HEADERS, 1): sheet.cell(2, column).value = values[header] with self.assertRaises(core.ProcessingFailure) as caught: core.read_existing_monthly(workbook, date(2026, 7, 27)) codes = {error.code for error in caught.exception.errors} self.assertIn("MONTHLY_TOTAL_PRICE_INVALID", codes) self.assertIn("MONTHLY_KB_INVALID", codes) finally: workbook.close() def test_end_to_end_daily_and_monthly_zero_prices(self): xml = """ 20260727 27-07-26 20 T / Guangzhou Go-Easy TEST-GOEASYTEST-ROOM-1 12345TEST GUEST ONE 1LBSM TEST 2026-07-272026-07-28 11 T- Rainbow Holiday Service TEST-RAINBOWTEST-ROOM-2 67890TEST GUEST TWO 1LBMS TEST 2026-07-272026-07-29 2TEST-BLOCK0 T / Q.B.D. TRAVEL GROUP TEST-QBDTEST-ROOM-3 900TEST GUEST THREE 2GRPA1 TEST 2026-07-272026-07-30 """ with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) xml_path = temp_path / "synthetic.xml" output_dir = temp_path / "output" result_path = output_dir / "result.json" xml_path.write_text(xml, encoding="utf-8") exit_code = core.process( argparse.Namespace( xml=str(xml_path.resolve()), monthly_base=None, output_dir=str(output_dir.resolve()), result_json=str(result_path.resolve()), ) ) self.assertEqual(exit_code, 0) result = json.loads(result_path.read_text(encoding="utf-8")) self.assertEqual(result["status"], "success") daily_path = output_dir / result["outputs"]["daily_report"] monthly_path = output_dir / result["outputs"]["monthly_report"] daily = load_workbook(daily_path, data_only=False) try: confirmation_column = core.DAILY_HEADERS.index("CONFIRMATION_NO") + 1 real_price_column = core.DAILY_HEADERS.index("REAL PRICE") + 1 total_column = core.DAILY_HEADERS.index("TOTAL PRICE") + 1 prices = { daily.active.cell(row, confirmation_column).value: ( daily.active.cell(row, real_price_column).value, daily.active.cell(row, total_column).value, ) for row in range(2, daily.active.max_row + 1) if daily.active.cell(row, confirmation_column).value } self.assertEqual( prices, { "TEST-GOEASY": (0, 0), "TEST-RAINBOW": (0, 0), "TEST-QBD": (1800, 10800), }, ) for row in range(2, daily.active.max_row + 1): if daily.active.cell(row, confirmation_column).value: for column in (real_price_column, total_column): self.assertNotEqual(daily.active.cell(row, column).data_type, "f") finally: daily.close() monthly = load_workbook(monthly_path, data_only=False) try: self.assertIn(core.KB_SHEET, monthly.sheetnames) target = monthly[core.KB_SHEET] companies = [ target.cell(row, 8).value for row in range(2, target.max_row + 1) if target.cell(row, 8).value ] self.assertCountEqual( companies, ["T / Guangzhou Go-Easy", "T- Rainbow Holiday Service"], ) target_values = { target.cell(row, 9).value: ( target.cell(row, 18).value, target.cell(row, 19).value, target.cell(row, 20).value, ) for row in range(2, target.max_row + 1) if target.cell(row, 9).value } self.assertEqual( target_values, { "TEST-GOEASY": (0, 0, 100), "TEST-RAINBOW": (0, 0, 100), }, ) qbd = monthly["QBD"] self.assertEqual(qbd.cell(2, 9).value, "TEST-QBD") self.assertEqual(qbd.cell(2, 18).value, 1800) self.assertEqual(qbd.cell(2, 19).value, 10800) for cell in (qbd.cell(2, 18), qbd.cell(2, 19)): self.assertNotEqual(cell.data_type, "f") for row in range(2, target.max_row + 1): for column in (18, 19, 20): self.assertNotEqual(target.cell(row, column).data_type, "f") finally: monthly.close() if __name__ == "__main__": unittest.main(verbosity=2)