git-subtree-dir: fiscal git-subtree-mainline:d95165fc8fgit-subtree-split:102ba84500
142 lines
9.8 KiB
Python
142 lines
9.8 KiB
Python
"""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
|