fix: harden Flow v2 authoritative cutover semantics
This commit is contained in:
@@ -48,7 +48,8 @@ class AuthoritativeOperationalDecision:
|
||||
"reason_if_blocked": self.reason_code if not self.eligible else None,
|
||||
"operational_queue": self.queue,
|
||||
"authoritative_v2": True,
|
||||
"suppress_current_card": self.queue == "not_current",
|
||||
"is_duplicate_representation": self.reason_code == "DUPLICATE_SUPPRESSED",
|
||||
"suppress_current_card": self.reason_code == "DUPLICATE_SUPPRESSED",
|
||||
})
|
||||
return result
|
||||
|
||||
@@ -78,7 +79,8 @@ def _active_obligations(obligations: Iterable[Mapping[str, Any]]) -> list[Mappin
|
||||
|
||||
def _ref(row: Mapping[str, Any]) -> dict[str, Any]:
|
||||
return {"source": "task", "id": str(row.get("id") or ""), "status": "pending",
|
||||
"action_code": _code(row.get("action_code"))}
|
||||
"action_code": _code(row.get("action_code")),
|
||||
"source_system": str(row.get("source_system") or "")}
|
||||
|
||||
|
||||
def decide_authoritative_operation(
|
||||
@@ -110,6 +112,10 @@ def decide_authoritative_operation(
|
||||
oid, canonical, state, business_action, hard_blocker, "blocked", True,
|
||||
"HARD_FACTUAL_BLOCKER", "Um bloqueio factual ou de sistema impede a ação atual.", hard_blocker, confidence=confidence,
|
||||
)
|
||||
# Terminal factual state cannot be reopened by a leftover operational task.
|
||||
if state in TERMINAL_STATES:
|
||||
return AuthoritativeOperationalDecision(oid, canonical, state, business_action, None, "not_current", False,
|
||||
"TERMINAL_BUSINESS_STATE", "O processo factual está concluído.", confidence=confidence)
|
||||
|
||||
active = _active_obligations(obligations)
|
||||
by_code: dict[str, list[Mapping[str, Any]]] = {}
|
||||
@@ -143,7 +149,8 @@ def decide_authoritative_operation(
|
||||
|
||||
picked = choose(("REVIEW_MANUALLY", "REVIEW_REQUIRED", "REVIEW", "REVIEW_RECONSTRUCTED_PROCESS"), "review", "ACTIVE_REVIEW_OBLIGATION")
|
||||
picked = picked or choose(("SUPPORT",), "do_now", "ACTIVE_SUPPORT_OBLIGATION")
|
||||
picked = picked or choose(("SEND_INFO",), "do_now", "ACTIVE_RESPONSE_OBLIGATION")
|
||||
if state == "INQUIRY" or business_action == "SEND_INFO":
|
||||
picked = picked or choose(("SEND_INFO",), "do_now", "ACTIVE_RESPONSE_OBLIGATION")
|
||||
picked = picked or choose(("CALL_CUSTOMER",), "do_now", "EXPLICIT_CALL_OBLIGATION")
|
||||
if picked:
|
||||
return picked
|
||||
@@ -168,9 +175,6 @@ def decide_authoritative_operation(
|
||||
obligation_source_refs=tuple(_ref(row) for row in rows), confidence=confidence,
|
||||
)
|
||||
|
||||
if state in TERMINAL_STATES:
|
||||
return AuthoritativeOperationalDecision(oid, canonical, state, business_action, None, "not_current", False,
|
||||
"TERMINAL_BUSINESS_STATE", "O processo factual está concluído.", confidence=confidence)
|
||||
if business_action:
|
||||
queue = "review" if business_action in REVIEW_ACTIONS else "do_now"
|
||||
return AuthoritativeOperationalDecision(oid, canonical, state, business_action, business_action, queue, True,
|
||||
|
||||
@@ -408,6 +408,7 @@ def _opportunity_item(
|
||||
"business_next_action": decision.get("business_next_action"),
|
||||
"effective_action": decision.get("effective_action"),
|
||||
"canonical_opportunity_id": decision.get("canonical_opportunity_id") or opportunity_id,
|
||||
"material_process_key": decision.get("material_process_key"),
|
||||
})
|
||||
return item, independent_exceptions
|
||||
|
||||
|
||||
@@ -38,6 +38,53 @@ def _int(value: Any) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def _authoritative_material_identity(item: Dict[str, Any]) -> str:
|
||||
return str(item.get("material_process_key") or item.get("canonical_opportunity_id")
|
||||
or item.get("process_key") or item.get("work_item_key") or item.get("id"))
|
||||
|
||||
|
||||
def _collapse_authoritative_material_processes(items: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""Enforce one current card per material process without deleting evidence."""
|
||||
current_queues = {"do_now", "review", "blocked", "exception"}
|
||||
action_rank = {
|
||||
"REVIEW_MANUALLY": 10, "REVIEW_RECONSTRUCTED_PROCESS": 11, "REVIEW_REQUIRED": 12,
|
||||
"SUPPORT": 20, "SEND_INFO": 30, "CALL_CUSTOMER": 40,
|
||||
"FOLLOW_UP_CUSTOMER_REVIEW": 50, "FOLLOW_UP_PAYMENT": 51,
|
||||
}
|
||||
groups: Dict[str, List[Dict[str, Any]]] = {}
|
||||
noncurrent: List[Dict[str, Any]] = []
|
||||
for item in items:
|
||||
if item.get("operational_queue") not in current_queues:
|
||||
noncurrent.append(item)
|
||||
continue
|
||||
groups.setdefault(_authoritative_material_identity(item), []).append(item)
|
||||
selected: List[Dict[str, Any]] = []
|
||||
for identity, group in groups.items():
|
||||
winner = min(group, key=lambda row: (
|
||||
0 if row.get("operational_queue") in {"blocked", "exception"} else 1,
|
||||
action_rank.get(str(row.get("action_code") or ""), 100),
|
||||
str(row.get("work_item_key") or ""),
|
||||
))
|
||||
merged = dict(winner)
|
||||
if len(group) > 1:
|
||||
merged["suppressed_competing_actions"] = [
|
||||
{"work_item_key": row.get("work_item_key"), "action_code": row.get("action_code"),
|
||||
"source_refs": row.get("source_refs") or []}
|
||||
for row in group if row is not winner
|
||||
]
|
||||
refs, seen = list(merged.get("source_refs") or []), set()
|
||||
for ref in refs:
|
||||
seen.add((str(ref.get("source")), str(ref.get("id"))))
|
||||
for row in group:
|
||||
for ref in row.get("source_refs") or []:
|
||||
key = (str(ref.get("source")), str(ref.get("id")))
|
||||
if key not in seen:
|
||||
refs.append(ref); seen.add(key)
|
||||
merged["source_refs"] = refs
|
||||
selected.append(merged)
|
||||
return selected + noncurrent
|
||||
|
||||
|
||||
def _is_manually_resolved_error(value: Any) -> bool:
|
||||
text_value = str(value or "").strip().casefold()
|
||||
return "limpo manualmente" in text_value or "resolvido manualmente" in text_value
|
||||
@@ -722,6 +769,8 @@ def get_operations_summary(
|
||||
) if opportunity_ids else {})
|
||||
projection = canonicalize_operations(normalized_rows, decisions, evidence_rows=[dict(row) for row in evidence_rows])
|
||||
canonical_items = _attach_operation_urls(list(projection["items"]))
|
||||
if active_flow_mode == "authoritative":
|
||||
canonical_items = _collapse_authoritative_material_processes(canonical_items)
|
||||
partition = partition_canonical_items(canonical_items, display_limit=display_limit)
|
||||
actionable_items = partition["actionable_items"]
|
||||
all_waiting_items = partition["waiting_items"]
|
||||
@@ -750,7 +799,7 @@ def get_operations_summary(
|
||||
"backlog_total": partition["backlog_total"],
|
||||
"not_current_total": partition["not_current_total"],
|
||||
}
|
||||
return {
|
||||
result = {
|
||||
"counts": cleaned_counts,
|
||||
"recent_outbox": [dict(r) for r in recent_outbox],
|
||||
"recent_documents": [dict(r) for r in recent_documents],
|
||||
|
||||
@@ -481,8 +481,8 @@ def _get_authoritative_opportunity_decisions(ids: list[str], *, connection: Any
|
||||
FROM opportunity_flow_state_v2 WHERE opportunity_id::text IN :opportunity_ids
|
||||
""", ids)
|
||||
tasks = _bulk_rows(conn, """
|
||||
SELECT id::text, opportunity_id::text, action_code, status, due_at,
|
||||
resolved_at, superseded_by_task_id::text, created_at, metadata
|
||||
SELECT id::text, opportunity_id::text, action_code, status, due_at, source_system,
|
||||
resolved_at, resolution_code, superseded_by_task_id::text, created_at, metadata
|
||||
FROM tasks WHERE opportunity_id::text IN :opportunity_ids AND status = 'pending'
|
||||
""", ids)
|
||||
customers = _bulk_rows(conn, """
|
||||
@@ -514,10 +514,31 @@ def _get_authoritative_opportunity_decisions(ids: list[str], *, connection: Any
|
||||
reconciliation_blocking=oid in reconciliation_ids,
|
||||
)
|
||||
value = decision.to_dict()
|
||||
winning_ids = {str(ref.get("id") or "") for ref in value.get("obligation_source_refs") or []}
|
||||
obligation_audit = []
|
||||
for task in tasks_by_id.get(oid, []):
|
||||
task_id = str(task.get("id") or "")
|
||||
if task_id in winning_ids:
|
||||
classification = "VALID_ACTIVE_OBLIGATION"
|
||||
elif task.get("superseded_by_task_id"):
|
||||
classification = "SUPERSEDED_OBLIGATION"
|
||||
elif task.get("resolved_at") or task.get("resolution_code"):
|
||||
classification = "SATISFIED_OBLIGATION"
|
||||
elif ((task_code := str(task.get("action_code") or "").upper()).startswith("SEND_")
|
||||
and task_code != "SEND_INFO") or task_code == "CREATE_JASMIN_QUOTE":
|
||||
classification = "PREMATURE_OR_STALE_OBLIGATION"
|
||||
else:
|
||||
classification = "STALE_LEGACY_OBLIGATION"
|
||||
obligation_audit.append({
|
||||
"id": task_id, "action_code": task.get("action_code"), "status": task.get("status"),
|
||||
"source_system": task.get("source_system"), "classification": classification,
|
||||
})
|
||||
value["obligation_audit_refs"] = obligation_audit
|
||||
if projection is None:
|
||||
value["opportunity_id"] = oid
|
||||
value["target_url"] = f"/opportunities/{oid}"
|
||||
else:
|
||||
value["material_process_key"] = projection.get("material_process_key")
|
||||
value["target_url"] = f"/opportunities/{decision.canonical_opportunity_id or oid}"
|
||||
decisions[oid] = value
|
||||
return decisions
|
||||
|
||||
Reference in New Issue
Block a user