187 lines
9.2 KiB
Python
187 lines
9.2 KiB
Python
"""Authoritative BLIF Flow v2 -> operator decision adapter.
|
|
|
|
The persisted projection owns factual business state. Pending tasks are only
|
|
operational obligations and may override that state when this policy proves
|
|
that they are current. This module is deliberately pure and performs no I/O.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import asdict, dataclass
|
|
from datetime import datetime, timezone
|
|
from typing import Any, Iterable, Mapping
|
|
|
|
from app.admin_ui.labels import action_label
|
|
|
|
|
|
FORMAL_ACTIONS = {"CREATE_PROFORMA", "CREATE_INVOICE"}
|
|
REVIEW_ACTIONS = {"REVIEW", "REVIEW_MANUALLY", "REVIEW_REQUIRED", "REVIEW_RECONSTRUCTED_PROCESS"}
|
|
PAYMENT_FOLLOWUPS = {"FOLLOW_UP_PAYMENT", "FOLLOW_UP_PROFORMA"}
|
|
VALID_OVERRIDES = REVIEW_ACTIONS | {"SUPPORT", "SEND_INFO", "CALL_CUSTOMER", "FOLLOW_UP_CUSTOMER_REVIEW"} | PAYMENT_FOLLOWUPS
|
|
TERMINAL_STATES = {"COMPLETED", "LOST", "NO_INTEREST"}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class AuthoritativeOperationalDecision:
|
|
opportunity_id: str
|
|
canonical_opportunity_id: str
|
|
business_state: str
|
|
business_next_action: str | None
|
|
effective_action: str | None
|
|
queue: str
|
|
eligible: bool
|
|
reason_code: str
|
|
reason_text: str
|
|
blocking_action_code: str | None = None
|
|
obligation_source_refs: tuple[dict[str, Any], ...] = ()
|
|
confidence: str = "high"
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
result = asdict(self)
|
|
result["obligation_source_refs"] = list(self.obligation_source_refs)
|
|
# Shared legacy presentation contract consumed by Operations and detail.
|
|
result.update({
|
|
"action_code": self.effective_action or "NO_ACTION",
|
|
"label": action_label(self.effective_action, "Sem ação"),
|
|
"description": self.reason_text,
|
|
"priority": "alta" if self.queue in {"review", "blocked", "exception"} else "normal",
|
|
"can_execute": self.eligible and self.effective_action is not None,
|
|
"reason_if_blocked": self.reason_code if not self.eligible else None,
|
|
"operational_queue": self.queue,
|
|
"authoritative_v2": True,
|
|
"is_duplicate_representation": self.reason_code == "DUPLICATE_SUPPRESSED",
|
|
"suppress_current_card": self.reason_code == "DUPLICATE_SUPPRESSED",
|
|
})
|
|
return result
|
|
|
|
|
|
def _code(value: Any) -> str:
|
|
return str(value or "").strip().upper()
|
|
|
|
|
|
def _dt(value: Any) -> datetime | None:
|
|
if not value:
|
|
return None
|
|
if isinstance(value, datetime):
|
|
parsed = value
|
|
else:
|
|
try:
|
|
parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
|
|
except ValueError:
|
|
return None
|
|
return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)
|
|
|
|
|
|
def _active_obligations(obligations: Iterable[Mapping[str, Any]]) -> list[Mapping[str, Any]]:
|
|
return [row for row in obligations
|
|
if str(row.get("status") or "").lower() == "pending"
|
|
and not row.get("resolved_at") and not row.get("superseded_by_task_id")]
|
|
|
|
|
|
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")),
|
|
"source_system": str(row.get("source_system") or "")}
|
|
|
|
|
|
def decide_authoritative_operation(
|
|
projection: Mapping[str, Any] | None,
|
|
*, obligations: Iterable[Mapping[str, Any]] = (), now: datetime | None = None,
|
|
fiscal_complete: bool = True, reconciliation_blocking: bool = False,
|
|
hard_blocker: str | None = None,
|
|
) -> AuthoritativeOperationalDecision:
|
|
"""Return the sole operator decision, failing closed without a projection."""
|
|
now = now or datetime.now(timezone.utc)
|
|
if projection is None:
|
|
return AuthoritativeOperationalDecision(
|
|
"", "", "MISSING_PROJECTION", None, "REVIEW_REQUIRED", "review", True,
|
|
"MISSING_V2_PROJECTION", "A projeção Flow v2 está em falta; é necessária revisão, sem recorrer ao V1.",
|
|
confidence="low",
|
|
)
|
|
oid = str(projection.get("opportunity_id") or "")
|
|
canonical = str(projection.get("canonical_opportunity_id") or oid)
|
|
state = _code(projection.get("business_state"))
|
|
business_action = _code(projection.get("business_next_action")) or None
|
|
confidence = str(projection.get("confidence") or "low")
|
|
if projection.get("is_duplicate_representation"):
|
|
return AuthoritativeOperationalDecision(
|
|
oid, canonical, state, business_action, None, "not_current", False,
|
|
"DUPLICATE_SUPPRESSED", f"Representação duplicada do processo material canónico {canonical}.", confidence="high",
|
|
)
|
|
if hard_blocker:
|
|
return AuthoritativeOperationalDecision(
|
|
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]]] = {}
|
|
for row in active:
|
|
by_code.setdefault(_code(row.get("action_code")), []).append(row)
|
|
|
|
# A formal-document prerequisite blocks only a transition which needs it.
|
|
if business_action in FORMAL_ACTIONS and not fiscal_complete:
|
|
return AuthoritativeOperationalDecision(
|
|
oid, canonical, state, business_action, "VALIDATE_FISCAL_CUSTOMER", "blocked", True,
|
|
"FISCAL_IDENTITY_REQUIRED", "Validar os dados fiscais antes de criar o documento oficial.",
|
|
"VALIDATE_FISCAL_CUSTOMER", confidence=confidence,
|
|
)
|
|
if business_action in FORMAL_ACTIONS and reconciliation_blocking:
|
|
return AuthoritativeOperationalDecision(
|
|
oid, canonical, state, business_action, "RECONCILE_DOCUMENTS", "blocked", True,
|
|
"DOCUMENT_RECONCILIATION_REQUIRED", "Confirmar a ligação do documento formal atual.",
|
|
"RECONCILE_DOCUMENTS", confidence=confidence,
|
|
)
|
|
|
|
def choose(codes: Iterable[str], queue: str, reason: str):
|
|
for code in codes:
|
|
rows = by_code.get(code, [])
|
|
if rows:
|
|
return AuthoritativeOperationalDecision(
|
|
oid, canonical, state, business_action, code, queue, True, reason,
|
|
"Existe uma obrigação operacional pendente e válida.",
|
|
obligation_source_refs=tuple(_ref(row) for row in rows), confidence=confidence,
|
|
)
|
|
return None
|
|
|
|
picked = choose(("REVIEW_MANUALLY", "REVIEW_REQUIRED", "REVIEW", "REVIEW_RECONSTRUCTED_PROCESS"), "review", "ACTIVE_REVIEW_OBLIGATION")
|
|
picked = picked or choose(("SUPPORT",), "do_now", "ACTIVE_SUPPORT_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
|
|
|
|
waiting_state = state in {"AWAITING_CUSTOMER", "AWAITING_PAYMENT"}
|
|
followup_order = ("FOLLOW_UP_CUSTOMER_REVIEW",) if state == "AWAITING_CUSTOMER" else tuple(PAYMENT_FOLLOWUPS)
|
|
if waiting_state:
|
|
for code in followup_order:
|
|
rows = by_code.get(code, [])
|
|
if not rows:
|
|
continue
|
|
due_rows = [row for row in rows if _dt(row.get("due_at")) is None or _dt(row.get("due_at")) <= now]
|
|
if due_rows:
|
|
return AuthoritativeOperationalDecision(
|
|
oid, canonical, state, business_action, code, "do_now", True, "DUE_FOLLOW_UP",
|
|
"O follow-up ativo chegou à data e continua por satisfazer.",
|
|
obligation_source_refs=tuple(_ref(row) for row in due_rows), confidence=confidence,
|
|
)
|
|
return AuthoritativeOperationalDecision(
|
|
oid, canonical, state, business_action, None, "waiting", False, "FOLLOW_UP_NOT_DUE",
|
|
"O follow-up ativo ainda não chegou à data.",
|
|
obligation_source_refs=tuple(_ref(row) for row in rows), 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,
|
|
"BUSINESS_NEXT_ACTION", str(projection.get("reason_text") or "A ação decorre do estado factual Flow v2."), confidence=confidence)
|
|
if waiting_state:
|
|
return AuthoritativeOperationalDecision(oid, canonical, state, None, None, "waiting", False,
|
|
"WAITING_EXTERNAL_EVENT", "O processo aguarda um evento externo.", confidence=confidence)
|
|
return AuthoritativeOperationalDecision(oid, canonical, state, None, None, "backlog", False,
|
|
"NO_CURRENT_INTERNAL_ACTION", "Não existe ação interna atual.", confidence=confidence)
|