Files
ARR-2.0-0918/tests/test_arr_cancelled_review.py
T

120 lines
6.3 KiB
Python

"""Cancellation eligibility before field review; synthetic, offline sources only."""
import copy
import hashlib
import json
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
from arr_processing.policy import load_processor_policy
from arr_web.arr_data_review import DataFieldReviews
from arr_web.contracts import PortalError
from integrations.ohip.collect_arr_source import CollectionError
from tests.test_arr_data_review import source_document, raw, gap, CONTEXT, DAY, REQUEST, ACTOR, SOURCE_MANIFEST
class CancelledReviewTests(unittest.TestCase):
def setUp(self):
temporary = tempfile.TemporaryDirectory(prefix="arr-cancelled-review-")
self.addCleanup(temporary.cleanup)
self.root = Path(temporary.name) / "reviews"
self.policy = load_processor_policy(Path(__file__).resolve().parents[1])
self.service = DataFieldReviews(root=self.root, policy=self.policy, context=CONTEXT)
self.document = source_document(2)
def prepare(self):
return self.service.prepare(REQUEST, raw(self.document), SOURCE_MANIFEST, DAY)
def evidence(self, statuses=("Cancelled", "InHouse")):
return {"version": "arr-reservation-status-evidence/v1", "policy_id": "arr-exclude-cancelled/v1",
"original_sha256": hashlib.sha256(raw(self.document)).hexdigest(),
"source_manifest_sha256": SOURCE_MANIFEST, "hotel_id": CONTEXT["hotel_id"], "report_date": DAY,
"records": [{"source_sequence": row["source_sequence"], "reservation_id": row["reservation_id"],
"reservation_status": status, "sources": [{"file": "synthetic.response.bin", "sha256": "f" * 64}]}
for row, status in zip(self.document["records"], statuses)]}
def apply(self, revision, evidence=None):
with patch("integrations.ohip.reservation_status.saved_status_evidence", return_value=evidence or self.evidence()):
return self.service.apply_saved_status_evidence(REQUEST, Path("unused-synthetic-capture"),
expected_revision=revision)
def test_cancelled_fields_and_rate_are_not_manual_requirements_but_other_rows_still_are(self):
self.document["records"][0]["reservation_status"] = "Cancelled"
for field in ("DISP_ROOM_NO", "RATE_CODE", "BLOCK_CODE", "ADULTS"):
gap(self.document, field)
gap(self.document, "DISP_ROOM_NO", sequence=2)
review = self.prepare()
self.assertEqual([item["item_id"] for item in review["items"]], ["2:DISP_ROOM_NO"])
self.assertEqual(review["excluded_cancelled_count"], 1)
self.assertFalse(review["can_finalize"])
def test_saved_statuses_remove_room_task_preserve_original_and_freeze_audited_source(self):
gap(self.document, "DISP_ROOM_NO")
before = self.prepare()
original = self.service.original(REQUEST)
review = self.apply(before["revision"])
self.assertEqual(review["pending_count"], 0)
self.assertEqual(review["items"], [])
self.assertTrue(review["can_finalize"])
self.assertEqual(self.service.original(REQUEST), original)
self.assertIsNone(self.service.payload(REQUEST))
self.assertEqual(self.prepare(), review, "a saved-status overlay must not bypass explicit continuation")
self.assertEqual(self.apply(before["revision"]), review, "exact retry is idempotent")
final = self.service.finalize(REQUEST, review["revision"], ACTOR)
restored = DataFieldReviews(root=self.root, policy=self.policy, context=CONTEXT)
payload, binding = restored.payload(REQUEST)
derived = json.loads(payload)
self.assertEqual(derived["records"][0]["fields"], self.document["records"][0]["fields"])
self.assertEqual(derived["records"][0]["reservation_status"], "Cancelled")
audit = derived["manual_data_review"]["manifest"]
self.assertEqual(audit["changes"], [])
self.assertEqual(audit["reservation_status_evidence"]["original_sha256"], hashlib.sha256(original[0]).hexdigest())
self.assertEqual([event["action"] for event in audit["events"]], ["reservation_status_evidence", "finalize"])
self.assertEqual(restored.get(REQUEST), final)
self.assertEqual(restored.original(REQUEST), original)
self.assertEqual(len(binding), 64)
with self.assertRaises(PortalError):
self.apply(final["revision"])
def test_prior_staff_decision_on_cancelled_row_stays_audited_but_cannot_be_edited(self):
gap(self.document, "BLOCK_CODE")
before = self.prepare()
saved = self.service.update(REQUEST, "1:BLOCK_CODE", before["revision"], "VERIFIED", ACTOR)
review = self.apply(saved["revision"])
self.assertEqual(review["items"], [])
with self.assertRaises(PortalError):
self.service.update(REQUEST, "1:BLOCK_CODE", review["revision"], "CHANGED", ACTOR)
self.service.finalize(REQUEST, review["revision"], ACTOR)
derived = json.loads(self.service.payload(REQUEST)[0])
self.assertEqual(derived["manual_data_review"]["manifest"]["changes"][0]["value"], "VERIFIED")
self.assertEqual(derived["records"][0]["fields"]["BLOCK_CODE"]["value"], "VERIFIED")
def test_overlay_rejects_wrong_order_context_and_revision_without_publishing(self):
gap(self.document, "DISP_ROOM_NO")
review = self.prepare()
with self.assertRaises(PortalError):
self.apply(review["revision"] + 1)
for alter in (lambda e: e["records"].reverse(),
lambda e: e.update(original_sha256="0" * 64),
lambda e: e["records"].pop(),
lambda e: e["records"][0].update(reservation_status="")):
evidence = self.evidence()
alter(evidence)
with self.assertRaises(CollectionError):
self.apply(review["revision"], evidence)
self.assertEqual(self.service.get(REQUEST), review)
def test_status_evidence_tamper_is_rejected(self):
gap(self.document, "DISP_ROOM_NO")
review = self.prepare()
self.apply(review["revision"])
evidence_file = next((self.root / REQUEST).glob("reservation-status-*.json"))
evidence_file.write_bytes(evidence_file.read_bytes() + b" ")
with self.assertRaises(CollectionError):
self.service.get(REQUEST)
if __name__ == "__main__":
unittest.main()