feat: add daily manual price review workflow
This commit is contained in:
@@ -11,3 +11,5 @@ stdout/stderr, temporary paths and source bytes never enter the public response
|
||||
|
||||
Older remote-run, signature and callback modules remain as ARR1 compatibility/audit code. The ARR2 Web entrypoint does
|
||||
not import them, root requirements do not install their HTTP client, and Compose exposes no corresponding service.
|
||||
When that retired path is composed for audit or a historical callback, it uses its frozen v3 success/failure projection;
|
||||
it cannot enter `review_required` or accept an artificial price.
|
||||
|
||||
@@ -10,6 +10,7 @@ from typing import Any, Dict, Mapping, Optional
|
||||
|
||||
from arr_ingestion.contracts import (
|
||||
ARTIFACT_SIZE_LIMITS,
|
||||
LEGACY_RESULT_SCHEMA_VERSION,
|
||||
OPAQUE_ID_RE,
|
||||
PROCESSOR_VERSION_RE,
|
||||
RESULT_SCHEMA_VERSION,
|
||||
@@ -269,7 +270,7 @@ class ProcessingResult:
|
||||
_fail("PROCESSING_CONTRACT_INVALID", "failed result artifact shape is invalid")
|
||||
|
||||
result_schema_version = values.get("result_schema_version")
|
||||
if result_schema_version != RESULT_SCHEMA_VERSION:
|
||||
if result_schema_version != LEGACY_RESULT_SCHEMA_VERSION:
|
||||
_fail("PROCESSING_CONTRACT_INVALID", "result schema version is unsupported")
|
||||
return cls(
|
||||
delivery_id=_opaque(values.get("delivery_id"), "delivery identifier"),
|
||||
|
||||
@@ -37,9 +37,36 @@ class LocalDailyProcessor:
|
||||
if not (self.policy.skill_root / "scripts" / "process_daily.py").is_file():
|
||||
raise ValueError("ARR daily processor is unavailable")
|
||||
|
||||
def run(self, source_xml: Path, output_dir: Path) -> LocalProcessingOutput:
|
||||
def run(
|
||||
self,
|
||||
source_xml: Path,
|
||||
output_dir: Path,
|
||||
*,
|
||||
manual_overrides: Optional[Path] = None,
|
||||
review_job_id: Optional[str] = None,
|
||||
review_case_id: Optional[str] = None,
|
||||
manual_override_sha256: Optional[str] = None,
|
||||
) -> LocalProcessingOutput:
|
||||
if not source_xml.is_file():
|
||||
raise IngestionError("SOURCE_NOT_FOUND", "registered source XML is unavailable")
|
||||
manual_arguments = (
|
||||
manual_overrides,
|
||||
review_job_id,
|
||||
review_case_id,
|
||||
manual_override_sha256,
|
||||
)
|
||||
if any(value is not None for value in manual_arguments):
|
||||
if (
|
||||
manual_overrides is None
|
||||
or not manual_overrides.is_file()
|
||||
or not review_job_id
|
||||
or not review_case_id
|
||||
or not manual_override_sha256
|
||||
):
|
||||
raise IngestionError(
|
||||
"MANUAL_OVERRIDE_INVALID",
|
||||
"frozen manual-price overrides are unavailable",
|
||||
)
|
||||
output_dir.mkdir(parents=True, exist_ok=False, mode=0o700)
|
||||
result_path = output_dir / "result.json"
|
||||
structured_path = output_dir / "structured-result.json"
|
||||
@@ -55,6 +82,19 @@ class LocalDailyProcessor:
|
||||
"--structured-result-json",
|
||||
str(structured_path.resolve()),
|
||||
]
|
||||
if manual_overrides is not None:
|
||||
command.extend(
|
||||
(
|
||||
"--manual-override-json",
|
||||
str(manual_overrides.resolve()),
|
||||
"--review-job-id",
|
||||
str(review_job_id),
|
||||
"--review-case-id",
|
||||
str(review_case_id),
|
||||
"--manual-override-sha256",
|
||||
str(manual_override_sha256),
|
||||
)
|
||||
)
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
@@ -68,20 +108,22 @@ class LocalDailyProcessor:
|
||||
raise IngestionError(
|
||||
"PROCESSOR_TIMEOUT",
|
||||
"deterministic XML processing exceeded its time limit",
|
||||
retryable=True,
|
||||
) from None
|
||||
except OSError:
|
||||
raise IngestionError(
|
||||
"PROCESSOR_UNAVAILABLE",
|
||||
"deterministic XML processing could not start",
|
||||
retryable=True,
|
||||
) from None
|
||||
|
||||
result = strict_json_file(result_path, "processor result")
|
||||
structured = strict_json_file(structured_path, "structured processor result")
|
||||
status = result.get("status")
|
||||
if (
|
||||
status not in {"success", "failed"}
|
||||
status not in {"success", "failed", "review_required"}
|
||||
or structured.get("status") != status
|
||||
or (completed.returncode == 0) != (status == "success")
|
||||
or (completed.returncode == 0) != (status in {"success", "review_required"})
|
||||
):
|
||||
raise IngestionError(
|
||||
"PROCESSOR_RESULT_INVALID",
|
||||
@@ -121,6 +163,15 @@ class LocalDailyProcessor:
|
||||
raise IngestionError(
|
||||
"PROCESSOR_RESULT_INVALID", "successful processor output is incomplete"
|
||||
)
|
||||
elif status == "review_required":
|
||||
if (
|
||||
business_date is None
|
||||
or outputs.get("daily_report") is not None
|
||||
or outputs.get("exception_report") is not None
|
||||
):
|
||||
raise IngestionError(
|
||||
"PROCESSOR_RESULT_INVALID", "review processor output is inconsistent"
|
||||
)
|
||||
else:
|
||||
artifacts["exception_report"] = self._output_path(
|
||||
output_dir, outputs.get("exception_report"), ".xlsx"
|
||||
|
||||
@@ -9,6 +9,15 @@ from pathlib import Path
|
||||
from arr_ingestion.validation import ProcessorPolicy
|
||||
|
||||
|
||||
# This is the immutable identity of the retired direct-MCP/callback contract.
|
||||
# It is not a selectable active processor version: only the compatibility
|
||||
# replay path uses it, and that path has no review-required state.
|
||||
LEGACY_DIRECT_PROCESSOR_VERSION = "3.0.0"
|
||||
LEGACY_DIRECT_RULE_SET_SHA256 = (
|
||||
"c41257208324a43e711de13ec9776a5e6486db334757f152531bf8292a2018eb"
|
||||
)
|
||||
|
||||
|
||||
def load_processor_policy(project_root: Path) -> ProcessorPolicy:
|
||||
skill_root = (project_root / "arr-opera-daily-ingest").resolve()
|
||||
script = skill_root / "scripts" / "process_daily.py"
|
||||
@@ -33,3 +42,21 @@ def load_processor_policy(project_root: Path) -> ProcessorPolicy:
|
||||
rule_set_sha256=rule_set_sha256,
|
||||
skill_root=skill_root,
|
||||
)
|
||||
|
||||
|
||||
def load_legacy_direct_processor_policy(project_root: Path) -> ProcessorPolicy:
|
||||
"""Return the frozen v3 identity for retired direct-MCP replay only.
|
||||
|
||||
The active package supplies a narrowly scoped ``--legacy-v3-output``
|
||||
compatibility replay. Keeping this identity separate prevents the ARR2
|
||||
XML upload path from accepting or producing a v3 review-less result.
|
||||
"""
|
||||
|
||||
active = load_processor_policy(project_root)
|
||||
return ProcessorPolicy(
|
||||
processor_version=LEGACY_DIRECT_PROCESSOR_VERSION,
|
||||
rule_set_sha256=LEGACY_DIRECT_RULE_SET_SHA256,
|
||||
skill_root=active.skill_root,
|
||||
python_binary=active.python_binary,
|
||||
legacy_direct_v3=True,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user