feat: add daily manual price review workflow
This commit is contained in:
@@ -5,6 +5,7 @@ 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
|
||||
@@ -12,6 +13,7 @@ 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,
|
||||
@@ -23,6 +25,19 @@ from tests.test_arr_opera_daily_ingest import (
|
||||
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)
|
||||
@@ -79,18 +94,21 @@ class ProgrammaticUploadTests(unittest.TestCase):
|
||||
{"source.xml", "result.json", "structured-result.json", "daily-report.xlsx"},
|
||||
)
|
||||
|
||||
def test_business_failure_is_recorded_without_partial_active_version(self):
|
||||
price_failure = xml_document(
|
||||
reservation(1, rate_code="GRPA4", rate_amount="1800"),
|
||||
reservation(2, rate_code="NOT-ALLOWED", rate_amount="INVALID"),
|
||||
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_failure.encode("utf-8"))
|
||||
receipt = upload.submit("ARR.XML", price_review.encode("utf-8"))
|
||||
|
||||
self.assertEqual(receipt["status"], "failed")
|
||||
self.assertEqual(receipt["ingestion_status"], "recorded_failure")
|
||||
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 = {
|
||||
@@ -100,9 +118,106 @@ class ProgrammaticUploadTests(unittest.TestCase):
|
||||
}
|
||||
self.assertEqual(
|
||||
committed,
|
||||
{"source.xml", "result.json", "structured-result.json", "exception-report.xlsx"},
|
||||
{"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()
|
||||
|
||||
Reference in New Issue
Block a user