87 lines
5.2 KiB
Python
87 lines
5.2 KiB
Python
"""Human-facing status of every tracked product: nothing may silently 'do nothing'. Computed from the resolver result and refined when the Agent reports the write outcome."""
|
|
from __future__ import annotations
|
|
|
|
from sqlalchemy import select, update
|
|
|
|
LABELS = {
|
|
"ENRIQUECIDO": "Enriquecido: campos seguros preenchidos.",
|
|
"PRONTO_PARA_PREENCHER": "Evidência suficiente: aguardando a escrita segura pelo Agent.",
|
|
"SIMULADO": "Simulação (DRY_RUN): há candidato, nada foi gravado.",
|
|
"AGUARDANDO_EVIDENCIA": "Aguardando evidência: nenhuma fonte confiável ainda.",
|
|
"EAN_AUSENTE": "Sem EAN e sem produto único verificado na base: informe o código de barras ou peça a decisão de um administrador.",
|
|
"EAN_INVALIDO": "Código de barras inválido: nenhuma fonte externa foi consultada.",
|
|
"NOME_AMBIGUO": "Nome genérico ou ambíguo demais para identificar o produto com segurança.",
|
|
"SEM_MATCH_DE_PRODUTO": "Nenhum produto conhecido corresponde a este nome/EAN.",
|
|
"NCM_NAO_VERIFICADO": "Produto identificado, mas o NCM ainda não foi verificado (falta segunda fonte independente ou decisão de administrador).",
|
|
"CONFLITO_DE_NCM": "As fontes divergem sobre o NCM: nada é preenchido até um administrador decidir.",
|
|
"PROVIDER_SEM_QUOTA": "Limite diário das fontes externas atingido; será repetido após o reset.",
|
|
"PROVIDER_NAO_CONFIGURADO": "Nenhuma fonte externa com NCM está configurada.",
|
|
"AGUARDANDO_RETRY": "Fontes indisponíveis agora; nova tentativa automática agendada.",
|
|
"HUMAN_OVERRIDE": "Alterado por uma pessoa: nunca mais preenchido automaticamente.",
|
|
"TECHNICAL_PLACEHOLDER": "NCM provisório (00000000) reconhecido como 'ainda não classificado'.",
|
|
"WRITE_FAILED": "A escrita segura foi recusada ou falhou (veja o motivo).",
|
|
"VERIFICACAO_POS_WRITE_FALHOU": "A escrita foi feita mas a releitura não confirmou o valor.",
|
|
"NADA_A_PREENCHER": "Todos os campos elegíveis já estão preenchidos.",
|
|
"FUNCAO_DESATIVADA": "Enriquecimento desativado para esta instalação.",
|
|
}
|
|
|
|
|
|
def classify(result: dict, *, has_gtin: bool, specific_name: bool) -> tuple[str, str]:
|
|
"""(ui_status, reason_code) from a resolve() result."""
|
|
fields = result.get("fields") or {}
|
|
ncm = fields.get("CLASSIFICACAO_FISCAL") or {}
|
|
state = result.get("state")
|
|
if state == "FEATURE_DISABLED":
|
|
return "FUNCAO_DESATIVADA", state
|
|
if state == "INVALID_GTIN":
|
|
return "EAN_INVALIDO", state
|
|
if state == "PENDING_PROVIDER_QUOTA":
|
|
return "PROVIDER_SEM_QUOTA", state
|
|
if state in ("PENDING_PROVIDERS_OFFLINE", "PENDING_TIME_BUDGET"):
|
|
return "AGUARDANDO_RETRY", state
|
|
if state == "CONFLICT" or ncm.get("status") == "CONFLICT":
|
|
return "CONFLITO_DE_NCM", "CONFLICT"
|
|
if ncm.get("status") == "HUMAN_OVERRIDDEN":
|
|
return "HUMAN_OVERRIDE", "HUMAN_OVERRIDDEN"
|
|
if any(f.get("status") == "AUTO_FILL_SAFE" for f in fields.values()):
|
|
return "PRONTO_PARA_PREENCHER", state or "RESOLVED"
|
|
if ncm.get("status") == "ALREADY_SET" and not any(f.get("status") in ("AUTO_FILL_SAFE", "SUGGESTION_ONLY") for f in fields.values()):
|
|
return "NADA_A_PREENCHER", "ALL_FIELDS_SET"
|
|
if not has_gtin:
|
|
if state == "AMBIGUOUS" or not specific_name:
|
|
return "NOME_AMBIGUO", "GENERIC_OR_AMBIGUOUS_NAME"
|
|
return "EAN_AUSENTE", "NAME_ONLY_NO_VERIFIED_MATCH"
|
|
if ncm.get("status") == "SUGGESTION_ONLY":
|
|
return "NCM_NAO_VERIFICADO", ncm.get("rule") or "NCM_NOT_VERIFIED"
|
|
if state == "NOT_FOUND":
|
|
return "SEM_MATCH_DE_PRODUTO", "GTIN_UNKNOWN"
|
|
return "AGUARDANDO_EVIDENCIA", state or "INSUFFICIENT_EVIDENCE"
|
|
|
|
|
|
def after_write(outcomes: list[dict], mode: str) -> tuple[str | None, str | None]:
|
|
"""Refines the status with what the Agent reported (APPLIED / DRY_RUN / ABORTED / FAILED / HUMAN_OVERRIDDEN)."""
|
|
kinds = {o.get("outcome") for o in outcomes}
|
|
reasons = [o.get("reason") for o in outcomes if o.get("reason")]
|
|
if "HUMAN_OVERRIDDEN" in kinds and "APPLIED" not in kinds:
|
|
return "HUMAN_OVERRIDE", "HUMAN_OVERRIDDEN"
|
|
if "APPLIED" in kinds:
|
|
return "ENRIQUECIDO", None
|
|
if "FAILED" in kinds:
|
|
return ("VERIFICACAO_POS_WRITE_FALHOU" if "POST_WRITE_VERIFICATION_FAILED" in reasons else "WRITE_FAILED"), (reasons[0] if reasons else "FAILED")
|
|
if "ABORTED" in kinds:
|
|
return "WRITE_FAILED", (reasons[0] if reasons else "ABORTED")
|
|
if "DRY_RUN" in kinds:
|
|
return "SIMULADO", "DRY_RUN_NO_WRITE"
|
|
return None, None
|
|
|
|
|
|
def mark_for_reevaluation(session, installation_id: int, *, codes: list[str] | None = None, gtin: str | None = None) -> int:
|
|
"""Flags tracked, NOT-yet-enriched products for a new evaluation (KB/cache first; providers only when really needed). Idempotent; never touches enriched / human-overridden ones."""
|
|
from ..models import ProductEnrichment
|
|
q = update(ProductEnrichment).where(ProductEnrichment.installation_id == installation_id, ProductEnrichment.ui_status.notin_(["ENRIQUECIDO", "HUMAN_OVERRIDE"]) | ProductEnrichment.ui_status.is_(None))
|
|
if codes:
|
|
q = q.where(ProductEnrichment.product_code.in_(codes))
|
|
if gtin:
|
|
q = q.where(ProductEnrichment.gtin == gtin)
|
|
return session.execute(q.values(reevaluate=True)).rowcount or 0
|