Files
wyndham-ARR/tests/test_arr_programmatic.py
2026-08-06 22:40:18 +08:00

224 lines
9.2 KiB
Python

from __future__ import annotations
import tempfile
import unittest
from datetime import date
from pathlib import Path
from arr_ingestion.contracts import IngestionError
from arr_ingestion.repository import InMemoryIngestionRepository
from arr_ingestion.service import IngestionService
from arr_ingestion.validation import DeliveryValidator
from arr_processing.local import LocalDailyProcessor
from arr_processing.policy import load_processor_policy
from arr_storage.filesystem import FilesystemObjectBackend
from arr_storage.store import ManagedObjectStore
from arr_web.contracts import PortalError
from arr_web.programmatic import ProgrammaticUploadCoordinator
from tests.test_arr_opera_daily_ingest import (
reservation,
success_xml,
xml_document,
)
PROJECT_ROOT = Path(__file__).resolve().parents[1]
class RejectingManualReplayProcessor:
def __init__(self, delegate: LocalDailyProcessor) -> None:
self._delegate = delegate
def run(self, *args, **kwargs):
if kwargs.get("manual_overrides") is not None:
raise IngestionError(
"PROCESSOR_RESULT_INVALID",
"manual replay contract is invalid",
)
return self._delegate.run(*args, **kwargs)
def coordinator(root: Path):
store = ManagedObjectStore(
FilesystemObjectBackend(root / "objects", create=True)
)
repository = InMemoryIngestionRepository()
policy = load_processor_policy(PROJECT_ROOT)
service = IngestionService(DeliveryValidator(store, policy), repository)
return (
ProgrammaticUploadCoordinator(
object_store=store,
ingestion_repository=repository,
ingestion_service=service,
processor=LocalDailyProcessor(policy),
processor_version=policy.processor_version,
rule_set_sha256=policy.rule_set_sha256,
),
repository,
root / "objects",
)
class ProgrammaticUploadTests(unittest.TestCase):
def test_valid_xml_reaches_atomic_terminal_success_without_remote_boundary(self):
with tempfile.TemporaryDirectory() as temporary:
upload, repository, objects = coordinator(Path(temporary))
receipt = upload.submit("ARR.XML", success_xml().encode("utf-8"))
self.assertEqual(receipt["status"], "succeeded")
self.assertEqual(receipt["ingestion_status"], "committed")
self.assertEqual(receipt["business_date"], "2026-07-27")
self.assertEqual(receipt["version_no"], 1)
self.assertEqual(
repository._jobs[receipt["job_id"]].registration.uploaded_filename,
"ARR.XML",
)
self.assertEqual(
repository.current_version_id(date(2026, 7, 27)),
receipt["daily_version_id"],
)
records = repository.version_records(int(receipt["daily_version_id"]))
self.assertEqual(len(records), 5)
self.assertEqual(
sum(1 for item in records if item["outcome"] == "retained"),
3,
)
committed = {
path.name
for path in objects.rglob("*")
if path.is_file() and not path.name.endswith(".arr-metadata.json")
}
self.assertEqual(
committed,
{"source.xml", "result.json", "structured-result.json", "daily-report.xlsx"},
)
def test_price_unmatched_is_recorded_for_review_without_partial_active_version(self):
price_review = xml_document(
reservation(1, rate_code="GRPA1", rate_amount="900"),
reservation(2, rate_code="GRPA1", rate_amount="1800"),
)
with tempfile.TemporaryDirectory() as temporary:
upload, repository, objects = coordinator(Path(temporary))
receipt = upload.submit("ARR.XML", price_review.encode("utf-8"))
self.assertEqual(receipt["status"], "needs_review")
self.assertEqual(receipt["ingestion_status"], "recorded_review")
self.assertIsNotNone(receipt["review_case_id"])
self.assertEqual(receipt["review_completed_items"], 0)
self.assertEqual(receipt["review_total_items"], 1)
self.assertIsNone(receipt["version_no"])
self.assertIsNone(repository.current_version_id(date(2026, 7, 27)))
committed = {
path.name
for path in objects.rglob("*")
if path.is_file() and not path.name.endswith(".arr-metadata.json")
}
self.assertEqual(
committed,
{"source.xml", "result.json", "structured-result.json"},
)
def test_finalized_review_replays_original_xml_and_commits_manual_version(self):
source = xml_document(
reservation(1, rate_code="GRPA1", rate_amount="900"),
reservation(2, rate_code="GRPA1", rate_amount="1800", rooms="2"),
)
with tempfile.TemporaryDirectory() as temporary:
upload, repository, objects = coordinator(Path(temporary))
receipt = upload.submit("ARR.XML", source.encode("utf-8"))
self.assertEqual(receipt["status"], "needs_review")
self.assertIsNone(repository.current_version_id(date(2026, 7, 27)))
review = upload.get_price_review(receipt["job_id"], 50, 0)
self.assertEqual(review["total_items"], 1)
updated = upload.update_price_review_item(
receipt["job_id"],
review["items"][0]["item_id"],
review["case_id"],
review["revision"],
"0",
"operator@example.test",
)
final = upload.finalize_price_review(
receipt["job_id"],
updated["case_id"],
updated["revision"],
"operator@example.test",
)
self.assertEqual(final["status"], "succeeded")
self.assertEqual(final["ingestion_status"], "committed")
self.assertEqual(final["version_no"], 1)
self.assertEqual(repository.current_version_id(date(2026, 7, 27)), final["daily_version_id"])
records = repository.version_records(int(final["daily_version_id"]))
self.assertEqual(
[record["pricing_method"] for record in records],
["price_reference_exact", "manual_review"],
)
completed = upload.get_price_review(receipt["job_id"], 50, 0)
self.assertEqual(completed["case_status"], "completed")
committed = {
path.name
for path in objects.rglob("*")
if path.is_file() and not path.name.endswith(".arr-metadata.json")
}
self.assertEqual(
committed,
{
"source.xml",
"result.json",
"structured-result.json",
"daily-report.xlsx",
"manual-override.json",
},
)
def test_non_price_validation_error_remains_business_failure(self):
invalid = xml_document(reservation(1, departure="2026-07-26"))
with tempfile.TemporaryDirectory() as temporary:
upload, repository, _objects = coordinator(Path(temporary))
receipt = upload.submit("ARR.XML", invalid.encode("utf-8"))
self.assertEqual(receipt["status"], "failed")
self.assertEqual(receipt["ingestion_status"], "recorded_failure")
self.assertIsNone(repository.current_version_id(date(2026, 7, 27)))
def test_deterministic_finalization_error_closes_review_case(self):
source = xml_document(
reservation(1, rate_code="GRPA1", rate_amount="900"),
reservation(2, rate_code="GRPA1", rate_amount="1800"),
)
with tempfile.TemporaryDirectory() as temporary:
upload, repository, _objects = coordinator(Path(temporary))
receipt = upload.submit("ARR.XML", source.encode("utf-8"))
review = upload.get_price_review(receipt["job_id"], 50, 0)
updated = upload.update_price_review_item(
receipt["job_id"],
review["items"][0]["item_id"],
review["case_id"],
review["revision"],
"0",
"operator@example.test",
)
upload.processor = RejectingManualReplayProcessor(upload.processor) # type: ignore[assignment]
with self.assertRaises(PortalError) as raised:
upload.finalize_price_review(
receipt["job_id"],
updated["case_id"],
updated["revision"],
"operator@example.test",
)
self.assertEqual(raised.exception.code, "PROCESSOR_RESULT_INVALID")
self.assertEqual(
upload.get_price_review(receipt["job_id"], 50, 0)["case_status"],
"failed",
)
self.assertEqual(repository._jobs[receipt["job_id"]].status, "failed")
self.assertIsNone(repository.current_version_id(date(2026, 7, 27)))
if __name__ == "__main__":
unittest.main()