feat: sync latest ARR implementation
This commit is contained in:
203
tests/test_booking_excel_ingestion.py
Normal file
203
tests/test_booking_excel_ingestion.py
Normal file
@@ -0,0 +1,203 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import unittest
|
||||
import zipfile
|
||||
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import PatternFill
|
||||
|
||||
from booking_ingestion.excel import (
|
||||
BookingExcelError,
|
||||
REVIEW_CONFIRMED,
|
||||
REVIEW_PENDING,
|
||||
build_excel_parse_result,
|
||||
parse_excel_bytes,
|
||||
validate_excel_filename,
|
||||
)
|
||||
|
||||
|
||||
def workbook_bytes(sheets: list[tuple[str, list[list[object]]]]) -> bytes:
|
||||
workbook = Workbook()
|
||||
first = True
|
||||
for title, rows in sheets:
|
||||
worksheet = workbook.active if first else workbook.create_sheet()
|
||||
first = False
|
||||
worksheet.title = title
|
||||
for row in rows:
|
||||
worksheet.append(row)
|
||||
output = io.BytesIO()
|
||||
workbook.save(output)
|
||||
workbook.close()
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
def booking_rows(*rows: list[object]) -> bytes:
|
||||
return workbook_bytes(
|
||||
[
|
||||
(
|
||||
"BOOKING",
|
||||
[
|
||||
["Booking export"],
|
||||
["NO", "Tour Code", "Tour Days", "โรงแรม", "备注"],
|
||||
*rows,
|
||||
],
|
||||
),
|
||||
(
|
||||
"ROOM_TYPE_MASTER",
|
||||
[["Pattern", "Category"], ["【U-TWN8.5】", "U-TWN"]],
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class BookingExcelParserTests(unittest.TestCase):
|
||||
def test_extracts_tour_code_room_items_and_review_rows(self) -> None:
|
||||
payload = booking_rows(
|
||||
[
|
||||
1,
|
||||
"\t\nLT260715LD",
|
||||
"2-2-1",
|
||||
"17-19 酒店 (【U-TWN8.5】 17,【U-เตียงเสริม13】 2)(ออฟฟิศจ่าย)【ยืนยันเเล้ว】;",
|
||||
],
|
||||
[
|
||||
2,
|
||||
"LLT260715AC",
|
||||
"2-2",
|
||||
"17-19 酒店 (Sup【高级房TWN】 12,Sup【高级房DBL】 9)",
|
||||
],
|
||||
[3, "LLT260715AD", "2-3", "17-20 酒店 (【U-TWN13】"],
|
||||
[4, "LLT260720HB", "2-2", "20-22 酒店 (【U-TWN1.1】 6)"],
|
||||
[5, "HD260721VIPA", "2-2", "23-26 酒店 (【拼จU-4ค24】 2)"],
|
||||
[6, "IGNORED", "2-2", "23-26 酒店 (【surcharge】 1)"],
|
||||
)
|
||||
|
||||
document = parse_excel_bytes(payload)
|
||||
|
||||
self.assertEqual(document.worksheets, ("BOOKING",))
|
||||
self.assertEqual([row.group_code_raw for row in document.rows], [
|
||||
"LT260715LD",
|
||||
"LLT260715AC",
|
||||
"LLT260715AD",
|
||||
"LLT260720HB",
|
||||
"HD260721VIPA",
|
||||
])
|
||||
self.assertEqual(document.extracted_item_count, 7)
|
||||
self.assertEqual(document.pending_item_count, 2)
|
||||
|
||||
mixed = document.rows[0]
|
||||
self.assertEqual(
|
||||
[
|
||||
(item.room_type_code, item.quantity, item.review_status)
|
||||
for item in mixed.room_items
|
||||
],
|
||||
[
|
||||
("U-TWN", 17, REVIEW_CONFIRMED),
|
||||
(None, 2, REVIEW_PENDING),
|
||||
],
|
||||
)
|
||||
self.assertEqual(mixed.room_items[1].room_type_raw, "U-เตียงเสริม13")
|
||||
|
||||
split = document.rows[1]
|
||||
self.assertEqual(
|
||||
[(item.room_type_code, item.quantity) for item in split.room_items],
|
||||
[("TWN", 12), ("DBL", 9)],
|
||||
)
|
||||
self.assertEqual(document.rows[2].room_items[0].quantity, 1)
|
||||
self.assertEqual(document.rows[2].room_items[0].room_type_code, "U-TWN")
|
||||
self.assertEqual(document.rows[3].room_items[0].room_type_code, "U-TWN")
|
||||
self.assertEqual(document.rows[4].room_items[0].quantity, 2)
|
||||
self.assertIsNone(document.rows[4].room_items[0].room_type_code)
|
||||
|
||||
parsed = build_excel_parse_result(mixed)
|
||||
self.assertEqual(parsed["status"], "needs_review")
|
||||
self.assertEqual(len(parsed["room_items"]), 2)
|
||||
self.assertEqual(parsed["warnings"][0]["code"], "BOOKING_ROOM_TYPE_REVIEW_REQUIRED")
|
||||
|
||||
def test_physically_last_tour_row_replaces_earlier_and_cancel_removes_it(self) -> None:
|
||||
payload = booking_rows(
|
||||
[1, "SAME", "", "酒店 (【U-TWN8.5】 5)"],
|
||||
[2, "CANCELLED", "", "酒店 (【U-TWN8.5】 3)"],
|
||||
[3, "SAME", "", "酒店 (【U-DBL12】 2)"],
|
||||
[4, "CANCELLED", "", "取消"],
|
||||
)
|
||||
|
||||
document = parse_excel_bytes(payload)
|
||||
|
||||
self.assertEqual(len(document.rows), 1)
|
||||
self.assertEqual(document.rows[0].group_code_key, "SAME")
|
||||
self.assertEqual(document.rows[0].room_items[0].room_type_code, "U-DBL")
|
||||
self.assertEqual(document.rows[0].room_items[0].quantity, 2)
|
||||
|
||||
def test_yellow_formatting_alone_does_not_cancel_a_tour(self) -> None:
|
||||
workbook = Workbook()
|
||||
worksheet = workbook.active
|
||||
worksheet.title = "Booking"
|
||||
worksheet.append(["NO", "Tour Code", "Tour Days", "โรงแรม", "备注"])
|
||||
worksheet.append([1, "YELLOW", "", "酒店 (【U-TWN8.5】 3)", ""])
|
||||
worksheet.append([2, "YELLOW", "", "酒店 (【U-TWN8.5】 4)", ""])
|
||||
yellow = PatternFill(fill_type="solid", fgColor="FFFF00")
|
||||
for cell in worksheet[3]:
|
||||
if cell.value not in (None, ""):
|
||||
cell.fill = yellow
|
||||
output = io.BytesIO()
|
||||
workbook.save(output)
|
||||
workbook.close()
|
||||
|
||||
document = parse_excel_bytes(output.getvalue())
|
||||
|
||||
self.assertEqual(len(document.rows), 1)
|
||||
self.assertEqual(document.rows[0].group_code_key, "YELLOW")
|
||||
self.assertEqual(document.rows[0].room_items[0].quantity, 4)
|
||||
|
||||
def test_unbracketed_room_description_requires_review(self) -> None:
|
||||
payload = booking_rows(
|
||||
[1, "SUITE", "", "酒店 (Family Suite Two b/r Garden View 2)"],
|
||||
)
|
||||
|
||||
document = parse_excel_bytes(payload)
|
||||
item = document.rows[0].room_items[0]
|
||||
|
||||
self.assertEqual(item.room_type_raw, "Family Suite")
|
||||
self.assertEqual(item.quantity, 2)
|
||||
self.assertEqual(item.review_status, REVIEW_PENDING)
|
||||
|
||||
def test_filename_accepts_xlsx_only(self) -> None:
|
||||
self.assertEqual(validate_excel_filename("预订报表.XLSX"), "预订报表.XLSX")
|
||||
for invalid in ("report.xls", "report.xlsm", "../report.xlsx", ""):
|
||||
with self.subTest(invalid=invalid), self.assertRaises(BookingExcelError):
|
||||
validate_excel_filename(invalid)
|
||||
|
||||
def test_non_xlsx_and_macro_payloads_are_rejected(self) -> None:
|
||||
with self.assertRaises(BookingExcelError) as invalid:
|
||||
parse_excel_bytes(b"not an xlsx")
|
||||
self.assertEqual(invalid.exception.code, "BOOKING_EXCEL_INVALID")
|
||||
|
||||
source = booking_rows([1, "SYN", "", "酒店 (【U-TWN8.5】 1)"])
|
||||
original = zipfile.ZipFile(io.BytesIO(source))
|
||||
output = io.BytesIO()
|
||||
with original, zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as target:
|
||||
for info in original.infolist():
|
||||
target.writestr(info, original.read(info.filename))
|
||||
target.writestr("xl/vbaProject.bin", b"macro")
|
||||
with self.assertRaises(BookingExcelError) as macro:
|
||||
parse_excel_bytes(output.getvalue())
|
||||
self.assertEqual(macro.exception.code, "BOOKING_EXCEL_MACRO_UNSUPPORTED")
|
||||
|
||||
def test_workbook_without_tour_and_hotel_headers_is_rejected(self) -> None:
|
||||
payload = workbook_bytes(
|
||||
[("Data", [["Group Code", "Room Type", "Quantity"], ["SYN", "DBL", 1]])]
|
||||
)
|
||||
with self.assertRaises(BookingExcelError) as raised:
|
||||
parse_excel_bytes(payload)
|
||||
self.assertEqual(raised.exception.code, "BOOKING_EXCEL_HEADERS_MISSING")
|
||||
|
||||
def test_required_formula_is_rejected(self) -> None:
|
||||
formula = booking_rows([1, "SYN", "", "=\"酒店 (【U-TWN8.5】 1)\""])
|
||||
with self.assertRaises(BookingExcelError) as raised:
|
||||
parse_excel_bytes(formula)
|
||||
self.assertEqual(raised.exception.code, "BOOKING_EXCEL_FORMULA_UNSUPPORTED")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Reference in New Issue
Block a user