Implement document reconciliation v2

This commit is contained in:
plx
2026-08-05 23:55:33 +00:00
parent 4a60607503
commit 29382ec6c1
25 changed files with 2082 additions and 343 deletions

View File

@@ -0,0 +1,119 @@
"""Pure classification and action building for Document Reconciliation v2 backfills."""
from __future__ import annotations
from collections import Counter, defaultdict
from typing import Any, Dict, Iterable, List
def select_complete_group_batch(rows: Iterable[Dict[str, Any]], batch_size: int,
resume_from: tuple[str, str] | None = None) -> List[Dict[str, Any]]:
"""Reference group paginator used by tests and non-SQL callers."""
ordered = sorted(rows, key=lambda row: (str(row["opportunity_id"]),
str(row["document_kind"]), str(row.get("created_at") or ""), str(row["id"])))
keys = []
for row in ordered:
key = (str(row["opportunity_id"]), str(row["document_kind"]))
if resume_from and key <= resume_from:
continue
if key not in keys:
if len(keys) >= max(1, int(batch_size)):
continue
keys.append(key)
selected = set(keys)
return [row for row in ordered if (str(row["opportunity_id"]), str(row["document_kind"])) in selected]
def _document_identity(row: Dict[str, Any]) -> str:
return str(row.get("external_id") or row.get("document_number") or row["id"])
def _is_cancelled(row: Dict[str, Any]) -> bool:
return str(row.get("status") or "").upper() in {"CANCELLED", "CANCELED"}
def classify_group(rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Classify documents from one opportunity and document-kind group."""
active_primaries = [
row
for row in rows
if bool(row.get("is_primary"))
and str(row.get("role") or "").lower() in {"current", "accepted"}
]
duplicate_identity = len({_document_identity(row) for row in rows}) < len(rows)
ambiguous = len(active_primaries) > 1 or duplicate_identity
actions = []
for row in rows:
role = str(row.get("role") or "").lower()
evidence = row.get("payload") if isinstance(row.get("payload"), dict) else {}
if ambiguous:
relationship, reason = "REVIEW_REQUIRED", "multiple primaries or duplicate association"
elif _is_cancelled(row) and row in active_primaries:
relationship, reason = "REVIEW_REQUIRED", "cancelled document was inferred primary"
elif len(active_primaries) == 1 and row["id"] == active_primaries[0]["id"]:
relationship, reason = "PRIMARY", "single unambiguous legacy primary"
elif role == "historical":
relationship, reason = "HISTORICAL", "explicit legacy historical role"
elif role in {"related", "accepted"}:
relationship, reason = "SECONDARY", "explicit non-primary related/accepted role"
elif role == "detached" and any(
evidence.get(key)
for key in ("manual_unlinked_from_opportunity_id", "removed_reason", "detached_evidence")
):
relationship, reason = "REMOVED", "explicit legacy detach evidence"
else:
relationship, reason = "REVIEW_REQUIRED", "legacy evidence is not unambiguous"
actions.append(
{
**row,
"relationship": relationship,
"decision_reason": reason,
"cancelled": _is_cancelled(row),
}
)
return actions
def build_backfill_result(
rows: Iterable[Dict[str, Any]],
*,
lines_without_document: int = 0,
documents_in_multiple_opportunities: int = 0,
) -> Dict[str, Any]:
"""Build backfill actions and summary from already-loaded document rows."""
groups: Dict[tuple, List[Dict[str, Any]]] = defaultdict(list)
for row in rows:
groups[(row["opportunity_id"], row["document_kind"])].append(row)
actions = [action for group in groups.values() for action in classify_group(group)]
counts = Counter(action["relationship"] for action in actions)
current_total = sum(
float(action.get("total_amount") or action.get("amount") or 0) for action in actions
)
primary_total = sum(
float(action.get("total_amount") or action.get("amount") or 0)
for action in actions
if action["relationship"] == "PRIMARY"
)
duplicates = sum(
1
for group in groups.values()
if len({_document_identity(row) for row in group}) < len(group)
)
return {
"summary": {
"total_opportunities": len({action["opportunity_id"] for action in actions}),
"total_documents": len(actions),
"auto_PRIMARY": counts["PRIMARY"],
"auto_SECONDARY": counts["SECONDARY"],
"auto_HISTORICAL": counts["HISTORICAL"],
"REVIEW_REQUIRED": counts["REVIEW_REQUIRED"],
"cancelled": sum(1 for action in actions if action["cancelled"]),
"duplicates": duplicates,
"conflicts": counts["REVIEW_REQUIRED"],
"lines_without_document": lines_without_document,
"documents_in_multiple_opportunities": documents_in_multiple_opportunities,
"current_total": current_total,
"primary_total": primary_total,
},
"actions": actions,
}