feat(bloco-a): higiene alembic heads e inventario dos 9 datasets no registry
This commit is contained in:
@@ -0,0 +1,185 @@
|
|||||||
|
"""Missão 6 / Bloco A.2: Inventário dos 9 datasets existentes no Hub e migração para o Reference Data Registry.
|
||||||
|
|
||||||
|
Datasets catalogados:
|
||||||
|
1. NCM (Siscomex)
|
||||||
|
2. TIPI_IPI (Receita Federal)
|
||||||
|
3. CEST (CONFAZ)
|
||||||
|
4. BA_ST_MVA (SEFAZ-BA)
|
||||||
|
5. NFE_MOC_TABLES (Receita Federal / ENCAT)
|
||||||
|
6. CFOP (Portal Nacional NF-e)
|
||||||
|
7. FCP (Portal Nacional NF-e)
|
||||||
|
8. ANP (Portal Nacional NF-e)
|
||||||
|
9. NCM_UTRIB (Portal Nacional NF-e)
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
from sqlalchemy import select, update
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from ..models import Dataset
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class DatasetInventoryItem:
|
||||||
|
kind: str
|
||||||
|
name: str
|
||||||
|
provider: str
|
||||||
|
source_url: str
|
||||||
|
version_label: str
|
||||||
|
expected_rows_approx: int
|
||||||
|
act: str
|
||||||
|
validity_info: str
|
||||||
|
coverage: str
|
||||||
|
licence_terms: str
|
||||||
|
raw_file_name: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
DATASET_INVENTORY_9: tuple[DatasetInventoryItem, ...] = (
|
||||||
|
DatasetInventoryItem(
|
||||||
|
kind="NCM",
|
||||||
|
name="Nomenclatura Comum do Mercosul (NCM)",
|
||||||
|
provider="Siscomex",
|
||||||
|
source_url="https://portalunico.siscomex.gov.br/classif/api/publico/nomenclatura/download/json",
|
||||||
|
version_label="2026-09-28",
|
||||||
|
expected_rows_approx=15156,
|
||||||
|
act="Resolução Gecex / MDIC",
|
||||||
|
validity_info="Vigente em 2026-09-28",
|
||||||
|
coverage="Códigos NCM de 8 dígitos, descrições oficiais, datas de início e fim de vigência nacional.",
|
||||||
|
licence_terms="Dado público oficial (Governo Federal / Siscomex); sem termos de uso restritivos conhecidos.",
|
||||||
|
raw_file_name="ncm_vigente_20260928.json",
|
||||||
|
),
|
||||||
|
DatasetInventoryItem(
|
||||||
|
kind="TIPI_IPI",
|
||||||
|
name="Tabela de Incidência do IPI (TIPI)",
|
||||||
|
provider="Receita Federal",
|
||||||
|
source_url="https://www.gov.br/receitafederal/pt-br/assuntos/orientacao-tributaria/tributos-e-receitas/tipi",
|
||||||
|
version_label="tipi-xlsx-v1",
|
||||||
|
expected_rows_approx=11028,
|
||||||
|
act="Decreto nº 11.158/2022 e alterações até ADE 001-2026",
|
||||||
|
validity_info="Vigente desde 2022 com atualizações até 2026",
|
||||||
|
coverage="Alíquotas de IPI (percentuais ou NT) por NCM de 8 dígitos e código de exceção EX.",
|
||||||
|
licence_terms="Dado público oficial (Receita Federal do Brasil); dados abertos governamentais.",
|
||||||
|
raw_file_name="1 TIPI 2022 - Atualizada ADE 001-2026.xlsx",
|
||||||
|
),
|
||||||
|
DatasetInventoryItem(
|
||||||
|
kind="CEST",
|
||||||
|
name="Código Especificador da Substituição Tributária (CEST)",
|
||||||
|
provider="CONFAZ",
|
||||||
|
source_url="https://www.confaz.fazenda.gov.br/legislacao/convenios/2018/CV142_18",
|
||||||
|
version_label="CV142_18",
|
||||||
|
expected_rows_approx=1096,
|
||||||
|
act="Convênio ICMS 142/18",
|
||||||
|
validity_info="Vigente com modificações sucessivas",
|
||||||
|
coverage="Relação de CEST (7 dígitos), segmentos (Anexos II a XXVI) e códigos NCM aplicáveis à ST.",
|
||||||
|
licence_terms="Dado público oficial (Conselho Nacional de Política Fazendária - CONFAZ).",
|
||||||
|
raw_file_name="CV142_18.html",
|
||||||
|
),
|
||||||
|
DatasetInventoryItem(
|
||||||
|
kind="BA_ST_MVA",
|
||||||
|
name="Pauta Fiscal e Margens de Valor Agregado ST da Bahia",
|
||||||
|
provider="SEFAZ-BA",
|
||||||
|
source_url="http://www.sefaz.ba.gov.br/legislacao/ricms/anexo1.pdf",
|
||||||
|
version_label="ba-st-mva-pdf-v1",
|
||||||
|
expected_rows_approx=757,
|
||||||
|
act="RICMS/BA Anexo 1 (Decreto nº 13.780/2012)",
|
||||||
|
validity_info="Vigente em 2026 na Bahia",
|
||||||
|
coverage="Produtos sujeitos a ICMS-ST no Estado da Bahia, NCM/SH, CEST e percentuais de MVA original/ajustada.",
|
||||||
|
licence_terms="Legislação tributária pública estadual (SEFAZ-BA).",
|
||||||
|
raw_file_name="decreto_2012_13780_ricms_anexo_1_vigente_2026.pdf",
|
||||||
|
),
|
||||||
|
DatasetInventoryItem(
|
||||||
|
kind="NFE_MOC_TABLES",
|
||||||
|
name="Tabelas de Domínios do MOC da NF-e / NFC-e",
|
||||||
|
provider="Receita Federal / ENCAT",
|
||||||
|
source_url="http://www.nfe.fazenda.gov.br/portal/principal.aspx",
|
||||||
|
version_label="moc7-tables-v1",
|
||||||
|
expected_rows_approx=104,
|
||||||
|
act="MOC 7.0 Anexo I (Visão Geral de Leiaute e Regras de Validação)",
|
||||||
|
validity_info="Vigente nacionalmente",
|
||||||
|
coverage="Domínios oficiais de ORIGEM_MERCADORIA (9), CST_ICMS (14), CSOSN (10), CST_PIS (36) e CST_COFINS (36).",
|
||||||
|
licence_terms="Documento técnico público oficial (ENCAT / Portal Nacional NF-e).",
|
||||||
|
raw_file_name="moc7-anexo-i-leiaute-e-rv.pdf",
|
||||||
|
),
|
||||||
|
DatasetInventoryItem(
|
||||||
|
kind="CFOP",
|
||||||
|
name="Tabela de Códigos Fiscais de Operações e Prestações (CFOP)",
|
||||||
|
provider="Portal Nacional NF-e",
|
||||||
|
source_url="https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=Iy/5QolvUsQ=",
|
||||||
|
version_label="cfop-xlsx-v1",
|
||||||
|
expected_rows_approx=593,
|
||||||
|
act="Informe Técnico 2023.002 v2.10",
|
||||||
|
validity_info="Vigente desde 2023",
|
||||||
|
coverage="CFOP de 4 dígitos, vigências, indicadores operacionais (devolução, anulação, remessa, combustível) e descrições.",
|
||||||
|
licence_terms="Dado público oficial (Portal Nacional da NF-e).",
|
||||||
|
raw_file_name="IT_2023_002_v2_10_Tabela_CFOP_Descrição.xlsx",
|
||||||
|
),
|
||||||
|
DatasetInventoryItem(
|
||||||
|
kind="FCP",
|
||||||
|
name="Tabela de Alíquotas de FCP por UF",
|
||||||
|
provider="Portal Nacional NF-e",
|
||||||
|
source_url="https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=Iy/5QolvUsQ=",
|
||||||
|
version_label="fcp-xlsx-v1",
|
||||||
|
expected_rows_approx=27,
|
||||||
|
act="Tabela de Alíquotas FCP por UF",
|
||||||
|
validity_info="Vigente (fevereiro/2025)",
|
||||||
|
coverage="Alíquotas adicionais do Fundo de Combate à Pobreza para os 26 estados e Distrito Federal.",
|
||||||
|
licence_terms="Dado público oficial (Portal Nacional da NF-e).",
|
||||||
|
raw_file_name="Tabela_Alíquotas_FCP_por_UF_2025-02_07.xlsx",
|
||||||
|
),
|
||||||
|
DatasetInventoryItem(
|
||||||
|
kind="ANP",
|
||||||
|
name="Tabela de Códigos de Produto ANP",
|
||||||
|
provider="Portal Nacional NF-e",
|
||||||
|
source_url="https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=Iy/5QolvUsQ=",
|
||||||
|
version_label="anp-xlsx-v1",
|
||||||
|
expected_rows_approx=184,
|
||||||
|
act="Tabela de Código de Produto ANP",
|
||||||
|
validity_info="Vigente (novembro/2024)",
|
||||||
|
coverage="Códigos cProdANP e descrições oficiais de combustíveis e derivados de petróleo regulados pela ANP.",
|
||||||
|
licence_terms="Dado público oficial (ANP / Portal Nacional da NF-e).",
|
||||||
|
raw_file_name="Tabela_Codigo_Produto_ANP-20241118.xlsx",
|
||||||
|
),
|
||||||
|
DatasetInventoryItem(
|
||||||
|
kind="NCM_UTRIB",
|
||||||
|
name="Tabela de Correlação NCM x Unidade Tributável",
|
||||||
|
provider="Portal Nacional NF-e",
|
||||||
|
source_url="https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=Iy/5QolvUsQ=",
|
||||||
|
version_label="ncm-utrib-xlsx-v1",
|
||||||
|
expected_rows_approx=10374,
|
||||||
|
act="Informe Técnico 2024.001 - NCM x uTrib",
|
||||||
|
validity_info="Vigente a partir de 01.10.2026 / 2024",
|
||||||
|
coverage="Mapeamento de cada NCM de 8 dígitos para a respectiva Unidade Tributável (uTrib) padrão nacional.",
|
||||||
|
licence_terms="Dado público oficial (Portal Nacional da NF-e).",
|
||||||
|
raw_file_name="IT2024.001v2.40 - Tabela NCM 2022 com Utrib_Comércio Exterior_vigência 01.10.26.xlsx",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sync_inventory_to_registry(session: Session) -> dict[str, int]:
|
||||||
|
"""Sincroniza os datasets existentes com as metainformações do Reference Data Registry.
|
||||||
|
|
||||||
|
Garante que os datasets já ativos no Hub (ACTIVE ou VIGENTE) possuam:
|
||||||
|
- licence_terms preenchidos
|
||||||
|
- act oficial correto
|
||||||
|
- raw_file_path preenchido quando disponível
|
||||||
|
- visibilidade no Registry sem criar segunda fonte ou duplicar linhas.
|
||||||
|
"""
|
||||||
|
inventory_map = {item.kind: item for item in DATASET_INVENTORY_9}
|
||||||
|
updated = 0
|
||||||
|
active_datasets = session.scalars(select(Dataset).where(Dataset.status.in_(("ACTIVE", "VIGENTE")))).all()
|
||||||
|
for ds in active_datasets:
|
||||||
|
inv = inventory_map.get(ds.kind)
|
||||||
|
if inv:
|
||||||
|
changed = False
|
||||||
|
if not ds.licence_terms:
|
||||||
|
ds.licence_terms = inv.licence_terms
|
||||||
|
changed = True
|
||||||
|
if not ds.act:
|
||||||
|
ds.act = inv.act
|
||||||
|
changed = True
|
||||||
|
if inv.raw_file_name and not ds.raw_file_path:
|
||||||
|
ds.raw_file_path = f"sources/raw/bootstrap_v1/{inv.raw_file_name}"
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
updated += 1
|
||||||
|
session.commit()
|
||||||
|
return {"active_found": len(active_datasets), "updated": updated}
|
||||||
@@ -12,7 +12,8 @@ from sqlalchemy import select, update
|
|||||||
|
|
||||||
from .models import Dataset
|
from .models import Dataset
|
||||||
|
|
||||||
STATUSES = ("DRAFT", "APPROVED", "VIGENTE", "OBSOLETO", "REJECTED")
|
STATUSES = ("DRAFT", "APPROVED", "VIGENTE", "ACTIVE", "OBSOLETO", "SUPERSEDED", "REJECTED")
|
||||||
|
VIGENTE_STATUSES = ("VIGENTE", "ACTIVE")
|
||||||
|
|
||||||
|
|
||||||
class RegistryError(Exception):
|
class RegistryError(Exception):
|
||||||
@@ -43,7 +44,7 @@ def diff_against_vigente(session, dataset: Dataset) -> dict:
|
|||||||
"""A summary an ADMIN reviews before approving: what the current VIGENTE of the same kind looks
|
"""A summary an ADMIN reviews before approving: what the current VIGENTE of the same kind looks
|
||||||
like vs this DRAFT - never the row-by-row content (that lives in the kind-specific entry tables),
|
like vs this DRAFT - never the row-by-row content (that lives in the kind-specific entry tables),
|
||||||
just the registry-level facts that matter for an approval decision."""
|
just the registry-level facts that matter for an approval decision."""
|
||||||
current = session.scalar(select(Dataset).where(Dataset.kind == dataset.kind, Dataset.status == "VIGENTE"))
|
current = session.scalar(select(Dataset).where(Dataset.kind == dataset.kind, Dataset.status.in_(VIGENTE_STATUSES)).order_by(Dataset.id.desc()))
|
||||||
return {
|
return {
|
||||||
"kind": dataset.kind,
|
"kind": dataset.kind,
|
||||||
"draft": {"id": dataset.id, "version_label": dataset.version_label, "sha256": dataset.sha256, "record_count": dataset.record_count,
|
"draft": {"id": dataset.id, "version_label": dataset.version_label, "sha256": dataset.sha256, "record_count": dataset.record_count,
|
||||||
@@ -67,7 +68,7 @@ def approve_draft(session, dataset_id: int, *, admin_name: str) -> Dataset:
|
|||||||
raise RegistryError(f"only a DRAFT dataset can be approved (current status: {dataset.status})")
|
raise RegistryError(f"only a DRAFT dataset can be approved (current status: {dataset.status})")
|
||||||
if dataset.parse_errors:
|
if dataset.parse_errors:
|
||||||
raise RegistryError("dataset has recorded parse errors - re-submit a clean version")
|
raise RegistryError("dataset has recorded parse errors - re-submit a clean version")
|
||||||
session.execute(update(Dataset).where(Dataset.kind == dataset.kind, Dataset.status == "VIGENTE").values(status="OBSOLETO"))
|
session.execute(update(Dataset).where(Dataset.kind == dataset.kind, Dataset.status.in_(VIGENTE_STATUSES)).values(status="OBSOLETO"))
|
||||||
dataset.status = "VIGENTE"
|
dataset.status = "VIGENTE"
|
||||||
dataset.approved_by = admin_name
|
dataset.approved_by = admin_name
|
||||||
dataset.approved_at = datetime.now(timezone.utc)
|
dataset.approved_at = datetime.now(timezone.utc)
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
"""Missão 6 — Bloco A: Testes de Higiene (Migrations e Inventário dos 9 Datasets)."""
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from alembic.config import Config
|
||||||
|
from alembic.script import ScriptDirectory
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
def test_alembic_master_head_is_single():
|
||||||
|
"""Garante que na branch master o alembic possui uma única head: 0034_reference_data_registry."""
|
||||||
|
cfg = Config(str(ROOT / "central" / "alembic.ini"))
|
||||||
|
script = ScriptDirectory.from_config(cfg)
|
||||||
|
heads = script.get_heads()
|
||||||
|
assert heads == ["0034_reference_data_registry"], f"Esperado head única 0034, obtido: {heads}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_feature_branch_migration_chains_to_0034():
|
||||||
|
"""Garante que na branch feature/cfop-ippt-write a migração 0035 encadeia exatamente em 0034."""
|
||||||
|
# Obter conteúdo de 0035 na branch feature/cfop-ippt-write via git show
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "show", "feature/cfop-ippt-write:central/alembic/versions/0035_cfop_ippt_write_mode.py"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
cwd=str(ROOT),
|
||||||
|
)
|
||||||
|
assert result.returncode == 0, f"Arquivo 0035_cfop_ippt_write_mode.py deve existir na branch feature/cfop-ippt-write: {result.stderr}"
|
||||||
|
content = result.stdout
|
||||||
|
assert 'revision = "0035_cfop_ippt_write_mode"' in content
|
||||||
|
assert 'down_revision = "0034_reference_data_registry"' in content
|
||||||
|
|
||||||
|
# Verifica que 0033 não existe mais na branch
|
||||||
|
res_old = subprocess.run(
|
||||||
|
["git", "show", "feature/cfop-ippt-write:central/alembic/versions/0033_cfop_ippt_write_mode.py"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
cwd=str(ROOT),
|
||||||
|
)
|
||||||
|
assert res_old.returncode != 0, "Migração 0033 não deve mais existir na branch feature"
|
||||||
|
|
||||||
|
|
||||||
|
def test_inventory_9_datasets_coverage():
|
||||||
|
"""Garante que os 9 datasets existentes no Hub estão integralmente catalogados."""
|
||||||
|
from central.app.datasets.inventory import DATASET_INVENTORY_9
|
||||||
|
kinds = [item.kind for item in DATASET_INVENTORY_9]
|
||||||
|
expected_kinds = ["NCM", "TIPI_IPI", "CEST", "BA_ST_MVA", "NFE_MOC_TABLES", "CFOP", "FCP", "ANP", "NCM_UTRIB"]
|
||||||
|
assert kinds == expected_kinds
|
||||||
|
for item in DATASET_INVENTORY_9:
|
||||||
|
assert item.name
|
||||||
|
assert item.provider
|
||||||
|
assert item.source_url.startswith("http")
|
||||||
|
assert item.version_label
|
||||||
|
assert item.expected_rows_approx > 0
|
||||||
|
assert item.act
|
||||||
|
assert item.validity_info
|
||||||
|
assert item.coverage
|
||||||
|
assert item.licence_terms
|
||||||
|
|
||||||
|
|
||||||
|
def test_sync_inventory_to_registry_and_listing():
|
||||||
|
"""Garante que datasets ACTIVE são reconhecidos pelo Reference Data Registry."""
|
||||||
|
from central.app.datasets.inventory import sync_inventory_to_registry
|
||||||
|
from central.app.reference_registry import list_registry, diff_against_vigente
|
||||||
|
from central.app.models import Dataset
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from central.app.models import Base
|
||||||
|
|
||||||
|
engine = create_engine("sqlite:///:memory:")
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
|
||||||
|
with Session(engine) as session:
|
||||||
|
# Cadastrar dataset com status antigo ACTIVE (sem licence_terms)
|
||||||
|
ds = Dataset(
|
||||||
|
kind="NCM", provider="Siscomex", sha256="abc123hash",
|
||||||
|
version_label="2026-09-28", act="", source_url="https://portalunico.siscomex.gov.br",
|
||||||
|
status="ACTIVE", record_count=15156,
|
||||||
|
)
|
||||||
|
session.add(ds)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Antes do sync, campos novos eram vazios
|
||||||
|
assert ds.licence_terms is None
|
||||||
|
|
||||||
|
# Executar sincronização
|
||||||
|
stats = sync_inventory_to_registry(session)
|
||||||
|
assert stats["active_found"] == 1
|
||||||
|
assert stats["updated"] == 1
|
||||||
|
assert ds.licence_terms is not None
|
||||||
|
assert ds.act == "Resolução Gecex / MDIC"
|
||||||
|
|
||||||
|
# Listagem do registry deve listar datasets ACTIVE
|
||||||
|
listed = list_registry(session)
|
||||||
|
assert len(listed) == 1
|
||||||
|
assert listed[0].kind == "NCM"
|
||||||
|
|
||||||
|
# Diff contra DRAFT deve encontrar o ACTIVE como vigente
|
||||||
|
draft = Dataset(
|
||||||
|
kind="NCM", provider="Siscomex", sha256="diff123hash",
|
||||||
|
version_label="2026-10-01", act="Nova Resolução", source_url="url",
|
||||||
|
status="DRAFT", record_count=15200,
|
||||||
|
)
|
||||||
|
session.add(draft)
|
||||||
|
session.commit()
|
||||||
|
diff = diff_against_vigente(session, draft)
|
||||||
|
assert diff["current_vigente"] is not None
|
||||||
|
assert diff["current_vigente"]["sha256"] == "abc123hash"
|
||||||
|
assert diff["record_count_delta"] == 15200 - 15156
|
||||||
|
|
||||||
Reference in New Issue
Block a user