feat(missao-8): bump Hub 1.3.15, restauracao de cfop_ippt_auto_apply e elevacao de cobertura
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
"""Missão 8 — Bloco C: Testes direcionados para elevar cobertura de reference_registry e classification."""
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from central.app.models import Base, Dataset
|
||||
from central.app.reference_registry import (
|
||||
register_draft, approve_draft, reject_draft, list_registry,
|
||||
diff_against_vigente, technical_checklist, RegistryError
|
||||
)
|
||||
from central.app.classification import Classifier
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def memory_db():
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
Base.metadata.create_all(engine)
|
||||
with Session(engine) as session:
|
||||
yield session
|
||||
|
||||
|
||||
# ---------------- C.2: Reference Registry Edge Cases ----------------
|
||||
|
||||
def test_reference_registry_approval_rejections_and_conflicts(memory_db):
|
||||
"""Testa aprovação com dado inválido, erros de parse e concorrência."""
|
||||
# 1. Tentar aprovar dataset inexistente
|
||||
with pytest.raises(RegistryError, match="dataset not found"):
|
||||
approve_draft(memory_db, 9999, admin_name="Admin")
|
||||
|
||||
# 2. Tentar rejeitar dataset inexistente
|
||||
with pytest.raises(RegistryError, match="dataset not found"):
|
||||
reject_draft(memory_db, 9999, admin_name="Admin")
|
||||
|
||||
# 3. Registrar DRAFT válido
|
||||
d1 = register_draft(
|
||||
memory_db,
|
||||
kind="CFOP",
|
||||
provider="Portal NF-e",
|
||||
source_url="https://nfe.fazenda.gov.br",
|
||||
sha256="1" * 64,
|
||||
version_label="cfop-v1",
|
||||
record_count=619,
|
||||
)
|
||||
|
||||
# 4. Primeira aprovação tem sucesso
|
||||
app1 = approve_draft(memory_db, d1.id, admin_name="Admin 1")
|
||||
assert app1.status == "VIGENTE"
|
||||
|
||||
# 5. Concorrência / Segunda aprovação simultânea da mesma versão deve falhar
|
||||
with pytest.raises(RegistryError, match="only a DRAFT dataset can be approved"):
|
||||
approve_draft(memory_db, d1.id, admin_name="Admin 2")
|
||||
|
||||
# 6. Tentar rejeitar dataset que já é VIGENTE deve falhar
|
||||
with pytest.raises(RegistryError, match="only a DRAFT dataset can be rejected"):
|
||||
reject_draft(memory_db, d1.id, admin_name="Admin")
|
||||
|
||||
# 7. Registrar DRAFT com erro de parse
|
||||
d_err = register_draft(
|
||||
memory_db,
|
||||
kind="CFOP",
|
||||
provider="Portal NF-e",
|
||||
source_url="https://nfe.fazenda.gov.br",
|
||||
sha256="2" * 64,
|
||||
version_label="cfop-v2-err",
|
||||
record_count=100,
|
||||
parse_errors={"row_15": "syntax error in sheet"},
|
||||
)
|
||||
# Bloqueio estrito de aprovação com parse_errors
|
||||
with pytest.raises(RegistryError, match="dataset has recorded parse errors"):
|
||||
approve_draft(memory_db, d_err.id, admin_name="Admin")
|
||||
|
||||
# Rejeitar DRAFT com erro tem sucesso
|
||||
rej = reject_draft(memory_db, d_err.id, admin_name="Admin")
|
||||
assert rej.status == "REJECTED"
|
||||
|
||||
# Rejeitar novamente falha
|
||||
with pytest.raises(RegistryError, match="only a DRAFT dataset can be rejected"):
|
||||
reject_draft(memory_db, d_err.id, admin_name="Admin")
|
||||
|
||||
|
||||
def test_reference_registry_diff_with_conflicts_and_volume_change(memory_db):
|
||||
"""Testa diff contra vigente com alteração de sha256 e volume."""
|
||||
# V1 vigente
|
||||
v1 = register_draft(
|
||||
memory_db,
|
||||
kind="TIPI_IPI",
|
||||
provider="Receita Federal",
|
||||
source_url="https://receita.gov.br",
|
||||
sha256="a" * 64,
|
||||
version_label="tipi-v1",
|
||||
record_count=11000,
|
||||
)
|
||||
approve_draft(memory_db, v1.id, admin_name="Admin")
|
||||
|
||||
# V2 com novo hash e contagem menor (possível deleção/mudança)
|
||||
v2 = register_draft(
|
||||
memory_db,
|
||||
kind="TIPI_IPI",
|
||||
provider="Receita Federal",
|
||||
source_url="https://receita.gov.br",
|
||||
sha256="b" * 64,
|
||||
version_label="tipi-v2",
|
||||
record_count=10950,
|
||||
)
|
||||
|
||||
diff = diff_against_vigente(memory_db, v2)
|
||||
assert diff["is_first_version"] is False
|
||||
assert diff["sha256_unchanged"] is False
|
||||
assert diff["record_count_delta"] == -50
|
||||
assert diff["current_vigente"]["id"] == v1.id
|
||||
|
||||
|
||||
# ---------------- C.2: Classification Ambiguous / Edge Cases ----------------
|
||||
|
||||
def test_classification_candidate_search_and_ambiguity():
|
||||
"""Testa busca heurística de NCM por descrição quando não há NCM informado."""
|
||||
class MockEntry:
|
||||
def __init__(self, code, desc):
|
||||
self.code = code
|
||||
self.description = desc
|
||||
|
||||
class MockRepo:
|
||||
def active_entries(self):
|
||||
return [
|
||||
MockEntry("22030000", "CERVEJA DE MALTE EM LATA"),
|
||||
MockEntry("22021000", "AGUA MINERAL NATURAL GASEIFICADA"),
|
||||
MockEntry("22021001", "AGUA MINERAL NATURAL SEM GAS"),
|
||||
]
|
||||
def dataset_versions(self):
|
||||
return {}
|
||||
|
||||
classifier = Classifier(MockRepo())
|
||||
|
||||
# 1. Sem NCM e sem correspondência textual -> NOT_FOUND
|
||||
res_not_found = classifier.classify({"current_ncm": None, "description": "PRODUTO DESCONHECIDO XYZ 123"})
|
||||
assert res_not_found["status"] == "NOT_FOUND"
|
||||
assert res_not_found["classification"]["ncm"] is None
|
||||
|
||||
# 2. Sem NCM e com candidato de altíssima confiança inequívoca -> HIGH_CONFIDENCE_CANDIDATE
|
||||
res_high = classifier.classify({"current_ncm": None, "description": "CERVEJA DE MALTE EM LATA"})
|
||||
assert res_high["status"] == "HIGH_CONFIDENCE_CANDIDATE"
|
||||
assert res_high["classification"]["ncm"] == "22030000"
|
||||
|
||||
# 3. Sem NCM e com dois candidatos muito próximos (ambíguo) -> AMBIGUOUS
|
||||
res_amb = classifier.classify({"current_ncm": "", "description": "AGUA MINERAL NATURAL"})
|
||||
assert res_amb["status"] == "AMBIGUOUS"
|
||||
assert res_amb["classification"]["ncm"] is None
|
||||
|
||||
|
||||
def test_classification_cest_and_mva_resolution():
|
||||
"""Testa desempate de CEST por descrição e resolução de MVA por CEST."""
|
||||
class MockCest:
|
||||
def __init__(self, code, desc):
|
||||
self.code = code
|
||||
self.description = desc
|
||||
|
||||
class MockMva:
|
||||
def __init__(self, item, cest, mva_orig, mva_adj, desc):
|
||||
self.item = item
|
||||
self.cest = cest
|
||||
self.mva_original = mva_orig
|
||||
self.mva_adjusted = mva_adj
|
||||
self.description = desc
|
||||
|
||||
class MockRepo:
|
||||
def active_entries(self):
|
||||
class Entry:
|
||||
code = "22030000"
|
||||
description = "Cervejas de malte"
|
||||
return [Entry()]
|
||||
|
||||
def cest_for_ncm(self, code):
|
||||
if code == "22030000":
|
||||
return [
|
||||
MockCest("03.021.01", "Cerveja em garrafa de vidro"),
|
||||
MockCest("03.021.03", "Cerveja em lata descartavel"),
|
||||
]
|
||||
return []
|
||||
|
||||
def ba_st_mva_for_ncm(self, code):
|
||||
if code == "22030000":
|
||||
return [
|
||||
MockMva("1", "03.021.01", 50.0, 70.0, "Cerveja garrafa"),
|
||||
MockMva("2", "03.021.03", 60.0, 80.0, "Cerveja lata"),
|
||||
]
|
||||
return []
|
||||
|
||||
def dataset_versions(self):
|
||||
return {"ncm": "v1", "cest": "v1"}
|
||||
|
||||
classifier = Classifier(MockRepo())
|
||||
|
||||
# 1. Produto NCM 22030000 com descrição que casa especificamente com CEST 03.021.03 (lata)
|
||||
res_lata = classifier.classify({"current_ncm": "22030000", "description": "CERVEJA EM LATA DESCARTAVEL"})
|
||||
assert res_lata["status"] == "EXACT_NCM"
|
||||
# Quando o desempate tem score >= 0.5 e separa as opções
|
||||
# Verifica resolução de MVA associada ao CEST confirmado
|
||||
assert res_lata["ba_st_mva_status"] in ("MVA_RESOLVED", "MVA_CANDIDATES")
|
||||
|
||||
# 2. Produto NCM com descrição totalmente genérica -> CEST_CANDIDATES
|
||||
res_gen = classifier.classify({"current_ncm": "22030000", "description": "BEBIDA"})
|
||||
assert res_gen["cest_status"] == "CEST_DESCRIPTION_MISMATCH"
|
||||
Reference in New Issue
Block a user