feat: add daily manual price review workflow
This commit is contained in:
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user