Files
wyndham-ARR/tests/test_monthly_reports_service.py
2026-07-29 16:38:05 +08:00

186 lines
6.4 KiB
Python

from __future__ import annotations
import json
import tempfile
import unittest
from datetime import date
from decimal import Decimal
from pathlib import Path
from monthly_reports.contracts import (
ChannelObservation,
DailyVersionPin,
ErrorCode,
MonthlyFact,
MonthlySnapshot,
)
from monthly_reports.publishing import BuildError, BuiltWorkbook, PublicationOutcome
from monthly_reports.repository import FileMetadata, RepositoryError, ReservedReport
from monthly_reports.service import MonthlyReportService, RunRequest, write_run_result
def synthetic_snapshot() -> MonthlySnapshot:
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-BLOCK-A",
no_of_rooms=1,
company_name="SYN-COMPANY",
confirmation_no="SYN-CONFIRMATION",
disp_room_no="SYN-ROOM",
effective_rate_amount=Decimal("450"),
full_name="SYN-GUEST",
res_comment="SYN-COMMENT",
trace_text="",
products="",
rate_code="SYN-RATE",
room_category_label="SYN-TYPE",
real_price=Decimal("450"),
total_price=Decimal("900"),
kb_amount=None,
)
return MonthlySnapshot(
(source,),
(DailyVersionPin(date(2026, 7, 8), 100),),
(ChannelObservation(date(2026, 7, 8), 100, "QBD", 1, 1),),
)
class FakeRepository:
def __init__(self, load_error=None):
self.load_error = load_error
self.failed = []
self.activated = []
def load_snapshot(self, _year, _month, _as_of):
if self.load_error:
raise self.load_error
return synthetic_snapshot()
def reserve_report(self, _report):
return ReservedReport(44, 2)
def activate_report(self, reservation, artifact, result_json):
self.activated.append((reservation, artifact, result_json))
def mark_failed(self, reservation, code, safe_message):
self.failed.append((reservation, code, safe_message))
class FakeBuilder:
def __init__(self, fail: bool = False):
self.fail = fail
def build(self, report, work_dir: Path):
if self.fail:
raise BuildError(
ErrorCode.OUTPUT_VALIDATION_FAILED,
"synthetic builder failure",
)
output = work_dir / report.filename
output.write_bytes(b"synthetic-workbook")
return BuiltWorkbook(
output,
"a" * 64,
output.stat().st_size,
{"semantic_sha256": "c" * 64},
)
class FakePublisher:
def publish(self, report, reservation, built, repository, work_dir):
artifact = FileMetadata(
"monthly_xlsx",
report.filename,
f"outputs/monthly_reports/2026/07/archive/v{reservation.version_no:04d}/{report.filename}",
built.sha256,
built.byte_size,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
)
result_json = FileMetadata(
"result_json",
"result.json",
f"outputs/monthly_reports/2026/07/archive/v{reservation.version_no:04d}/result.json",
"b" * 64,
1,
"application/json",
)
repository.activate_report(reservation, artifact, result_json)
return PublicationOutcome(
latest_path=work_dir / "latest.xlsx",
latest_result_path=work_dir / "latest.result.json",
archive_path=work_dir / report.filename,
result_path=work_dir / "result.json",
artifact=artifact,
result_json=result_json,
)
class MonthlyReportsServiceTests(unittest.TestCase):
def test_success_is_privacy_minimized_and_versioned(self):
repository = FakeRepository()
with tempfile.TemporaryDirectory(prefix="monthly-report-service-") as temp_dir:
root = Path(temp_dir)
service = MonthlyReportService(
repository,
FakeBuilder(),
FakePublisher(),
root / "staging",
)
result = service.run(RunRequest(2026, 7, date(2026, 7, 26)))
result_path = root / "run.result.json"
write_run_result(result_path, result)
serialized = result_path.read_text(encoding="utf-8")
self.assertEqual(result.status, "success")
self.assertEqual(result.exit_code, 0)
self.assertEqual(result.row_count, 1)
self.assertEqual(result.report_version_id, 44)
self.assertEqual(json.loads(serialized)["channel_manifest"][2]["worksheet"], "QBD")
self.assertNotIn("SYN-GUEST", serialized)
self.assertNotIn("SYN-CONFIRMATION", serialized)
self.assertNotIn("SYN-BLOCK", serialized)
self.assertEqual(len(repository.activated), 1)
def test_builder_failure_marks_reserved_report_failed(self):
repository = FakeRepository()
with tempfile.TemporaryDirectory(prefix="monthly-report-service-") as temp_dir:
result = MonthlyReportService(
repository,
FakeBuilder(fail=True),
FakePublisher(),
Path(temp_dir) / "staging",
).run(RunRequest(2026, 7, date(2026, 7, 26)))
self.assertEqual(result.status, "failed")
self.assertEqual(result.error_code, ErrorCode.OUTPUT_VALIDATION_FAILED)
self.assertEqual(repository.failed[0][0], ReservedReport(44, 2))
def test_source_failure_uses_business_or_system_exit_code(self):
cases = (
(ErrorCode.SOURCE_SNAPSHOT_STALE, 2),
(ErrorCode.DATABASE_FAILED, 4),
)
for code, exit_code in cases:
with self.subTest(code=code), tempfile.TemporaryDirectory() as temp_dir:
repository = FakeRepository(RepositoryError(code, "synthetic source failure"))
result = MonthlyReportService(
repository,
FakeBuilder(),
FakePublisher(),
Path(temp_dir) / "staging",
).run(RunRequest(2026, 7, date(2026, 7, 26)))
self.assertEqual(result.exit_code, exit_code)
self.assertEqual(result.error_stage, "source")
if __name__ == "__main__":
unittest.main()