"""Visible status, re-evaluation (panel / Reprocessar / Sincronizar / KB decision), operator attestation, deterministic name-only match, RTC != Enrichment.""" from sqlalchemy import select from sqlalchemy.orm import Session from central.app.enrichment import api as enrich_api, history, kb, status from central.app.enrichment.quota import ProviderGateway from central.app.enrichment.resolver import resolve from central.app.jobs import create_job from central.app.models import Agent, AuditEvent, Installation, KbKnowledge, KbProduct, KbPromotionAudit, ProductEnrichment from central.tests.test_control_panel_pages import _panel_client from central.tests.test_enrichment_history import G, run as seed_history, staging from central.tests.test_product_enrichment_api import H, _second_agent, resolve as api_resolve, setup from central.tests.test_product_enrichment_resolver import COSMOS, Fake, G1, env, gtin # noqa: F401 from central.tests.test_rtc_security import _become, _post def _kb_conflict(env): seed_history(env, [staging("RINCAO", G1, "19059090", years={2024: 900, 2025: 900}, desc="COCA LATA 350 ML"), staging("UNIAO", G1, "22021000", years={2024: 20, 2025: 13}, desc="REFRIGERANTE COCA COLA LATA 350 ML")]) def test_attestation_resolves_a_conflict_with_audit_and_only_among_candidates(env): _kb_conflict(env) cur = history.official_sets(env) p = env.scalar(select(KbProduct).where(KbProduct.gtin == G)) assert kb.evaluate(env, p, "ncm", lambda v: v in cur)["status"] == kb.CONFLICT for bad, why in (("21069090", "NCM_IS_NOT_A_CANDIDATE_OF_THIS_GTIN"), ("22030000x", "NCM_IS_NOT_A_CANDIDATE_OF_THIS_GTIN")): try: kb.attest(env, G, bad, "Rafael", lambda v: v in cur) assert False except ValueError as error: assert str(error) == why res = kb.attest(env, G, "22021000", "Rafael", lambda v: v in cur, note="lata 350ml = refrigerante") assert res["status"] == kb.VERIFIED_KB and res["value"] == "22021000" and set(res["families"]) == {"OPERATOR_ATTESTATION", "CLIENT_DATA"} audit = env.scalars(select(KbPromotionAudit)).one() assert audit.sources["operator"] == "Rafael" and audit.new_status == "VERIFIED_KB" rejected = env.scalar(select(KbKnowledge).where(KbKnowledge.knowledge_key == "ncm", KbKnowledge.value == "19059090")) assert rejected.status == kb.STALE and rejected.evidence["info"]["historical_state"] == "REJECTED_BY_OPERATOR" def test_name_only_match_is_deterministic_and_conservative(env): _kb_conflict(env) cur = history.official_sets(env) kb.attest(env, G, "22021000", "Rafael", lambda v: v in cur) zero = gtin("789490070001") seed_history(env, [staging("UNIAO", zero, "22021000", years={2024: 20, 2025: 13}, desc="REFRIGERANTE COCA COLA ZERO LATA 350 ML", code="9")]) env.commit() r = resolve(env, ProviderGateway(env, []), gtin_raw="", description="COCA-COLA ORIGINAL LATA 350 ML", current={"CLASSIFICACAO_FISCAL": "00000000"}) assert r["state"] == "EXACT_KB_NAME_VERIFIED" and r["fields"]["CLASSIFICACAO_FISCAL"]["status"] == "AUTO_FILL_SAFE" and r["fields"]["CLASSIFICACAO_FISCAL"]["value"] == "22021000" ambiguous = resolve(env, ProviderGateway(env, []), gtin_raw="", description="COCA COLA ZERO LATA 350 ML", current={}) assert ambiguous["fields"]["CLASSIFICACAO_FISCAL"]["status"] != "AUTO_FILL_SAFE" # ZERO is not a neutral extra word: no cross-match generic = resolve(env, ProviderGateway(env, []), gtin_raw="", description="REFRIGERANTE", current={}) assert generic["state"] == "AMBIGUOUS" assert not kb.alias_match(frozenset({"COCA", "COLA", "LATA", "350", "ML"}), frozenset({"COCA", "COLA", "ZERO", "LATA", "350", "ML"})) assert kb.alias_match(frozenset({"COCA", "COLA", "LATA", "350", "ML"}), frozenset({"REFRIGERANTE", "COCA", "COLA", "LATA", "350", "ML"})) def test_every_tracked_product_has_a_visible_status(monkeypatch): client, engine = setup(monkeypatch) out = api_resolve(client, "N1", "", "COCA-COLA ORIGINAL LATA 350 ML", {"CLASSIFICACAO_FISCAL": "00000000"}) with Session(engine) as s: row = s.scalars(select(ProductEnrichment)).one() assert row.ui_status == "EAN_AUSENTE" and row.reason_code == "NAME_ONLY_NO_VERIFIED_MATCH" and row.last_attempt_at is not None assert out["fields"]["CLASSIFICACAO_FISCAL"]["technical_placeholder"] == "00000000" generic = api_resolve(client, "N2", "", "REFRIGERANTE", {}) invalid = api_resolve(client, "N3", "7891000100104", "PRODUTO X", {}) with Session(engine) as s: st = {r.product_code: r.ui_status for r in s.scalars(select(ProductEnrichment))} assert st["N2"] == "NOME_AMBIGUO" and st["N3"] == "EAN_INVALIDO" assert all(status.LABELS.get(v) for v in st.values()) from central.tests.test_product_enrichment_resolver import G1 as g done = api_resolve(client, "N4", g, "REFRIGERANTE COLA LATA 350ML", {}) with Session(engine) as s: assert s.scalars(select(ProductEnrichment).where(ProductEnrichment.product_code == "N4")).one().ui_status == "PRONTO_PARA_PREENCHER" client.post("/v1/products/enrichment/applied", json={"product_code": "N4", "fields": [{"field": "CLASSIFICACAO_FISCAL", "outcome": "APPLIED", "old": "00000000", "new": "22021000"}]}, headers=H) client.post("/v1/products/enrichment/applied", json={"product_code": "N1", "fields": [{"field": "CLASSIFICACAO_FISCAL", "outcome": "ABORTED", "reason": "STALE_CONFLICT"}]}, headers=H) with Session(engine) as s: st = {r.product_code: (r.ui_status, r.reason_code) for r in s.scalars(select(ProductEnrichment))} assert st["N4"][0] == "ENRIQUECIDO" and st["N1"] == ("WRITE_FAILED", "STALE_CONFLICT") def test_reprocess_sync_and_panel_flag_only_unresolved_products_for_reevaluation(monkeypatch): client, engine = setup(monkeypatch) api_resolve(client, "U1", "", "COCA-COLA ORIGINAL LATA 350 ML", {"CLASSIFICACAO_FISCAL": "00000000"}) api_resolve(client, "E1", G1[:0] or gtin("789100010010"), "REFRIGERANTE COLA LATA 350ML", {}) client.post("/v1/products/enrichment/applied", json={"product_code": "E1", "fields": [{"field": "CLASSIFICACAO_FISCAL", "outcome": "APPLIED", "old": None, "new": "22021000"}]}, headers=H) assert client.get("/v1/products/enrichment/pending", headers=H).json() == {"codes": []} with Session(engine) as s: agent = s.scalars(select(Agent)).first() for kind in ("REPROCESS_ALL", "SYNC_INCREMENTAL"): create_job(s, agent, kind, {}) assert s.scalars(select(ProductEnrichment).where(ProductEnrichment.reevaluate.is_(True))).all()[0].product_code == "U1" for r in s.scalars(select(ProductEnrichment)): r.reevaluate = False s.commit() for j in s.scalars(select(__import__("central.app.models", fromlist=["Job"]).Job)): j.status = "COMPLETED" s.commit() with Session(engine) as s: create_job(s, s.scalars(select(Agent)).first(), "REPROCESS_PRODUCTS", {"codigos": ["U1"]}) assert client.get("/v1/products/enrichment/pending", headers=H).json() == {"codes": ["U1"]} # the enriched product E1 is never re-evaluated api_resolve(client, "U1", "", "COCA-COLA ORIGINAL LATA 350 ML", {"CLASSIFICACAO_FISCAL": "00000000"}) assert client.get("/v1/products/enrichment/pending", headers=H).json() == {"codes": []} # consumed by the new evaluation def test_panel_reevaluate_and_attest_routes_are_csrf_admin_and_audited(monkeypatch): client, agent_id, _ = _panel_client() with client._session_factory() as s: seed_history(s, [staging("RINCAO", G1, "19059090", years={2024: 900, 2025: 900}, desc="COCA LATA 350 ML"), staging("UNIAO", G1, "22021000", years={2024: 20, 2025: 13}, desc="REFRIGERANTE COCA COLA LATA 350 ML")]) from central.tests.test_product_enrichment_resolver import seed seed(s) i = s.scalars(select(Installation)).one() i.enrichment_mode, i.features_configured = "ENABLED", True s.commit() _become(client, "PADRAO") assert _post(client, "/kb/attest", gtin="0" + G1, ncm="22021000").status_code == 403 _become(client, "ADMIN") assert client.post("/kb/attest", data={"csrf_token": "bad", "gtin": "0" + G1, "ncm": "22021000"}, follow_redirects=False).status_code in (400, 403) assert _post(client, "/kb/attest", gtin="0" + G1, ncm="21069090").status_code == 422 assert _post(client, "/kb/attest", gtin="0" + G1, ncm="22021000").status_code == 303 assert _post(client, f"/agents/{agent_id}/enrichment/reevaluate", code="").status_code == 303 with client._session_factory() as s: acts = [e.action for e in s.scalars(select(AuditEvent))] assert "KB_NCM_ATTESTED" in acts and "KB_ATTEST_DENIED" in acts and "ENRICHMENT_REEVALUATE" in acts page = client.get("/?kb_q=COCA COLA LATA 350 ML").text assert "VERIFIED_KB" in page and "decidido por" in page def test_rtc_mode_and_enrichment_mode_are_independent_switches_with_distinct_labels(): client, agent_id, _ = _panel_client() _become(client, "ADMIN") assert _post(client, f"/agents/{agent_id}/features", enrichment_mode="DRY_RUN", corrector_enabled="1").status_code == 303 with client._session_factory() as s: i = s.scalars(select(Installation)).one() assert i.enrichment_mode == "DRY_RUN" and i.rtc_write_mode == "DISABLED" assert _post(client, f"/agents/{agent_id}/rtc-mode", mode="DRY_RUN").status_code == 303 assert _post(client, f"/agents/{agent_id}/rtc-mode", mode="ENABLED", confirm="PREENCHER").status_code == 303 with client._session_factory() as s: i = s.scalars(select(Installation)).one() assert i.rtc_write_mode == "ENABLED" and i.enrichment_mode == "DRY_RUN" # turning cClassTrib to Automatic never touches the enrichment page = client.get(f"/agents/{agent_id}").text assert "NÃO é o preenchimento RTC" in page and "Enriquecimento de produtos novos" in page