feat: add daily manual price review workflow

This commit is contained in:
Wyndham ARR
2026-08-06 22:40:18 +08:00
parent aae3d8e1db
commit ca3e8e18fa
77 changed files with 7750 additions and 602 deletions

View File

@@ -166,7 +166,7 @@ class IngestionServiceTests(unittest.TestCase):
def test_business_failure_is_recorded_without_switching_current(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))
@@ -179,6 +179,134 @@ class IngestionServiceTests(unittest.TestCase):
self.assertIsNone(outcome.daily_version_id)
self.assertIsNone(repository.current_version_id(date(2026, 7, 27)))
def test_review_receipt_has_no_version_then_frozen_manual_replay_commits_once(self):
source = xml_document(
reservation(1, rate_code="GRPA1", rate_amount="900"),
reservation(2, rate_code="GRPA1", rate_amount="1800"),
)
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
review_raw, review_store, _values = build_delivery(source, root / "review")
repository = InMemoryIngestionRepository()
envelope = register(repository, review_raw)
service = IngestionService(DeliveryValidator(review_store, policy()), repository)
review_outcome = service.ingest(review_raw)
self.assertEqual(review_outcome.status, "recorded_review")
self.assertIsNone(review_outcome.daily_version_id)
self.assertIsNone(repository.current_version_id(date(2026, 7, 27)))
case_id = review_outcome.review_case_id
self.assertIsNotNone(case_id)
current = repository.get_price_review(envelope.job_id, 50, 0)
self.assertEqual(current["total_items"], 1)
self.assertEqual(current["completed_items"], 0)
updated = repository.update_price_review_item(
envelope.job_id,
current["items"][0]["item_id"],
str(case_id),
current["revision"],
"0",
"operator@example.test",
)
self.assertEqual(updated["items"][0]["real_price"], "0")
plan = repository.begin_price_review_generation(
envelope.job_id,
str(case_id),
updated["revision"],
"operator@example.test",
envelope.processor_version,
envelope.rule_set_sha256,
"1" * 64,
)
self.assertEqual(plan.status, "ready")
self.assertEqual(plan.attempt_no, 2)
self.assertEqual(plan.source, envelope.artifacts["source_xml"])
self.assertIsNotNone(plan.manual_override_bytes)
self.assertIsNotNone(plan.manual_override_sha256)
with self.assertRaisesRegex(IngestionError, "已冻结"):
repository.update_price_review_item(
envelope.job_id,
current["items"][0]["item_id"],
str(case_id),
updated["revision"],
"1",
"operator@example.test",
)
manifest = json.loads((plan.manual_override_bytes or b"{}").decode("utf-8"))
self.assertEqual(manifest["overrides"][0]["real_price"], "0.00")
final_raw, final_store, _final_values = build_delivery(
source,
root / "final",
manual_override=manifest,
review_job_id=envelope.job_id,
review_case_id=str(case_id),
)
final_envelope = json.loads(final_raw)
final_envelope["delivery_id"] = "delivery-002"
final_envelope["attempt_no"] = int(plan.attempt_no or 0)
final_raw = json.dumps(final_envelope, ensure_ascii=False).encode("utf-8")
final_service = IngestionService(DeliveryValidator(final_store, policy()), repository)
final = final_service.ingest(final_raw)
duplicate = final_service.ingest(final_raw)
self.assertEqual(final.status, "committed")
self.assertEqual(duplicate, final)
self.assertEqual(final.version_no, 1)
self.assertEqual(repository.current_version_id(date(2026, 7, 27)), final.daily_version_id)
self.assertEqual(
[record["pricing_method"] for record in repository.version_records(final.daily_version_id or 0)],
["price_reference_exact", "manual_review"],
)
completed = repository.get_price_review(envelope.job_id, 50, 0)
self.assertEqual(completed["case_status"], "completed")
def test_frozen_review_generation_failure_reuses_manifest_on_retry(self):
source = xml_document(reservation(1, rate_code="GRPA1", rate_amount="1800"))
with tempfile.TemporaryDirectory() as temp_dir:
raw, store, _values = build_delivery(source, Path(temp_dir) / "review")
repository = InMemoryIngestionRepository()
envelope = register(repository, raw)
outcome = IngestionService(DeliveryValidator(store, policy()), repository).ingest(raw)
review = repository.get_price_review(envelope.job_id, 50, 0)
updated = repository.update_price_review_item(
envelope.job_id,
review["items"][0]["item_id"],
str(outcome.review_case_id),
review["revision"],
"12",
"operator@example.test",
)
first = repository.begin_price_review_generation(
envelope.job_id,
str(outcome.review_case_id),
updated["revision"],
"operator@example.test",
envelope.processor_version,
envelope.rule_set_sha256,
"2" * 64,
)
repository.record_price_review_generation_failure(
envelope.job_id,
str(outcome.review_case_id),
int(first.attempt_no or 0),
"OBJECT_STORE_UNAVAILABLE",
)
retry = repository.begin_price_review_generation(
envelope.job_id,
str(outcome.review_case_id),
updated["revision"],
"operator@example.test",
envelope.processor_version,
envelope.rule_set_sha256,
"3" * 64,
)
self.assertEqual(retry.status, "ready")
self.assertGreater(int(retry.attempt_no or 0), int(first.attempt_no or 0))
self.assertEqual(retry.manual_override_bytes, first.manual_override_bytes)
self.assertEqual(retry.manual_override_sha256, first.manual_override_sha256)
def test_unknown_job_is_rejected_after_artifact_validation(self):
with tempfile.TemporaryDirectory() as temp_dir:
raw, store, _values = build_delivery(self.success_xml(), Path(temp_dir))