feat: add daily manual price review workflow
This commit is contained in:
@@ -10,7 +10,13 @@ from typing import Any, Dict, Mapping, Optional
|
||||
|
||||
|
||||
DELIVERY_SCHEMA_VERSION = "1.0"
|
||||
RESULT_SCHEMA_VERSION = "3.0"
|
||||
# v4 is the active fixed-processor contract. v3 remains parseable only for
|
||||
# historical/direct-MCP compatibility and must never acquire review behavior.
|
||||
LEGACY_RESULT_SCHEMA_VERSION = "3.0"
|
||||
RESULT_SCHEMA_VERSION = "4.0"
|
||||
SUPPORTED_RESULT_SCHEMA_VERSIONS = frozenset(
|
||||
{LEGACY_RESULT_SCHEMA_VERSION, RESULT_SCHEMA_VERSION}
|
||||
)
|
||||
XLSX_MIME = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
|
||||
OPAQUE_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")
|
||||
@@ -41,7 +47,9 @@ ARTIFACT_ROLES = {
|
||||
"result_json",
|
||||
"structured_result_json",
|
||||
"exception_report",
|
||||
"manual_override_json",
|
||||
}
|
||||
LEGACY_ARTIFACT_ROLES = ARTIFACT_ROLES - {"manual_override_json"}
|
||||
ROLE_CONTRACTS = {
|
||||
"source_xml": ("opera_xml", ".xml", "application/xml"),
|
||||
"booking_source": ("booking_excel", ".xlsx", XLSX_MIME),
|
||||
@@ -55,6 +63,11 @@ ROLE_CONTRACTS = {
|
||||
"application/json",
|
||||
),
|
||||
"exception_report": ("exception_xlsx", ".xlsx", XLSX_MIME),
|
||||
"manual_override_json": (
|
||||
"manual_override_json",
|
||||
".json",
|
||||
"application/json",
|
||||
),
|
||||
}
|
||||
ARTIFACT_SIZE_LIMITS = {
|
||||
"source_xml": 100 * 1024 * 1024,
|
||||
@@ -65,9 +78,18 @@ ARTIFACT_SIZE_LIMITS = {
|
||||
"result_json": 5 * 1024 * 1024,
|
||||
"structured_result_json": 50 * 1024 * 1024,
|
||||
"exception_report": 20 * 1024 * 1024,
|
||||
"manual_override_json": 5 * 1024 * 1024,
|
||||
}
|
||||
|
||||
|
||||
def artifact_roles_for_schema(result_schema_version: str) -> set[str]:
|
||||
if result_schema_version == LEGACY_RESULT_SCHEMA_VERSION:
|
||||
return set(LEGACY_ARTIFACT_ROLES)
|
||||
if result_schema_version == RESULT_SCHEMA_VERSION:
|
||||
return set(ARTIFACT_ROLES)
|
||||
_fail("DELIVERY_INVALID", "result schema version is unsupported")
|
||||
|
||||
|
||||
class IngestionError(RuntimeError):
|
||||
"""A stable, non-sensitive validation or persistence error."""
|
||||
|
||||
@@ -206,8 +228,6 @@ class DeliveryEnvelope:
|
||||
_fail("DELIVERY_INVALID", "job identifier is invalid")
|
||||
if not isinstance(attempt_no, int) or isinstance(attempt_no, bool) or attempt_no < 1:
|
||||
_fail("DELIVERY_INVALID", "attempt number is invalid")
|
||||
if status not in {"success", "failed"}:
|
||||
_fail("DELIVERY_INVALID", "delivery status is invalid")
|
||||
if (
|
||||
not isinstance(processor_version, str)
|
||||
or not PROCESSOR_VERSION_RE.fullmatch(processor_version)
|
||||
@@ -215,8 +235,13 @@ class DeliveryEnvelope:
|
||||
_fail("DELIVERY_INVALID", "processor version is invalid")
|
||||
if not isinstance(rule_set_sha256, str) or not SHA256_RE.fullmatch(rule_set_sha256):
|
||||
_fail("DELIVERY_INVALID", "rule set identity is invalid")
|
||||
if result_schema_version != RESULT_SCHEMA_VERSION:
|
||||
if result_schema_version not in SUPPORTED_RESULT_SCHEMA_VERSIONS:
|
||||
_fail("DELIVERY_INVALID", "result schema version is unsupported")
|
||||
allowed_statuses = {"success", "failed"}
|
||||
if result_schema_version == RESULT_SCHEMA_VERSION:
|
||||
allowed_statuses.add("review_required")
|
||||
if status not in allowed_statuses:
|
||||
_fail("DELIVERY_INVALID", "delivery status is invalid")
|
||||
|
||||
raw_business_date = values.get("business_date")
|
||||
parsed_date: Optional[date]
|
||||
@@ -230,9 +255,10 @@ class DeliveryEnvelope:
|
||||
else:
|
||||
_fail("DELIVERY_INVALID", "business date is invalid")
|
||||
|
||||
raw_artifacts = _strict_mapping(values.get("artifacts"), ARTIFACT_ROLES, "artifact")
|
||||
artifact_roles = artifact_roles_for_schema(str(result_schema_version))
|
||||
raw_artifacts = _strict_mapping(values.get("artifacts"), artifact_roles, "artifact")
|
||||
artifacts: Dict[str, Optional[ArtifactRef]] = {}
|
||||
for role in sorted(ARTIFACT_ROLES):
|
||||
for role in sorted(artifact_roles):
|
||||
raw_artifact = raw_artifacts.get(role)
|
||||
artifacts[role] = (
|
||||
None if raw_artifact is None else ArtifactRef.from_dict(role, raw_artifact)
|
||||
@@ -240,6 +266,7 @@ class DeliveryEnvelope:
|
||||
always_required = ("source_xml", "result_json", "structured_result_json")
|
||||
if any(artifacts[role] is None for role in always_required):
|
||||
_fail("DELIVERY_INVALID", "required delivery artifact is missing")
|
||||
manual_override = artifacts.get("manual_override_json")
|
||||
if status == "success":
|
||||
if (
|
||||
parsed_date is None
|
||||
@@ -247,8 +274,18 @@ class DeliveryEnvelope:
|
||||
or artifacts["exception_report"] is not None
|
||||
):
|
||||
_fail("DELIVERY_INVALID", "success delivery artifact shape is invalid")
|
||||
elif status == "review_required":
|
||||
if (
|
||||
parsed_date is None
|
||||
or artifacts["daily_report"] is not None
|
||||
or artifacts["exception_report"] is not None
|
||||
or manual_override is not None
|
||||
):
|
||||
_fail("DELIVERY_INVALID", "review delivery artifact shape is invalid")
|
||||
elif artifacts["daily_report"] is not None or artifacts["exception_report"] is None:
|
||||
_fail("DELIVERY_INVALID", "failed delivery artifact shape is invalid")
|
||||
elif manual_override is not None:
|
||||
_fail("DELIVERY_INVALID", "failed delivery artifact shape is invalid")
|
||||
return cls(
|
||||
delivery_id=delivery_id,
|
||||
job_id=job_id,
|
||||
|
||||
Reference in New Issue
Block a user