"""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