feat: add daily manual price review workflow
This commit is contained in:
@@ -16,6 +16,7 @@ from openpyxl import load_workbook
|
||||
from arr_ingestion.contracts import IngestionError
|
||||
from arr_ingestion.validation import DeliveryValidator, ProcessorPolicy
|
||||
from tests.test_arr_opera_daily_ingest import (
|
||||
manual_override_manifest,
|
||||
reservation,
|
||||
run_processor,
|
||||
xml_document,
|
||||
@@ -55,9 +56,22 @@ def artifact_ref(key: str, filename: str, value: bytes, mime_type: str) -> dict[
|
||||
}
|
||||
|
||||
|
||||
def build_delivery(xml_text: str, root: Path) -> tuple[bytes, MemoryStore, dict[str, bytes]]:
|
||||
exit_code, xml_path, output_dir, result, _structured = run_processor(xml_text, root)
|
||||
status = "success" if exit_code == 0 else "failed"
|
||||
def build_delivery(
|
||||
xml_text: str,
|
||||
root: Path,
|
||||
*,
|
||||
manual_override: dict[str, object] | None = None,
|
||||
review_job_id: str | None = None,
|
||||
review_case_id: str | None = None,
|
||||
) -> tuple[bytes, MemoryStore, dict[str, bytes]]:
|
||||
_exit_code, xml_path, output_dir, result, structured = run_processor(
|
||||
xml_text,
|
||||
root,
|
||||
manual_override=manual_override,
|
||||
review_job_id=review_job_id,
|
||||
review_case_id=review_case_id,
|
||||
)
|
||||
status = result["status"]
|
||||
values = {
|
||||
"source_xml": xml_path.read_bytes(),
|
||||
"result_json": (output_dir / "result.json").read_bytes(),
|
||||
@@ -65,8 +79,10 @@ def build_delivery(xml_text: str, root: Path) -> tuple[bytes, MemoryStore, dict[
|
||||
}
|
||||
if status == "success":
|
||||
values["daily_report"] = (output_dir / result["outputs"]["daily_report"]).read_bytes()
|
||||
else:
|
||||
elif status == "failed":
|
||||
values["exception_report"] = (output_dir / result["outputs"]["exception_report"]).read_bytes()
|
||||
if structured["artifacts"]["manual_override_json"] is not None:
|
||||
values["manual_override_json"] = (root / "manual-override.json").read_bytes()
|
||||
keys = {role: f"arr/jobs/job-001/{role}/{role}" for role in values}
|
||||
objects = {keys[role]: value for role, value in values.items()}
|
||||
xlsx_mime = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
@@ -78,7 +94,7 @@ def build_delivery(xml_text: str, root: Path) -> tuple[bytes, MemoryStore, dict[
|
||||
"status": status,
|
||||
"processor_version": processor.PROCESSOR_VERSION,
|
||||
"rule_set_sha256": processor.rule_set_sha256(),
|
||||
"result_schema_version": "3.0",
|
||||
"result_schema_version": processor.RESULT_VERSION,
|
||||
"business_date": result["business_date"],
|
||||
"artifacts": {
|
||||
"source_xml": artifact_ref(keys["source_xml"], xml_path.name, values["source_xml"], "application/xml"),
|
||||
@@ -111,6 +127,16 @@ def build_delivery(xml_text: str, root: Path) -> tuple[bytes, MemoryStore, dict[
|
||||
if status == "failed"
|
||||
else None
|
||||
),
|
||||
"manual_override_json": (
|
||||
artifact_ref(
|
||||
keys["manual_override_json"],
|
||||
"manual-override.json",
|
||||
values["manual_override_json"],
|
||||
"application/json",
|
||||
)
|
||||
if "manual_override_json" in values
|
||||
else None
|
||||
),
|
||||
},
|
||||
}
|
||||
return json.dumps(envelope, ensure_ascii=False).encode("utf-8"), MemoryStore(objects), values
|
||||
@@ -186,7 +212,7 @@ class DeliveryValidationTests(unittest.TestCase):
|
||||
|
||||
def test_failed_delivery_is_retained_but_never_declared_activation_eligible(self):
|
||||
failure_xml = xml_document(
|
||||
reservation(1, rate_code="GRPA4", rate_amount="1800", res_comment="SYN-FAIL")
|
||||
reservation(1, departure="2026-07-26", res_comment="SYN-FAIL")
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
raw, store, _values = build_delivery(failure_xml, Path(temp_dir))
|
||||
@@ -206,6 +232,53 @@ class DeliveryValidationTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(IngestionError, "identity is invalid"):
|
||||
DeliveryValidator(store, policy()).validate(json.dumps(envelope).encode("utf-8"))
|
||||
|
||||
def test_review_delivery_is_replayed_without_daily_or_finance_artifacts(self):
|
||||
review_xml = xml_document(
|
||||
reservation(1, rate_code="GRPA1", rate_amount="900"),
|
||||
reservation(2, rate_code="GRPA1", rate_amount="1800"),
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
raw, store, _values = build_delivery(review_xml, Path(temp_dir))
|
||||
verified = DeliveryValidator(store, policy()).validate(raw)
|
||||
self.assertEqual(verified.envelope.status, "review_required")
|
||||
self.assertEqual(verified.structured_payload["candidate_rows"], 1)
|
||||
self.assertEqual(verified.structured_payload["review_required_rows"], 1)
|
||||
self.assertEqual(verified.structured_payload["review_issue_count"], 1)
|
||||
self.assertIsNone(verified.envelope.artifacts["daily_report"])
|
||||
self.assertIsNone(verified.envelope.artifacts["exception_report"])
|
||||
self.assertIsNone(verified.envelope.artifacts["manual_override_json"])
|
||||
self.assertEqual(store.fetch_count, 3)
|
||||
|
||||
def test_final_manual_delivery_replays_frozen_manifest(self):
|
||||
source = xml_document(
|
||||
reservation(1, rate_code="GRPA1", rate_amount="900"),
|
||||
reservation(2, rate_code="GRPA1", rate_amount="1800"),
|
||||
)
|
||||
review_case_id = "dailyreview-0123456789abcdef0123456789abcdef"
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = Path(temp_dir)
|
||||
review = run_processor(source, root / "review")
|
||||
manifest = manual_override_manifest(
|
||||
review[1],
|
||||
review[4]["review_issues"],
|
||||
job_id="job-001",
|
||||
review_case_id=review_case_id,
|
||||
real_price="0.00",
|
||||
)
|
||||
raw, store, _values = build_delivery(
|
||||
source,
|
||||
root / "final",
|
||||
manual_override=manifest,
|
||||
review_job_id="job-001",
|
||||
review_case_id=review_case_id,
|
||||
)
|
||||
verified = DeliveryValidator(store, policy()).validate(raw)
|
||||
self.assertEqual(verified.envelope.status, "success")
|
||||
self.assertEqual(verified.structured_payload["review_case_id"], review_case_id)
|
||||
self.assertEqual(verified.structured_payload["manually_priced_rows"], 1)
|
||||
self.assertIsNotNone(verified.envelope.artifacts["manual_override_json"])
|
||||
self.assertEqual(store.fetch_count, 5)
|
||||
|
||||
def test_duplicate_json_keys_are_rejected(self):
|
||||
raw = b'{"delivery_schema_version":"1.0","delivery_schema_version":"1.0"}'
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
|
||||
Reference in New Issue
Block a user