feat: prepare ARR for controlled public deployment

This commit is contained in:
Wyndham ARR
2026-07-29 16:38:05 +08:00
commit a701de9f0e
271 changed files with 48472 additions and 0 deletions

View File

@@ -0,0 +1,342 @@
from __future__ import annotations
import unittest
from datetime import date
from decimal import Decimal
from typing import Dict, List, Optional
from company_reports.contracts import (
BatchSnapshot,
BookingRoomItem,
DailyVersionPin,
ErrorCode,
FinanceFact,
WarningCode,
)
from company_reports.core import (
build_company_report,
company_for_fact,
normalize_group_code,
)
def finance_fact(
record_id: int,
*,
daily_version_id: int = 1000,
group: str = "SYN-GROUP-A",
arrival: date = date(2026, 7, 8),
departure: date = date(2026, 7, 10),
nights: int = 2,
room_label: str = "SYN-RM2",
total_price: Decimal = Decimal("900"),
block_code: str = "SYN-BLOCK-A",
company_key: str = "QBD",
channel_key: str = "QBD",
) -> FinanceFact:
return FinanceFact(
daily_record_id=record_id,
daily_version_id=daily_version_id,
business_date=date(2026, 7, 9),
channel_key=channel_key,
company_key=company_key,
company_name="SYN-COMPANY",
block_code=block_code,
group_code_key=group,
res_comment=group,
room_category_label=room_label,
arrival=arrival,
departure=departure,
nights=nights,
total_price=total_price,
booking_source_match_status="matched",
)
def room_item(
item_no: int,
*,
group: str = "SYN-GROUP-A",
arrival: date = date(2026, 7, 8),
departure: date = date(2026, 7, 10),
nights: int = 2,
room_type: str = "SYN-SUP-TWN",
quantity: int = 1,
parse_version_id: int = 700,
segment_no: int = 1,
) -> BookingRoomItem:
return BookingRoomItem(
group_code_key=group,
parse_version_id=parse_version_id,
segment_no=segment_no,
arrival=arrival,
departure=departure,
nights=nights,
item_no=item_no,
room_type_raw=room_type,
quantity=quantity,
)
def snapshot(
facts: List[FinanceFact],
items: List[BookingRoomItem],
parse_versions: Optional[Dict[str, int]] = None,
) -> BatchSnapshot:
pins_by_date = {fact.business_date: fact.daily_version_id for fact in facts}
pins = tuple(
DailyVersionPin(business_date, version_id)
for business_date, version_id in sorted(pins_by_date.items())
)
return BatchSnapshot(
facts=tuple(facts),
room_items=tuple(items),
daily_versions=pins,
group_parse_versions=parse_versions
if parse_versions is not None
else {"SYN-GROUP-A": 700},
)
class CompanyReportCoreTests(unittest.TestCase):
def test_group_code_normalization_and_company_mapping_precedence(self):
self.assertEqual(normalize_group_code(" syngroup-a "), "SYN-GROUP-A")
cases = (
("LIAN TAI", "OTHER", "LianTai"),
("QBD", "OTHER", "QBD"),
("UNMAPPED", "DY-AI-Easy-KB", "DY-AI-Easy-KB"),
("FENGRUN", "OTHER", "FengRun"),
("HANA TOUR", "OTHER", "HanaTour"),
)
for company_key, channel_key, expected in cases:
with self.subTest(expected=expected):
self.assertEqual(
company_for_fact(
finance_fact(
1,
company_key=company_key,
channel_key=channel_key,
)
),
expected,
)
def test_same_stay_is_aggregated_with_static_prices_and_room_quantities(self):
facts = [
finance_fact(1),
finance_fact(2, block_code="SYN-BLOCK-B"),
finance_fact(
3,
room_label="SYN-RM3",
total_price=Decimal("3400"),
block_code="SYN-BLOCK-A",
),
]
items = [
room_item(1, quantity=1),
room_item(2, quantity=1),
room_item(3, room_type="SYN-SUP-TRP", quantity=1),
]
report = build_company_report(
"QBD", 2026, 7, date(2026, 7, 10), snapshot(facts, items)
)
self.assertTrue(report.valid)
self.assertEqual(report.row_count, 1)
row = report.periods[0].rows[0]
self.assertEqual(row.block_code, "SYN-BLOCK-ASYN-BLOCK-B")
self.assertEqual(row.booking_room, "【SYN-SUP-TWN】2【SYN-SUP-TRP】1")
self.assertEqual(
row.total_booking_price,
"【SYN-RM2】900×2 + 【SYN-RM3】3,400×1 = 5,200",
)
self.assertFalse(row.duplicate_group)
self.assertFalse(row.multi_price_review)
self.assertFalse(report.periods[1].active)
self.assertEqual(report.periods[1].rows, ())
def test_same_group_with_different_stays_is_split_and_highlighted(self):
second_arrival = date(2026, 7, 17)
second_departure = date(2026, 7, 21)
facts = [
finance_fact(1),
finance_fact(
2,
arrival=second_arrival,
departure=second_departure,
nights=4,
total_price=Decimal("1800"),
),
]
items = [
room_item(1),
room_item(
2,
arrival=second_arrival,
departure=second_departure,
nights=4,
quantity=2,
segment_no=2,
),
]
report = build_company_report(
"QBD", 2026, 7, date(2026, 7, 31), snapshot(facts, items)
)
self.assertTrue(report.valid)
self.assertEqual([len(period.rows) for period in report.periods], [1, 0, 1])
all_rows = [row for period in report.periods for row in period.rows]
self.assertTrue(all(row.duplicate_group for row in all_rows))
self.assertEqual(
all_rows[1].total_booking_price,
"【SYN-RM2】1,800×1 = 1,800",
)
def test_same_room_type_with_multiple_static_prices_warns_but_succeeds(self):
facts = [
finance_fact(1, total_price=Decimal("900")),
finance_fact(2, total_price=Decimal("1800")),
]
report = build_company_report(
"QBD", 2026, 7, date(2026, 7, 10), snapshot(facts, [room_item(1)])
)
self.assertTrue(report.valid)
self.assertEqual(
[warning.code for warning in report.warnings],
[WarningCode.MULTI_PRICE_REVIEW],
)
row = report.periods[0].rows[0]
self.assertTrue(row.multi_price_review)
self.assertEqual(
row.total_booking_price,
"【SYN-RM2】900×1 + 【SYN-RM2】1,800×1 = 2,700",
)
def test_invalid_source_facts_block_the_company_report(self):
cases = (
(
finance_fact(1, group=""),
[room_item(1)],
{"SYN-GROUP-A": 700},
ErrorCode.GROUP_CODE_MISSING,
),
(
finance_fact(1, nights=3),
[room_item(1)],
{"SYN-GROUP-A": 700},
ErrorCode.NIGHTS_CONFLICT,
),
(
finance_fact(1, total_price=Decimal("-1")),
[room_item(1)],
{"SYN-GROUP-A": 700},
ErrorCode.TOTAL_PRICE_INVALID,
),
(
finance_fact(1),
[room_item(1)],
{},
ErrorCode.BOOKING_PARSE_FAILED,
),
(
finance_fact(1),
[],
{"SYN-GROUP-A": 700},
ErrorCode.ROOM_ITEMS_MISSING,
),
)
for fact, items, versions, expected in cases:
with self.subTest(expected=expected):
report = build_company_report(
"QBD",
2026,
7,
date(2026, 7, 10),
snapshot([fact], items, versions),
)
self.assertFalse(report.valid)
self.assertIn(expected, {error.code for error in report.errors})
with self.assertRaises(ValueError):
report.to_workbook_payload()
def test_departure_controls_period_and_rows_are_deterministic(self):
facts = [
finance_fact(
2,
group="SYN-GROUP-B",
arrival=date(2026, 7, 9),
departure=date(2026, 7, 11),
),
finance_fact(
1,
arrival=date(2026, 7, 6),
departure=date(2026, 7, 10),
nights=4,
),
]
items = [
room_item(
1,
arrival=date(2026, 7, 6),
departure=date(2026, 7, 10),
nights=4,
),
room_item(
2,
group="SYN-GROUP-B",
arrival=date(2026, 7, 9),
departure=date(2026, 7, 11),
),
]
report = build_company_report(
"QBD",
2026,
7,
date(2026, 7, 20),
snapshot(
facts,
items,
{"SYN-GROUP-A": 700, "SYN-GROUP-B": 700},
),
)
self.assertTrue(report.valid)
self.assertEqual([len(period.rows) for period in report.periods], [1, 1, 0])
self.assertEqual(report.periods[0].rows[0].departure, date(2026, 7, 10))
self.assertEqual(report.periods[1].rows[0].departure, date(2026, 7, 11))
def test_empty_company_still_has_three_header_only_periods(self):
report = build_company_report(
"HanaTour",
2026,
2,
date(2026, 2, 28),
snapshot([], [], {}),
)
self.assertTrue(report.valid)
self.assertEqual(report.filename, "HanaTour-February-2026.xlsx")
self.assertEqual(
[period.key for period in report.periods],
["01-10", "11-20", "21-28"],
)
self.assertEqual(report.row_count, 0)
def test_month_names_are_fixed_english_contract_values(self):
report = build_company_report(
"FengRun",
2026,
9,
date(2026, 9, 10),
snapshot([], [], {}),
)
self.assertEqual(report.filename, "FengRun-September-2026.xlsx")
self.assertEqual(report.periods[0].sheet_name, "FengRun 01-10 Sep 2026")
if __name__ == "__main__":
unittest.main()