feat: add daily manual price review workflow
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -10,6 +13,8 @@ from arr_ingestion.contracts import ArtifactRef, IngestionError
|
||||
from arr_ingestion.postgres import (
|
||||
DatabaseConfig,
|
||||
PostgresIngestionRepository,
|
||||
_manual_price,
|
||||
_review_price_text,
|
||||
)
|
||||
from arr_ingestion.repository import JobRegistration
|
||||
from tests.test_arr_opera_daily_ingest import run_processor, success_xml
|
||||
@@ -99,9 +104,15 @@ class RetryConnection:
|
||||
|
||||
|
||||
class LifecycleCursor:
|
||||
def __init__(self, run_status: str = "queued", attempt_status: str = "queued") -> None:
|
||||
def __init__(
|
||||
self,
|
||||
run_status: str = "queued",
|
||||
attempt_status: str = "queued",
|
||||
active_review: tuple[int, int] | None = None,
|
||||
) -> None:
|
||||
self.run_status = run_status
|
||||
self.attempt_status = attempt_status
|
||||
self.active_review = active_review
|
||||
self.last_query = ""
|
||||
self.executed: list[tuple[str, tuple[object, ...]]] = []
|
||||
|
||||
@@ -113,9 +124,61 @@ class LifecycleCursor:
|
||||
def fetchone(self):
|
||||
if self.last_query.startswith("SELECT run.id, run.run_status"):
|
||||
return (12, self.run_status, 13, self.attempt_status)
|
||||
if "FROM ingestion.daily_review_cases" in self.last_query:
|
||||
return self.active_review
|
||||
return None
|
||||
|
||||
|
||||
class ReviewGenerationCursor:
|
||||
def __init__(self) -> None:
|
||||
self.last_query = ""
|
||||
self.executed: list[tuple[str, tuple[object, ...]]] = []
|
||||
self.case_key = "dailyreview-0123456789abcdef0123456789abcdef"
|
||||
self.source_sha256 = "c" * 64
|
||||
|
||||
def execute(self, query: str, params: tuple[object, ...]) -> None:
|
||||
InsertCursor.assert_placeholder_count(query, params)
|
||||
self.last_query = " ".join(query.split())
|
||||
self.executed.append((self.last_query, params))
|
||||
|
||||
def fetchone(self):
|
||||
if self.last_query.startswith("SELECT review.id, review.case_key"):
|
||||
return (
|
||||
33,
|
||||
self.case_key,
|
||||
"open",
|
||||
1,
|
||||
date(2026, 7, 27),
|
||||
"4.0.0",
|
||||
"b" * 64,
|
||||
self.source_sha256,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
44,
|
||||
"awaiting_review",
|
||||
"4.0.0",
|
||||
"b" * 64,
|
||||
)
|
||||
if self.last_query.startswith("SELECT COALESCE(max(attempt_no), 0) + 1"):
|
||||
return (3,)
|
||||
if "FROM ingestion.processing_runs AS run" in self.last_query:
|
||||
return (
|
||||
"opera_xml",
|
||||
"arr/jobs/job-001/attempts/0001/committed/source_xml/source.xml",
|
||||
"source.xml",
|
||||
self.source_sha256,
|
||||
321,
|
||||
"application/xml",
|
||||
)
|
||||
return None
|
||||
|
||||
def fetchall(self):
|
||||
if "FROM ingestion.daily_review_items" in self.last_query:
|
||||
return [("QBD", "GRPA1", Decimal("1800.00"), Decimal("0.00"))]
|
||||
return []
|
||||
|
||||
|
||||
class RegistrationCursor:
|
||||
def __init__(self) -> None:
|
||||
self.last_query = ""
|
||||
@@ -166,6 +229,16 @@ class PostgresIngestionTests(unittest.TestCase):
|
||||
"postgresql://synthetic",
|
||||
)
|
||||
|
||||
def test_manual_review_accepts_integer_input_and_keeps_exact_decimal_storage(self):
|
||||
self.assertEqual(_manual_price("0"), Decimal("0.00"))
|
||||
self.assertEqual(_manual_price("2300"), Decimal("2300.00"))
|
||||
self.assertEqual(_review_price_text(Decimal("2300.00")), "2300")
|
||||
for invalid in ("0.00", "01", "-1", "1.5", ""):
|
||||
with self.subTest(invalid=invalid), self.assertRaisesRegex(
|
||||
IngestionError, "非负整数"
|
||||
):
|
||||
_manual_price(invalid)
|
||||
|
||||
def test_record_insert_maps_duplicate_lineage_and_booking_source_status(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
exit_code, _xml, _output, _result, payload = run_processor(
|
||||
@@ -382,6 +455,86 @@ class PostgresIngestionTests(unittest.TestCase):
|
||||
self.assertIn("PROCESSOR_TIMEOUT", rendered_params)
|
||||
self.assertNotIn("password", rendered_params.lower())
|
||||
|
||||
def test_deterministic_review_runtime_failure_closes_case_with_audit_event(self):
|
||||
cursor = LifecycleCursor(
|
||||
run_status="running",
|
||||
attempt_status="running",
|
||||
active_review=(81, 4),
|
||||
)
|
||||
repository = PostgresIngestionRepository(DatabaseConfig("postgresql://synthetic"))
|
||||
|
||||
repository._record_runtime_failure(
|
||||
cursor,
|
||||
"arrjob-programmatic-003",
|
||||
2,
|
||||
"PROCESSOR_RESULT_INVALID",
|
||||
)
|
||||
|
||||
statements = "\n".join(query for query, _params in cursor.executed)
|
||||
self.assertIn("case_status = 'failed'", statements)
|
||||
self.assertIn("PRICE_REVIEW_FAILED", repr(cursor.executed))
|
||||
self.assertIn("system:runtime", repr(cursor.executed))
|
||||
|
||||
def test_finalization_plan_reloads_registered_source_artifact(self):
|
||||
cursor = ReviewGenerationCursor()
|
||||
repository = PostgresIngestionRepository(DatabaseConfig("postgresql://synthetic"))
|
||||
|
||||
plan = repository._begin_price_review_generation(
|
||||
cursor,
|
||||
"job-001",
|
||||
cursor.case_key,
|
||||
1,
|
||||
"operator@example.test",
|
||||
"4.0.0",
|
||||
"b" * 64,
|
||||
"d" * 64,
|
||||
)
|
||||
|
||||
self.assertEqual(plan.status, "ready")
|
||||
self.assertEqual(plan.attempt_no, 3)
|
||||
self.assertIsNotNone(plan.source)
|
||||
assert plan.source is not None
|
||||
self.assertEqual(plan.source.role, "source_xml")
|
||||
self.assertEqual(plan.source.sha256, cursor.source_sha256)
|
||||
self.assertEqual(plan.source.object_key.rsplit("/", 1)[-1], "source.xml")
|
||||
self.assertIsNotNone(plan.manual_override_bytes)
|
||||
manifest = json.loads(plan.manual_override_bytes or b"{}")
|
||||
self.assertEqual(manifest["source_sha256"], cursor.source_sha256)
|
||||
self.assertEqual(manifest["overrides"][0]["real_price"], "0.00")
|
||||
statements = "\n".join(query for query, _params in cursor.executed)
|
||||
self.assertIn("FOR SHARE OF artifact", statements)
|
||||
self.assertIn("PRICE_REVIEW_FINALIZED", repr(cursor.executed))
|
||||
|
||||
def test_review_receipt_is_separate_from_finance_and_outbox_writes(self):
|
||||
source = PostgresIngestionRepository._record_review.__code__.co_consts
|
||||
rendered = "\n".join(value for value in source if isinstance(value, str))
|
||||
self.assertIn("delivery_status = 'recorded_review'", rendered)
|
||||
self.assertNotIn("finance.daily_versions", rendered)
|
||||
self.assertNotIn("ingestion.outbox_events", rendered)
|
||||
|
||||
def test_delivery_insert_binds_manual_override_artifact_column(self):
|
||||
source = PostgresIngestionRepository._commit.__code__.co_consts
|
||||
delivery_insert = next(
|
||||
value
|
||||
for value in source
|
||||
if isinstance(value, str)
|
||||
and "INSERT INTO ingestion.processing_deliveries" in value
|
||||
)
|
||||
|
||||
self.assertIn("manual_override_artifact_id", delivery_insert)
|
||||
self.assertEqual(delivery_insert.count("%s"), 16)
|
||||
|
||||
def test_finance_version_insert_matches_bound_finalization_values(self):
|
||||
source = PostgresIngestionRepository._commit_success.__code__.co_consts
|
||||
version_insert = next(
|
||||
value
|
||||
for value in source
|
||||
if isinstance(value, str)
|
||||
and "INSERT INTO finance.daily_versions" in value
|
||||
)
|
||||
|
||||
self.assertEqual(version_insert.count("%s"), 20)
|
||||
|
||||
|
||||
class Migration008ContractTests(unittest.TestCase):
|
||||
def test_rebuild_targets_v3_and_all_source_record_storage(self):
|
||||
|
||||
Reference in New Issue
Block a user