120 lines
6.3 KiB
Python
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()
|