from __future__ import annotations import json import os import shutil import subprocess import tempfile import unittest from datetime import date from decimal import Decimal from pathlib import Path from openpyxl import load_workbook from monthly_reports.contracts import ( ChannelObservation, DailyVersionPin, MonthlyFact, MonthlySnapshot, ) from monthly_reports.core import build_monthly_report PROJECT_ROOT = Path(__file__).resolve().parents[1] BUILDER = PROJECT_ROOT / "monthly_reports" / "xlsx" / "build_workbook.mjs" def artifact_tool_module() -> str: return os.environ.get("MONTHLY_REPORT_ARTIFACT_TOOL_MODULE", "").strip() def node_binary() -> str: return os.environ.get("MONTHLY_REPORT_NODE_BINARY", "").strip() or shutil.which("node") or "" def report_payload() -> dict: source = MonthlyFact( daily_record_id=1, daily_version_id=100, business_date=date(2026, 7, 8), channel_key="QBD", arrival=date(2026, 7, 8), departure=date(2026, 7, 10), nights=2, adults=2, children=0, block_code="=SYN-FORMULA-LIKE", no_of_rooms=1, company_name="SYN-COMPANY", confirmation_no="SYN-CONFIRMATION", disp_room_no="SYN-ROOM", effective_rate_amount=Decimal("450.50"), full_name="SYN-GUEST", res_comment="SYN-COMMENT", trace_text="", products="", rate_code="SYN-RATE", room_category_label="SYN-TYPE", real_price=Decimal("450.50"), total_price=Decimal("901.00"), kb_amount=None, ) report = build_monthly_report( 2026, 7, date(2026, 7, 8), MonthlySnapshot( (source,), (DailyVersionPin(date(2026, 7, 8), 100),), (ChannelObservation(date(2026, 7, 8), 100, "QBD", 1, 1),), ), ) return report.to_workbook_payload() @unittest.skipUnless( bool(artifact_tool_module() and Path(artifact_tool_module()).is_file()), "artifact-tool module is not configured", ) class MonthlyReportsXlsxTests(unittest.TestCase): def test_builder_exports_reopens_scans_and_renders_every_channel(self): node = node_binary() if not node: self.skipTest("Node.js is unavailable") with tempfile.TemporaryDirectory(prefix="monthly-report-xlsx-") as temp_dir: root = Path(temp_dir) payload_path = root / "payload.json" payload_path.write_text( json.dumps(report_payload(), ensure_ascii=False), encoding="utf-8", ) output = root / "monthly.xlsx" previews = root / "previews" summary_path = root / "summary.json" completed = subprocess.run( [ node, str(BUILDER), str(payload_path), str(output), str(previews), str(summary_path), ], cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=120, check=False, env=os.environ.copy(), ) self.assertEqual(completed.returncode, 0, completed.stderr) summary = json.loads(summary_path.read_text(encoding="utf-8")) self.assertEqual(summary["sheet_names"][:5], [ "LIANTAI-GROUP", "LIANTAI-FIT", "QBD", "DY-AI-Easy-KB", "FENGRUN" ]) self.assertEqual(summary["row_counts"], [0, 0, 1, 0, 0]) self.assertEqual(summary["formula_count"], 1) self.assertEqual(summary["preview_count"], 5) self.assertEqual(len(list(previews.glob("sheet-*.png"))), 5) self.assertEqual(output.stat().st_mode & 0o777, 0o600) self.assertFalse(Path(f"{output}.inspect.ndjson").exists()) workbook = load_workbook(output, data_only=False, read_only=True) try: self.assertEqual(workbook["QBD"]["S2"].value, "=R2*C2*G2") formulas = [ cell.value for sheet in workbook.worksheets for row in sheet.iter_rows() for cell in row if cell.data_type == "f" ] self.assertEqual(formulas, ["=R2*C2*G2"]) finally: workbook.close() def test_invalid_payload_returns_only_stable_error_code(self): node = node_binary() if not node: self.skipTest("Node.js is unavailable") with tempfile.TemporaryDirectory(prefix="monthly-report-xlsx-") as temp_dir: root = Path(temp_dir) invalid = root / "invalid.json" invalid.write_text( json.dumps({"schema_version": "bad", "sensitive": "SYN-SECRET"}), encoding="utf-8", ) completed = subprocess.run( [ node, str(BUILDER), str(invalid), str(root / "invalid.xlsx"), str(root / "previews"), str(root / "summary.json"), ], cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=30, check=False, env=os.environ.copy(), ) self.assertEqual(completed.returncode, 4) self.assertEqual( json.loads(completed.stderr), { "status": "failed", "code": "MONTHLY_REPORT_OUTPUT_VALIDATION_FAILED", }, ) self.assertNotIn("SYN-SECRET", completed.stderr) if __name__ == "__main__": unittest.main()