chore: establish project baseline
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
def test_parser_extracts_item_cest_and_ncm_from_official_table_text():
|
||||
from central.app.datasets.ba_st_mva import parse_page
|
||||
|
||||
records, rejected = parse_page("ITEM CEST NCM/SH DESCRIÇÃO\n3.3 03.003.00 2201.1 Água mineral 114%", 1)
|
||||
|
||||
assert records[0]["item"] == "3.3"
|
||||
assert records[0]["cest"] == "03.003.00"
|
||||
assert records[0]["ncm_sh"] == ["2201.1"]
|
||||
assert not rejected
|
||||
@@ -0,0 +1,113 @@
|
||||
from datetime import datetime, timezone
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from central.app.datasets import bootstrap_v1_extra as parsers
|
||||
from central.app.datasets.service import DatasetService
|
||||
from central.app.models import Base
|
||||
|
||||
ROOT = Path(__file__).parents[2] / "sources" / "raw" / "bootstrap_v1"
|
||||
FILES = {
|
||||
"cfop": ROOT / "IT_2023_002_v2_10_Tabela_CFOP_Descrição.xlsx",
|
||||
"fcp": ROOT / "Tabela_Alíquotas_FCP_por_UF_2025-02_07.xlsx",
|
||||
"anp": ROOT / "Tabela_Codigo_Produto_ANP-20241118.xlsx",
|
||||
"ncm_utrib": ROOT / "IT2024.001v2.40 - Tabela NCM 2022 com Utrib_Comércio Exterior_vigência 01.10.26.xlsx",
|
||||
"biofuel": ROOT / "IT 2025.004 v.1.20 - Tabela de Índice.xlsx",
|
||||
"credpres": ROOT / "cCredPres_2026-06-22.xlsx",
|
||||
}
|
||||
pytestmark = pytest.mark.skipif(not all(p.exists() for p in FILES.values()), reason="bootstrap_v1 extra source files not present locally")
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def _payload(name):
|
||||
parse_fn = {"cfop": parsers.parse_cfop, "fcp": parsers.parse_fcp, "anp": parsers.parse_anp,
|
||||
"ncm_utrib": parsers.parse_ncm_utrib, "biofuel": parsers.parse_rtc_biofuel_index,
|
||||
"credpres": parsers.parse_rtc_cred_pres}[name]
|
||||
return parse_fn(FILES[name].read_bytes())
|
||||
|
||||
|
||||
def _engine():
|
||||
engine = create_engine("sqlite://")
|
||||
Base.metadata.create_all(engine)
|
||||
return engine
|
||||
|
||||
|
||||
def test_cfop_parses_full_table_with_official_fields():
|
||||
payload = _payload("cfop")
|
||||
assert len(payload.entries) >= 500
|
||||
sample = payload.entries[0]
|
||||
assert set(sample) >= {"code", "title", "description", "effective_from", "ind_nfe", "ind_comb"}
|
||||
|
||||
|
||||
def test_fcp_covers_all_states_and_df():
|
||||
payload = _payload("fcp")
|
||||
ufs = {e["uf"] for e in payload.entries}
|
||||
assert len(ufs) == 27 # 26 states + DF
|
||||
assert "BA" in ufs
|
||||
|
||||
|
||||
def test_anp_has_no_invented_descriptions():
|
||||
payload = _payload("anp")
|
||||
assert len(payload.entries) >= 900
|
||||
assert all(set(e) == {"cprod_anp"} for e in payload.entries) # source truly has no description column
|
||||
|
||||
|
||||
def test_ncm_utrib_covers_thousands_of_ncms():
|
||||
payload = _payload("ncm_utrib")
|
||||
assert len(payload.entries) >= 10000
|
||||
assert all(len(e["ncm"]) == 8 for e in payload.entries[:50])
|
||||
|
||||
|
||||
def test_biofuel_index_separates_current_from_historical():
|
||||
payload = _payload("biofuel")
|
||||
current = [e for e in payload.entries if not e["is_historical"]]
|
||||
historical = [e for e in payload.entries if e["is_historical"]]
|
||||
assert current and historical
|
||||
assert all(e["p_bio_percent"] is not None for e in current)
|
||||
|
||||
|
||||
def test_credpres_preserves_rich_official_fields_not_just_code_description():
|
||||
payload = _payload("credpres")
|
||||
sample = payload.entries[0]
|
||||
assert sample["legal_basis"]
|
||||
assert "d_ini_vig_cbs" in sample and "aliquota_cbs_text" in sample
|
||||
|
||||
|
||||
@pytest.mark.parametrize("domain,method,lookup", [
|
||||
("cfop", "import_cfop", lambda s: s.cfop_lookup(code="1101")),
|
||||
("fcp", "import_fcp", lambda s: [s.fcp_for_uf("BA")]),
|
||||
("anp", "import_anp", lambda s: [s.anp_code_exists("110203073")]),
|
||||
("ncm_utrib", "import_ncm_utrib", lambda s: [s.ncm_utrib_for_ncm("01012100")]),
|
||||
("biofuel", "import_rtc_biofuel_index", lambda s: s.rtc_biofuel_for_anp("320102001")),
|
||||
("credpres", "import_rtc_cred_pres", lambda s: s.rtc_cred_pres(code="1")),
|
||||
])
|
||||
def test_import_idempotent_and_lookup_smoke(domain, method, lookup):
|
||||
engine = _engine()
|
||||
with Session(engine) as session:
|
||||
service = DatasetService(session)
|
||||
payload = _payload(domain)
|
||||
first = getattr(service, method)(payload, f"file://{domain}", datetime.now(timezone.utc))
|
||||
second = getattr(service, method)(payload, f"file://{domain}", datetime.now(timezone.utc))
|
||||
assert first.id == second.id
|
||||
assert second.status == "ACTIVE"
|
||||
assert second.record_count == len(payload.entries)
|
||||
|
||||
result = lookup(service)
|
||||
assert result
|
||||
assert all(item is not None and item is not False for item in result) or result == [True]
|
||||
|
||||
|
||||
def test_dataset_versions_includes_all_new_kinds():
|
||||
engine = _engine()
|
||||
with Session(engine) as session:
|
||||
service = DatasetService(session)
|
||||
versions = service.dataset_versions()
|
||||
for key in ("cfop", "fcp", "anp", "ncm_utrib", "rtc_biofuel_index", "rtc_ccredpres"):
|
||||
assert key in versions
|
||||
assert versions[key] is None
|
||||
service.import_cfop(_payload("cfop"), "file://cfop", datetime.now(timezone.utc))
|
||||
assert service.dataset_versions()["cfop"] is not None
|
||||
@@ -0,0 +1,38 @@
|
||||
from central.app.classification import Classifier
|
||||
|
||||
|
||||
class _Repository:
|
||||
def __init__(self, cest_entries):
|
||||
self._cest_entries = cest_entries
|
||||
|
||||
def active_entries(self):
|
||||
return [type("Entry", (), {"code": "19022000", "description": "Massas"})()]
|
||||
|
||||
def cest_for_ncm(self, code):
|
||||
return self._cest_entries if code == "19022000" else []
|
||||
|
||||
def legacy_ncm(self, code):
|
||||
return None
|
||||
|
||||
def dataset_versions(self):
|
||||
return {"ncm": "n", "cest": "c"}
|
||||
|
||||
|
||||
def test_stopwords_do_not_inflate_description_score():
|
||||
# "RISOLE DE CAMARAO" vs an official text that only shares the stopword "DE"
|
||||
# ("... preparadas de outro modo") must score 0, not a false-positive overlap.
|
||||
cest = type("Cest", (), {
|
||||
"code": "17.048.02",
|
||||
"description": "Massas alimenticias recheadas (mesmo cozidas ou preparadas de outro modo)",
|
||||
})()
|
||||
result = Classifier(_Repository([cest])).classify({"current_ncm": "19022000", "description": "RISOLE DE CAMARAO"})
|
||||
candidate = result["classification"]["cest_candidates"][0]
|
||||
assert candidate["description_score"] == 0.0
|
||||
assert result["cest_status"] == "CEST_DESCRIPTION_MISMATCH"
|
||||
|
||||
|
||||
def test_genuine_token_overlap_still_scores_above_zero():
|
||||
cest = type("Cest", (), {"code": "17.024.01", "description": "Queijo mucarela"})()
|
||||
result = Classifier(_Repository([cest])).classify({"current_ncm": "19022000", "description": "BOLINHO DE QUEIJO"})
|
||||
candidate = result["classification"]["cest_candidates"][0]
|
||||
assert candidate["description_score"] > 0
|
||||
@@ -0,0 +1,62 @@
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from central.app import api as api_module
|
||||
from central.app.main import create_app
|
||||
from central.app.models import Base
|
||||
|
||||
HEADERS = {"Authorization": "Bearer test-token"}
|
||||
PAYLOAD = {"current_ncm": "22030000", "description": "CERVEJA LATA"}
|
||||
|
||||
|
||||
def make_client(monkeypatch, calls, engine_version="0.3.0", datasets=None):
|
||||
versions = datasets if datasets is not None else {"ncm": "aaa"}
|
||||
|
||||
class FakeClassifier:
|
||||
def __init__(self, repository): pass
|
||||
|
||||
def classify(self, product):
|
||||
calls.append(product)
|
||||
return {"status": "EXACT_NCM", "engine_version": api_module.ENGINE_VERSION, "decision_version": "x", "dataset_versions": dict(versions), "classification": {"ncm": product["current_ncm"]}}
|
||||
|
||||
monkeypatch.setattr(api_module, "Classifier", FakeClassifier)
|
||||
monkeypatch.setattr(api_module, "ENGINE_VERSION", engine_version)
|
||||
monkeypatch.setattr(api_module.DatasetService, "dataset_versions", lambda self: dict(versions))
|
||||
engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
|
||||
Base.metadata.create_all(engine)
|
||||
return TestClient(create_app(session_factory=lambda: Session(engine), bootstrap_token="test-token")), versions
|
||||
|
||||
|
||||
def test_cache_hit_does_not_run_the_classifier(monkeypatch):
|
||||
calls = []
|
||||
client, _ = make_client(monkeypatch, calls)
|
||||
first = client.post("/v1/classify", json=PAYLOAD, headers=HEADERS).json()
|
||||
assert first["provenance"]["cache_hit"] is False and len(calls) == 1
|
||||
for _ in range(3):
|
||||
again = client.post("/v1/classify", json=PAYLOAD, headers=HEADERS).json()
|
||||
assert again["provenance"]["cache_hit"] is True
|
||||
assert {k: v for k, v in again.items() if k != "provenance"} == {k: v for k, v in first.items() if k != "provenance"} # same fiscal result
|
||||
assert len(calls) == 1 # CLASSIFIER_EXECUTIONS_ON_HIT = 0
|
||||
|
||||
|
||||
def test_batch_hits_skip_the_classifier_and_new_inputs_miss(monkeypatch):
|
||||
calls = []
|
||||
client, _ = make_client(monkeypatch, calls)
|
||||
products = [PAYLOAD, {"current_ncm": "19022000", "description": "MASSA"}]
|
||||
client.post("/v1/classify/batch", json={"products": products}, headers=HEADERS)
|
||||
assert len(calls) == 2
|
||||
res = client.post("/v1/classify/batch", json={"products": products + [{"current_ncm": "20099000", "description": "SUCO"}]}, headers=HEADERS).json()["results"]
|
||||
assert [r["provenance"]["cache_hit"] for r in res] == [True, True, False] and len(calls) == 3
|
||||
|
||||
|
||||
def test_engine_or_dataset_version_change_invalidates_naturally(monkeypatch):
|
||||
calls = []
|
||||
client, versions = make_client(monkeypatch, calls)
|
||||
client.post("/v1/classify", json=PAYLOAD, headers=HEADERS)
|
||||
versions["ncm"] = "bbb" # a new ACTIVE dataset version changes dataset_versions()
|
||||
assert client.post("/v1/classify", json=PAYLOAD, headers=HEADERS).json()["provenance"]["cache_hit"] is False
|
||||
monkeypatch.setattr(api_module, "ENGINE_VERSION", "0.4.0")
|
||||
assert client.post("/v1/classify", json=PAYLOAD, headers=HEADERS).json()["provenance"]["cache_hit"] is False
|
||||
assert len(calls) == 3
|
||||
@@ -0,0 +1,31 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from central.app.classification import classify_tipi_rate
|
||||
|
||||
|
||||
def _entry(ex=None, ipi_rate=None, ipi_rate_text=None):
|
||||
return SimpleNamespace(ex=ex, ipi_rate=ipi_rate, ipi_rate_text=ipi_rate_text)
|
||||
|
||||
|
||||
def test_ex_specific():
|
||||
assert classify_tipi_rate(_entry(ex="01")) == "EX_SPECIFIC"
|
||||
|
||||
|
||||
def test_source_blank():
|
||||
assert classify_tipi_rate(_entry(ipi_rate_text=None)) == "SOURCE_BLANK"
|
||||
|
||||
|
||||
def test_nt_or_non_taxed():
|
||||
assert classify_tipi_rate(_entry(ipi_rate_text="NT")) == "NT_OR_NON_TAXED"
|
||||
|
||||
|
||||
def test_explicit_zero_text():
|
||||
assert classify_tipi_rate(_entry(ipi_rate_text="0,00")) == "EXPLICIT_ZERO"
|
||||
|
||||
|
||||
def test_explicit_zero_numeric():
|
||||
assert classify_tipi_rate(_entry(ipi_rate=0, ipi_rate_text="0")) == "EXPLICIT_ZERO"
|
||||
|
||||
|
||||
def test_other_unrecognized_marker():
|
||||
assert classify_tipi_rate(_entry(ipi_rate_text="ISENTO")) == "OTHER"
|
||||
@@ -0,0 +1,11 @@
|
||||
import importlib
|
||||
|
||||
|
||||
def test_database_url_encodes_password_from_component_environment(monkeypatch):
|
||||
monkeypatch.delenv("DATABASE_URL", raising=False)
|
||||
monkeypatch.setenv("POSTGRES_PASSWORD", "a@b/c")
|
||||
import central.app.config as config
|
||||
|
||||
module = importlib.reload(config)
|
||||
|
||||
assert module.settings.database_url == "postgresql+psycopg://gti:a%40b%2Fc@postgres:5432/gti_fiscal"
|
||||
@@ -0,0 +1,180 @@
|
||||
import pytest
|
||||
|
||||
from central.app.dry_run_planner import (
|
||||
build_field_proposals,
|
||||
execute,
|
||||
mva_evidence,
|
||||
split_by_eligibility,
|
||||
)
|
||||
|
||||
|
||||
def _decision(**overrides):
|
||||
base = {
|
||||
"status": "EXACT_NCM",
|
||||
"cest_status": "NO_CEST_MAPPING_FOUND",
|
||||
"ipi_status": "NO_TIPI_MAPPING_FOUND",
|
||||
"ba_st_mva_status": "NO_MVA_MAPPING_FOUND",
|
||||
"classification": {
|
||||
"ncm": "07069000",
|
||||
"cest": [],
|
||||
"ipi": None,
|
||||
"ba_st_mva": None,
|
||||
"ba_st_mva_candidates": [],
|
||||
},
|
||||
"dataset_versions": {"ncm": "n1", "cest": "c1", "tipi_ipi": "t1", "ba_st_mva": "m1"},
|
||||
"engine_version": "0.1.0",
|
||||
"decision_version": "mvp",
|
||||
"request_fingerprint": "fp",
|
||||
}
|
||||
base.update(overrides)
|
||||
return base
|
||||
|
||||
|
||||
def test_ipi_resolved_numeric_current_unknown():
|
||||
decision = _decision(ipi_status="IPI_RESOLVED", classification={**_decision()["classification"], "ipi": {"rate": "3.25", "rate_numeric": 3.25, "rate_kind": "NUMERIC", "ex": None}})
|
||||
proposals = build_field_proposals("000026", decision, {})
|
||||
ipi = next(p for p in proposals if p["field"] == "IPI")
|
||||
assert ipi["status"] == "CURRENT_UNKNOWN"
|
||||
assert ipi["write_eligible"] is False
|
||||
assert ipi["proposed_value"] == 3.25
|
||||
|
||||
|
||||
def test_ipi_resolved_numeric_same_value():
|
||||
decision = _decision(ipi_status="IPI_RESOLVED", classification={**_decision()["classification"], "ipi": {"rate": "3.25", "rate_numeric": 3.25, "rate_kind": "NUMERIC", "ex": None}})
|
||||
proposals = build_field_proposals("000026", decision, {"ipi": 3.25, "ex_tipi": None})
|
||||
ipi = next(p for p in proposals if p["field"] == "IPI")
|
||||
assert ipi["status"] == "SAME_VALUE"
|
||||
assert ipi["write_eligible"] is False
|
||||
|
||||
|
||||
def test_ipi_resolved_numeric_change_required():
|
||||
decision = _decision(ipi_status="IPI_RESOLVED", classification={**_decision()["classification"], "ipi": {"rate": "3.25", "rate_numeric": 3.25, "rate_kind": "NUMERIC", "ex": None}})
|
||||
proposals = build_field_proposals("000026", decision, {"ipi": None, "ex_tipi": None})
|
||||
ipi = next(p for p in proposals if p["field"] == "IPI")
|
||||
assert ipi["status"] == "CHANGE_REQUIRED"
|
||||
assert ipi["write_eligible"] is True
|
||||
assert ipi["proposed_value"] == 3.25
|
||||
|
||||
|
||||
def test_ipi_non_numeric_marker_never_proposes_a_write():
|
||||
decision = _decision(ipi_status="IPI_RESOLVED", classification={**_decision()["classification"], "ipi": {"rate": "NT", "rate_numeric": None, "rate_kind": "NON_NUMERIC", "rate_classification": "NT_OR_NON_TAXED", "ex": None}})
|
||||
proposals = build_field_proposals("000004", decision, {"ipi": None, "ex_tipi": None})
|
||||
ipi = next(p for p in proposals if p["field"] == "IPI")
|
||||
assert ipi["status"] == "NON_NUMERIC_IPI_STATUS"
|
||||
assert ipi["proposed_value"] is None
|
||||
assert ipi["write_eligible"] is False
|
||||
assert ipi["reason_code"] == "TIPI_RATE_NT_OR_NON_TAXED"
|
||||
|
||||
|
||||
def test_ipi_resolved_without_concrete_rate_still_produces_a_row():
|
||||
decision = _decision(ipi_status="IPI_RESOLVED", classification={**_decision()["classification"], "ipi": {"rate": None, "rate_numeric": None, "rate_kind": "BLANK", "rate_classification": "SOURCE_BLANK", "ex": None}})
|
||||
proposals = build_field_proposals("000018", decision, {"ipi": 5, "ex_tipi": None})
|
||||
ipi = next(p for p in proposals if p["field"] == "IPI")
|
||||
assert ipi["status"] == "SOURCE_BLANK"
|
||||
assert ipi["proposed_value"] is None
|
||||
assert ipi["write_eligible"] is False
|
||||
assert ipi["reason_code"] == "TIPI_RATE_SOURCE_BLANK"
|
||||
|
||||
|
||||
def test_no_ex_tipi_row_when_no_ex_candidates():
|
||||
decision = _decision(ipi_status="IPI_RESOLVED", classification={**_decision()["classification"], "ipi": {"rate": "3.25", "rate_numeric": 3.25, "rate_kind": "NUMERIC", "ex": None}, "ipi_candidates": []})
|
||||
proposals = build_field_proposals("000026", decision, {"ipi": None, "ex_tipi": None})
|
||||
assert all(p["field"] != "EX_TIPI" for p in proposals)
|
||||
|
||||
|
||||
def test_ex_tipi_concrete_single_candidate_change_required():
|
||||
decision = _decision(
|
||||
ipi_status="IPI_CANDIDATES",
|
||||
classification={**_decision()["classification"], "ipi": None, "ipi_candidates": [{"ncm": "07069000", "ex": "01", "description": "specific", "rate": "5"}]},
|
||||
)
|
||||
proposals = build_field_proposals("000004", decision, {"ex_tipi": None})
|
||||
ex = next(p for p in proposals if p["field"] == "EX_TIPI")
|
||||
assert ex["status"] == "CHANGE_REQUIRED"
|
||||
assert ex["proposed_value"] == "01"
|
||||
assert ex["write_eligible"] is True
|
||||
|
||||
|
||||
def test_ex_tipi_multiple_candidates_is_candidate_not_eligible():
|
||||
decision = _decision(
|
||||
ipi_status="IPI_CANDIDATES",
|
||||
classification={**_decision()["classification"], "ipi": None, "ipi_candidates": [
|
||||
{"ncm": "07069000", "ex": "01", "description": "a", "rate": "5"},
|
||||
{"ncm": "07069000", "ex": "02", "description": "b", "rate": "7"},
|
||||
]},
|
||||
)
|
||||
proposals = build_field_proposals("000004", decision, {"ex_tipi": None})
|
||||
ex = next(p for p in proposals if p["field"] == "EX_TIPI")
|
||||
assert ex["status"] == "CANDIDATE"
|
||||
assert ex["proposed_value"] is None
|
||||
assert ex["write_eligible"] is False
|
||||
|
||||
|
||||
def test_cest_confirmed_change_required():
|
||||
decision = _decision(cest_status="CEST_CONFIRMED", classification={**_decision()["classification"], "cest": ["17.087.02"]})
|
||||
proposals = build_field_proposals("000018", decision, {"cest": ""})
|
||||
cest = next(p for p in proposals if p["field"] == "CEST")
|
||||
assert cest["status"] == "CHANGE_REQUIRED"
|
||||
assert cest["write_eligible"] is True
|
||||
assert cest["proposed_value"] == "17.087.02"
|
||||
|
||||
|
||||
def test_cest_candidates_not_eligible():
|
||||
decision = _decision(cest_status="CEST_CANDIDATES")
|
||||
proposals = build_field_proposals("000024", decision, {"cest": ""})
|
||||
cest = next(p for p in proposals if p["field"] == "CEST")
|
||||
assert cest["status"] == "CANDIDATE"
|
||||
assert cest["proposed_value"] is None
|
||||
assert cest["write_eligible"] is False
|
||||
|
||||
|
||||
def test_cest_description_mismatch_not_eligible():
|
||||
decision = _decision(cest_status="CEST_DESCRIPTION_MISMATCH")
|
||||
proposals = build_field_proposals("000018", decision, {"cest": ""})
|
||||
cest = next(p for p in proposals if p["field"] == "CEST")
|
||||
assert cest["status"] == "UNRESOLVED"
|
||||
assert cest["reason_code"] == "CEST_DESCRIPTION_SCORE_BELOW_THRESHOLD"
|
||||
assert cest["write_eligible"] is False
|
||||
|
||||
|
||||
def test_legacy_ncm_review_no_invented_successor():
|
||||
decision = _decision(status="LEGACY_NCM", classification={**_decision()["classification"], "ncm": "02071400"})
|
||||
proposals = build_field_proposals("000016", decision, {"ipi": "3.25", "cest": ""})
|
||||
assert len(proposals) == 1
|
||||
row = proposals[0]
|
||||
assert row["field"] == "CLASSIFICACAO_FISCAL"
|
||||
assert row["status"] == "LEGACY_NCM_REVIEW"
|
||||
assert row["proposed_value"] is None
|
||||
assert row["write_eligible"] is False
|
||||
|
||||
|
||||
def test_mva_never_produces_a_write_proposal():
|
||||
decision = _decision(ba_st_mva_status="MVA_CANDIDATES", classification={**_decision()["classification"], "ba_st_mva_candidates": [{"item": "1"}]})
|
||||
proposals = build_field_proposals("000024", decision, {"cest": ""})
|
||||
assert all(p["field"] != "BA_ST_MVA" for p in proposals)
|
||||
evidence = mva_evidence("000024", decision)
|
||||
assert evidence["status"] == "MVA_CANDIDATES"
|
||||
assert evidence["candidates"]
|
||||
|
||||
|
||||
def test_split_by_eligibility():
|
||||
decision = _decision(ipi_status="IPI_RESOLVED", classification={**_decision()["classification"], "ipi": {"rate": "3.25", "rate_numeric": 3.25, "rate_kind": "NUMERIC", "ex": None}})
|
||||
proposals = build_field_proposals("000026", decision, {})
|
||||
split = split_by_eligibility(proposals)
|
||||
assert split["blocked"] == proposals
|
||||
assert split["eligible"] == []
|
||||
|
||||
|
||||
def test_execute_refuses_non_eligible_proposal():
|
||||
decision = _decision(ipi_status="IPI_RESOLVED", classification={**_decision()["classification"], "ipi": {"rate": "3.25", "rate_numeric": 3.25, "rate_kind": "NUMERIC", "ex": None}})
|
||||
proposals = build_field_proposals("000026", decision, {})
|
||||
with pytest.raises(PermissionError):
|
||||
execute(proposals)
|
||||
|
||||
|
||||
def test_execute_never_writes_even_when_eligible():
|
||||
decision = _decision(ipi_status="IPI_RESOLVED", classification={**_decision()["classification"], "ipi": {"rate": "3.25", "rate_numeric": 3.25, "rate_kind": "NUMERIC", "ex": None}})
|
||||
proposals = build_field_proposals("000026", decision, {"ipi": "0"})
|
||||
eligible_only = split_by_eligibility(proposals)["eligible"]
|
||||
assert eligible_only
|
||||
with pytest.raises(NotImplementedError):
|
||||
execute(eligible_only)
|
||||
@@ -0,0 +1,270 @@
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from central.app.field_contract import (ALREADY, CHANGE, CONTRACT, MANUAL, NA, WRITE_FIELDS, Context, infer_role, resolve_product)
|
||||
|
||||
ROOT = Path(__file__).parents[2]
|
||||
REFS = {"cfop_valid": {"5101", "5102", "5401", "5405", "1102", "1101", "1403", "2102"}, "csosn": {"101", "102", "103", "300", "400", "500"}, "cst_icms": {"00", "20", "40", "41", "60"},
|
||||
"cst_pis": {"01", "49", "98"}, "cst_cofins": {"01", "49", "98"}, "origem": set("012345678"), "fcp_uf": {}, "cbs_rate_2026": None, "cclasstrib_count": 0}
|
||||
CO1 = {"uf": "BA", "crt": "1", "contribuinte_ipi": "N", "company_code": "000001"}
|
||||
CO3 = {"uf": "BA", "crt": "3", "contribuinte_ipi": "S", "company_code": "000001"}
|
||||
|
||||
|
||||
def product(**kw):
|
||||
p = {f: None for f in WRITE_FIELDS}
|
||||
p.update({"CODIGO": "000001", "PRODUTO": "CERVEJA LATA", "CODGRUPO": "000019", "TIPO": "VENDA", "PROD_COZINHA": None, "PRODUTO_SERVICO": None, "CLASSIFICACAO_FISCAL": "22030000",
|
||||
"CFOP": "5102", "CSOSN": "102", "CST": "000", "SITUACAO_TRIBUTARIA": "T", "IAT": "A", "IPPT": "T", "INCIDENCIA_PISCOFINS": "NENHUMA", "PISCOFINS": "N"})
|
||||
p.update(kw)
|
||||
return p
|
||||
|
||||
|
||||
def decision(**kw):
|
||||
d = {"status": "EXACT_NCM", "cest_status": "NO_CEST_MAPPING_FOUND", "ipi_status": "IPI_RESOLVED", "ba_st_mva_status": "NO_MVA_MAPPING_FOUND", "dataset_versions": {},
|
||||
"classification": {"ncm": "22030000", "ipi": {"rate_kind": "NUMERIC", "rate_numeric": 3.9, "rate": "3.9"}, "ipi_candidates": [], "cest": None, "cest_candidates": [], "ba_st_mva": None, "ba_st_mva_candidates": []}}
|
||||
cls = kw.pop("classification", {})
|
||||
d.update(kw)
|
||||
d["classification"] = {**d["classification"], **cls}
|
||||
return d
|
||||
|
||||
|
||||
EV = {"recipes": set(), "composed": set(), "purchases": {}, "stock_entry": {}, "stock_in": {"000001": True}}
|
||||
|
||||
|
||||
IPI_POP = {"scope": "COMPANY", "scope_key": "", "key": "store_ipi_profile", "value": "POPULATE_REFERENCE_RATE"}
|
||||
|
||||
|
||||
def run(p=None, d=None, co=CO1, ctx=None, ev=EV, manual=None, ipi_profile=IPI_POP):
|
||||
return resolve_product(p or product(), d or decision(), co, REFS, ev, Context((ctx or []) + ([ipi_profile] if ipi_profile else [])), manual_ncm=manual)
|
||||
|
||||
|
||||
def st(res, f):
|
||||
return res[f]["state"], res[f]["proposed"]
|
||||
|
||||
|
||||
def test_every_write_field_has_a_complete_contract():
|
||||
keys = {"ERP_FIELD", "UI_LABEL", "SEMANTICS", "DATA_TYPE", "SOURCE_OF_TRUTH", "REQUIRED_INPUTS", "RESOLUTION_RULE", "VALID_VALUES", "NOT_APPLICABLE_RULE", "MANUAL_INPUT_RULE", "WRITE_ALLOWED", "VALIDATION_RULE", "PROVENANCE"}
|
||||
assert set(CONTRACT) == set(WRITE_FIELDS) and len(WRITE_FIELDS) == 23
|
||||
for f, c in CONTRACT.items():
|
||||
assert keys <= set(c) and all(c[k] not in (None, "", []) for k in keys - {"WRITE_ALLOWED"}), f
|
||||
|
||||
|
||||
def test_only_five_final_states_and_manual_always_names_its_input():
|
||||
res = run()
|
||||
for f in WRITE_FIELDS:
|
||||
assert res[f]["state"] in (ALREADY, CHANGE, NA, MANUAL)
|
||||
if res[f]["state"] == MANUAL:
|
||||
assert res[f]["input_key"]
|
||||
assert res[f]["write_eligible"] == (res[f]["state"] == CHANGE)
|
||||
|
||||
|
||||
def test_ncm_states_and_operator_ncm_reprocessing():
|
||||
bad = run(p=product(CLASSIFICACAO_FISCAL="12345678"), d=decision(status="INVALID_NCM"))
|
||||
assert st(bad, "CLASSIFICACAO_FISCAL")[0] == MANUAL and bad["CLASSIFICACAO_FISCAL"]["input_key"] == "ncm"
|
||||
for f in ("IPI", "EX_TIPI", "CEST", "CFOP", "CSOSN"):
|
||||
assert bad[f]["state"] == MANUAL and bad[f]["reason"] == "BLOCKED_BY_NCM"
|
||||
assert st(run(manual=("22030000", False), p=product(CLASSIFICACAO_FISCAL="12345678"), d=decision(status="INVALID_NCM")), "CLASSIFICACAO_FISCAL")[0] == MANUAL
|
||||
fixed = run(manual=("22030000", True), p=product(CLASSIFICACAO_FISCAL="12345678"), d=decision())
|
||||
assert st(fixed, "CLASSIFICACAO_FISCAL") == (CHANGE, "22030000") and fixed["IPI"]["state"] == CHANGE and fixed["IPI"]["proposed"] == 3.9 # dependents reprocessed with the manual NCM
|
||||
|
||||
|
||||
def test_ipi_rules_never_write_nt_or_blank_and_ask_when_ambiguous():
|
||||
assert st(run(), "IPI") == (CHANGE, 3.9)
|
||||
assert st(run(p=product(IPI=3.9)), "IPI")[0] == ALREADY
|
||||
nt = run(d=decision(classification={"ipi": {"rate_kind": "NON_NUMERIC", "rate_classification": "NT"}}))
|
||||
assert nt["IPI"]["state"] == NA and not nt["IPI"]["write_eligible"]
|
||||
blank = run(d=decision(classification={"ipi": {"rate_kind": "BLANK"}}))
|
||||
assert blank["IPI"]["state"] == NA and "NEVER_ZERO" in blank["IPI"]["reason"]
|
||||
amb = decision(ipi_status="IPI_CANDIDATES", classification={"ipi": None, "ipi_candidates": [{"ex": None, "rate": "5", "description": "base"}, {"ex": "1", "rate": "0", "description": "Chope"}]})
|
||||
r = run(d=amb)
|
||||
assert r["IPI"]["state"] == MANUAL and r["IPI"]["input_key"] == "ex_tipi" and len(r["IPI"]["choices"]) == 2
|
||||
solved = run(d=amb, ctx=[{"scope": "PRODUCT", "scope_key": "000001", "key": "ex_tipi", "value": "NONE"}])
|
||||
assert st(solved, "IPI") == (CHANGE, 5.0) and solved["EX_TIPI"]["state"] == NA
|
||||
|
||||
|
||||
def test_cest_contract():
|
||||
off = [{"code": "03.021.03", "official_description": "Cerveja em lata", "description_score": 1.0}, {"code": "03.021.05", "official_description": "PET", "description_score": 0.5}]
|
||||
conf = run(d=decision(cest_status="CEST_CONFIRMED", classification={"cest": ["03.021.03"], "cest_candidates": off}))
|
||||
assert st(conf, "CEST") == (CHANGE, "0302103")
|
||||
assert run(p=product(CEST="0302103"), d=decision(cest_status="CEST_CANDIDATES", classification={"cest_candidates": off}))["CEST"]["state"] == ALREADY
|
||||
many = run(d=decision(cest_status="CEST_CANDIDATES", classification={"cest_candidates": off}))
|
||||
assert many["CEST"]["state"] == MANUAL and many["CEST"]["input_key"] == "cest" and len(many["CEST"]["choices"]) == 2
|
||||
assert run(d=decision(cest_status="CEST_DESCRIPTION_MISMATCH", classification={"cest_candidates": off}))["CEST"]["state"] == NA
|
||||
assert run()["CEST"]["state"] == NA
|
||||
assert run(p=product(CEST="9999999"))["CEST"]["state"] == MANUAL
|
||||
picked = run(d=decision(cest_status="CEST_CANDIDATES", classification={"cest_candidates": off}), ctx=[{"scope": "NCM", "scope_key": "22030000", "key": "cest", "value": "03.021.05"}])
|
||||
assert st(picked, "CEST") == (CHANGE, "0302105")
|
||||
|
||||
|
||||
def test_cfop_role_and_st_rules():
|
||||
ok = run() # RESALE (stock entries), not subject to ST, declared 5102 == derived
|
||||
assert ok["CFOP"]["state"] == ALREADY and ok["_facts"]["role"] == "RESALE"
|
||||
assert st(run(p=product(CFOP="5405")), "CFOP") == (CHANGE, "5102") # 5405 but proven NOT_SUBJECT
|
||||
assert st(run(p=product(CFOP=None)), "CFOP") == (CHANGE, "5102") # blank filled
|
||||
unk = run(d=decision(ba_st_mva_status="MVA_CANDIDATES"), p=product(CFOP=None)) # ST unknown, blank -> asks
|
||||
assert unk["CFOP"]["state"] == MANUAL and unk["CFOP"]["input_key"] == "st_status"
|
||||
assert run(d=decision(ba_st_mva_status="MVA_CANDIDATES"))["CFOP"]["state"] == ALREADY # declared valid, not contradicted
|
||||
role_unknown = run(ev={**EV, "stock_in": {}}, p=product(CFOP=None))
|
||||
assert role_unknown["CFOP"]["state"] == MANUAL and role_unknown["CFOP"]["input_key"] == "product_role"
|
||||
own = run(ev={**EV, "recipes": {"000001"}}, p=product(CFOP="5102"))
|
||||
assert st(own, "CFOP") == (CHANGE, "5101")
|
||||
assert run(p=product(TIPO="MATERIA-PRIMA"))["CFOP"]["state"] == NA
|
||||
|
||||
|
||||
def test_cfop_entrada_needs_company_entry_scope_then_derives():
|
||||
r = run(p=product(CFOP_ENTRADA=None))
|
||||
assert r["CFOP_ENTRADA"]["state"] == MANUAL and r["CFOP_ENTRADA"]["input_key"] == "entry_scope"
|
||||
r = run(p=product(CFOP_ENTRADA=None), ctx=[{"scope": "COMPANY", "scope_key": "", "key": "entry_scope", "value": "INTRA"}])
|
||||
assert st(r, "CFOP_ENTRADA") == (CHANGE, "1102")
|
||||
assert run(p=product(CFOP_ENTRADA="1102"))["CFOP_ENTRADA"]["state"] == ALREADY
|
||||
|
||||
|
||||
def test_icms_code_fields_follow_the_regime():
|
||||
r1 = run()
|
||||
assert r1["CST"]["state"] == NA and r1["CST_ENTRADA"]["state"] == NA and r1["CSOSN"]["state"] == ALREADY
|
||||
assert run(p=product(CSOSN=None))["CSOSN"]["state"] == MANUAL
|
||||
assert st(run(p=product(CSOSN=None), ctx=[{"scope": "GROUP", "scope_key": "000019", "key": "icms_treatment", "value": "EXEMPT"}]), "CSOSN") == (CHANGE, "400")
|
||||
assert st(run(p=product(CSOSN="102"), ctx=[{"scope": "GROUP", "scope_key": "000019", "key": "icms_treatment", "value": "ST_WITHHELD"}]), "CSOSN") == (CHANGE, "500")
|
||||
r3 = run(co=CO3, p=product(CSOSN=None))
|
||||
assert r3["CSOSN"]["state"] == NA and r3["CST"]["state"] == ALREADY and r3["ALIQUOTA"]["state"] in (ALREADY, MANUAL)
|
||||
blank = run(co=CO3, p=product(CST=None, COD_ORIGEM_MERCADORIA="0"))
|
||||
assert blank["CST"]["state"] == MANUAL and blank["CST"]["input_key"] == "icms_treatment"
|
||||
filled = run(co=CO3, p=product(CST=None, COD_ORIGEM_MERCADORIA="0"), ctx=[{"scope": "COMPANY", "scope_key": "", "key": "icms_treatment", "value": "TAXED"}])
|
||||
assert st(filled, "CST") == (CHANGE, "000")
|
||||
|
||||
|
||||
def test_aliquota_not_copied_blindly():
|
||||
assert run()["ALIQUOTA"]["state"] == NA # Simples CSOSN 102 highlights no ICMS
|
||||
assert run(p=product(CSOSN="101", ALIQUOTA=None))["ALIQUOTA"]["input_key"] == "credit_rate"
|
||||
assert st(run(co=CO3, p=product(ALIQUOTA="17"), ctx=[{"scope": "COMPANY", "scope_key": "", "key": "icms_rate", "value": "20,5"}]), "ALIQUOTA") == (CHANGE, 20.5)
|
||||
|
||||
|
||||
def test_origin_binding_rules():
|
||||
r0 = run(p=product(COD_ORIGEM_MERCADORIA=None, CST="040"))["COD_ORIGEM_MERCADORIA"] # audit V2: the CST first digit is padding, never origin evidence
|
||||
assert r0["state"] == MANUAL and r0["reason"] == "ORIGIN_NO_EVIDENCE" and r0["proposed"] is None
|
||||
assert run(p=product(COD_ORIGEM_MERCADORIA="0", CST="040"))["COD_ORIGEM_MERCADORIA"]["state"] == ALREADY
|
||||
assert run(p=product(COD_ORIGEM_MERCADORIA="2", CST="040"))["COD_ORIGEM_MERCADORIA"]["state"] == ALREADY # declared origin stays; CST digit is padding
|
||||
assert run(p=product(COD_ORIGEM_MERCADORIA=None, CST="540"))["COD_ORIGEM_MERCADORIA"]["state"] == MANUAL # 5 is not on the SGV screen
|
||||
assert st(run(p=product(COD_ORIGEM_MERCADORIA=None, CST="540"), ctx=[{"scope": "GROUP", "scope_key": "000019", "key": "origin", "value": "1"}]), "COD_ORIGEM_MERCADORIA") == (CHANGE, 1)
|
||||
|
||||
|
||||
def test_cst_ipi_pis_cofins_incidence_iat_ippt():
|
||||
assert run()["CST_IPI"]["state"] == NA
|
||||
assert run(co=CO3)["CST_IPI"]["state"] == MANUAL
|
||||
r = run()
|
||||
assert r["COD_SAIDA_PIS"]["state"] == MANUAL and r["COD_SAIDA_PIS"]["input_key"] == "pis_cofins_profile" and r["ALIQ_COFINS"]["state"] == MANUAL
|
||||
prof = '{"pis_out_cst": "49", "pis_in_cst": "98", "pis_rate": "0", "cofins_out_cst": "49", "cofins_in_cst": "98", "cofins_rate": "0"}'
|
||||
r = run(ctx=[{"scope": "COMPANY", "scope_key": "", "key": "pis_cofins_profile", "value": prof}])
|
||||
assert st(r, "COD_SAIDA_PIS") == (CHANGE, "49") and st(r, "COD_ENTRADA_COFINS") == (CHANGE, "98") and st(r, "ALIQ_PIS") == (CHANGE, "0")
|
||||
bad = run(ctx=[{"scope": "COMPANY", "scope_key": "", "key": "pis_cofins_profile", "value": '{"pis_out_cst": "77"}'}])
|
||||
assert bad["COD_SAIDA_PIS"]["state"] == MANUAL # not in the official CST table
|
||||
assert run(p=product(IAT=None))["IAT"]["input_key"] == "rounding"
|
||||
assert run()["INCIDENCIA_PISCOFINS"]["state"] == ALREADY
|
||||
assert st(run(ev={**EV, "recipes": {"000001"}}), "IPPT") == (CHANGE, "P") and run()["IPPT"]["state"] == ALREADY
|
||||
assert run(p=product(TIPO="MATERIA-PRIMA"))["IPPT"]["state"] == NA
|
||||
|
||||
|
||||
def test_context_hierarchy_product_beats_group_beats_company():
|
||||
ctx = Context([{"scope": "COMPANY", "scope_key": "", "key": "product_role", "value": "RESALE"}, {"scope": "GROUP", "scope_key": "G1", "key": "product_role", "value": "OWN_PRODUCTION"},
|
||||
{"scope": "PRODUCT", "scope_key": "P1", "key": "product_role", "value": "INPUT"}])
|
||||
assert ctx.get("product_role", "P1", "G1") == ("INPUT", "PRODUCT")
|
||||
assert ctx.get("product_role", "P2", "G1") == ("OWN_PRODUCTION", "GROUP")
|
||||
assert ctx.get("product_role", "P2", "G9") == ("RESALE", "COMPANY")
|
||||
assert Context([]).get("product_role", "P2", "G9") == (None, None)
|
||||
|
||||
|
||||
def test_role_inference_is_evidence_based_and_conflicts_stay_unknown():
|
||||
assert infer_role(product(TIPO="MATERIA-PRIMA"), EV, Context([]), "000001", "1")[0] == "INPUT"
|
||||
assert infer_role(product(), {**EV, "stock_in": {}}, Context([]), "000001", "1")[0] == "UNKNOWN"
|
||||
assert infer_role(product(PROD_COZINHA=1), {**EV, "purchases": {"000001": [{}]}}, Context([]), "000001", "1")[0] == "UNKNOWN"
|
||||
assert infer_role(product(PROD_COZINHA=1), EV, Context([]), "000001", "1")[0] == "UNKNOWN" # kitchen flag alone is not proof
|
||||
assert infer_role(product(PROD_COZINHA=1), {**EV, "recipes": {"000001"}}, Context([]), "000001", "1")[0] == "OWN_PRODUCTION"
|
||||
|
||||
|
||||
# ---------------- executor guards ----------------
|
||||
def load_executor():
|
||||
spec = importlib.util.spec_from_file_location("fwe_local", ROOT / "scripts" / "firebird_write_executor.py")
|
||||
m = importlib.util.module_from_spec(spec); sys.modules[spec.name] = m; spec.loader.exec_module(m)
|
||||
return m
|
||||
|
||||
|
||||
class FakeCursor:
|
||||
def __init__(self, db):
|
||||
self.db, self.rowcount, self._row = db, 0, None
|
||||
|
||||
def execute(self, sql, params=()):
|
||||
s = " ".join(sql.split())
|
||||
if "RDB$FIELD_TYPE" in s:
|
||||
self._row = (37, 20)
|
||||
elif s.startswith("SELECT") and "FROM C000025" in s:
|
||||
fields = [x.strip() for x in s.split("SELECT")[1].split("FROM")[0].split(",")]
|
||||
row = self.db["rows"].get(params[0])
|
||||
self._row = tuple(row[f] for f in fields) if row else None
|
||||
elif s.startswith("UPDATE"):
|
||||
row = self.db["rows"].get(params[-1])
|
||||
self.rowcount = 0 if row is None else self.db["update_rowcount"]
|
||||
if row is not None and self.rowcount == 1:
|
||||
for f, v in zip([x.split("=")[0].strip() for x in s.split("SET")[1].split("WHERE")[0].split(",")], params[:-1]):
|
||||
self.db["pending"][f] = v
|
||||
|
||||
def fetchone(self):
|
||||
return self._row
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
|
||||
class FakeConn:
|
||||
def __init__(self, rows, update_rowcount=1):
|
||||
self.db = {"rows": rows, "update_rowcount": update_rowcount, "pending": {}}
|
||||
self.rolled_back = self.committed = False
|
||||
|
||||
def cursor(self):
|
||||
return FakeCursor(self.db)
|
||||
|
||||
def commit(self):
|
||||
for row in self.db["rows"].values():
|
||||
row.update(self.db["pending"])
|
||||
self.committed = True
|
||||
|
||||
def rollback(self):
|
||||
self.db["pending"].clear(); self.rolled_back = True
|
||||
|
||||
|
||||
ALLOWED = r"D:\PROJETOS\GTISISTEMAS\Projeto\Banco\CONSTRUCAO.FDB"
|
||||
|
||||
|
||||
def test_executor_allowlist_is_exact_and_field_set_is_the_contract():
|
||||
ex = load_executor()
|
||||
assert ex._LOCAL_COMPLETION_FIELDS == set(WRITE_FIELDS)
|
||||
allow = ex.load_local_allowlist()
|
||||
assert len(allow) == 5
|
||||
for path in (ALLOWED, r"D:\PROJETOS\GTISISTEMAS\Projeto\TESTESOFICIAIS.FDB"):
|
||||
ex.assert_local_completion_target(path, allow)
|
||||
for bad in (r"D:\PROJETOS\GTISISTEMAS\Banco\GTI.FDB", r"D:\PROJETOS\GTISISTEMAS\Projeto\Banco\OUTRA.FDB", r"D:\PROJETOS\GTISISTEMAS\Projeto\Banco\CONSTRUCAO.FDB.bak", r"D:\PROJETOS\GTISISTEMAS\Projeto\Banco"):
|
||||
with pytest.raises(ex.WriteRefused):
|
||||
ex.assert_local_completion_target(bad, allow)
|
||||
|
||||
|
||||
def test_guarded_product_update_success_drift_rowcount_and_refusals():
|
||||
ex = load_executor()
|
||||
allow = ex.load_local_allowlist()
|
||||
conn = FakeConn({"000001": {"IPI": None, "CEST": "1234567"}})
|
||||
rec = ex.execute_guarded_product_update(conn, database_path=ALLOWED, product_code="000001", changes={"IPI": (None, 3.9), "CEST": ("1234567", "7654321")}, now="t", allowlist=allow)
|
||||
assert rec["rowcount"] == 1 and rec["fields"]["IPI"]["after"] == 3.9 and conn.committed
|
||||
with pytest.raises(ex.WriteRefused, match="drift"):
|
||||
ex.execute_guarded_product_update(FakeConn({"000001": {"IPI": 1.0}}), database_path=ALLOWED, product_code="000001", changes={"IPI": (None, 3.9)}, now="t", allowlist=allow)
|
||||
c2 = FakeConn({"000001": {"IPI": None}}, update_rowcount=2)
|
||||
with pytest.raises(ex.WriteRefused, match="rowcount"):
|
||||
ex.execute_guarded_product_update(c2, database_path=ALLOWED, product_code="000001", changes={"IPI": (None, 3.9)}, now="t", allowlist=allow)
|
||||
assert c2.rolled_back
|
||||
with pytest.raises(ex.WriteRefused, match="allowlist"):
|
||||
ex.execute_guarded_product_update(conn, database_path=r"D:\PROJETOS\GTISISTEMAS\Banco\GTI.FDB", product_code="000001", changes={"IPI": (None, 1)}, now="t", allowlist=allow)
|
||||
with pytest.raises(ex.WriteRefused, match="contract write target"):
|
||||
ex.execute_guarded_product_update(conn, database_path=ALLOWED, product_code="000001", changes={"CODIGO": ("000001", "9")}, now="t", allowlist=allow)
|
||||
with pytest.raises(ex.WriteRefused, match="null"):
|
||||
ex.execute_guarded_product_update(conn, database_path=ALLOWED, product_code="000001", changes={"IPI": (None, None)}, now="t", allowlist=allow)
|
||||
with pytest.raises(ex.WriteRefused, match="only C000025"):
|
||||
ex.execute_guarded_product_update(conn, database_path=ALLOWED, product_code="000001", changes={"IPI": (None, 1)}, now="t", allowlist=allow, table="NFCE_ITENS")
|
||||
@@ -0,0 +1,45 @@
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from central.app.context_keys import validate_entry
|
||||
from central.app.main import create_app
|
||||
from central.app.models import Base
|
||||
|
||||
H = {"Authorization": "Bearer test-token"}
|
||||
|
||||
|
||||
def client():
|
||||
engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
|
||||
Base.metadata.create_all(engine)
|
||||
return TestClient(create_app(session_factory=lambda: Session(engine), bootstrap_token="test-token"))
|
||||
|
||||
|
||||
def test_validation_rules():
|
||||
ok, err = validate_entry({"scope": "group", "scope_key": "000019", "key": "product_role", "value": "resale"})
|
||||
assert err is None and ok["value"] == "RESALE" and ok["scope"] == "GROUP"
|
||||
for bad in ({"scope": "GROUP", "key": "product_role", "value": "RESALE"}, # group without key
|
||||
{"scope": "COMPANY", "key": "product_role", "value": "BOSS"}, # not in enum
|
||||
{"scope": "GROUP", "scope_key": "1", "key": "ncm", "value": "22030000"}, # ncm is product scope
|
||||
{"scope": "PRODUCT", "scope_key": "1", "key": "ncm", "value": "2203"}, # not 8 digits
|
||||
{"scope": "COMPANY", "key": "pis_cofins_profile", "value": "{\"x\": 1}"}, # unknown profile key
|
||||
{"scope": "COMPANY", "key": "icms_rate", "value": "abc"}, {"scope": "COMPANY", "key": "nope", "value": "1"}):
|
||||
assert validate_entry(bad)[1]
|
||||
assert validate_entry({"scope": "COMPANY", "key": "pis_cofins_profile", "value": "{\"pis_out_cst\": \"49\"}"})[1] is None
|
||||
|
||||
|
||||
def test_import_is_all_or_nothing_upserting_and_listed():
|
||||
c = client()
|
||||
body = {"company_key": "abc123", "entries": [{"scope": "GROUP", "scope_key": "000019", "key": "product_role", "value": "RESALE", "reason": "revenda"},
|
||||
{"scope": "COMPANY", "key": "entry_scope", "value": "INTRA"}]}
|
||||
assert c.post("/v1/context/import", json=body, headers=H).json() == {"created": 2, "updated": 0}
|
||||
body["entries"][0]["value"] = "OWN_PRODUCTION"
|
||||
assert c.post("/v1/context/import", json=body, headers=H).json() == {"created": 0, "updated": 2}
|
||||
listed = c.get("/v1/context", params={"company_key": "abc123"}, headers=H).json()["entries"]
|
||||
assert {(e["scope"], e["key"], e["value"]) for e in listed} == {("GROUP", "product_role", "OWN_PRODUCTION"), ("COMPANY", "entry_scope", "INTRA")}
|
||||
bad = {"company_key": "abc123", "entries": [{"scope": "COMPANY", "key": "rounding", "value": "A"}, {"scope": "COMPANY", "key": "rounding", "value": "Z"}]}
|
||||
r = c.post("/v1/context/import", json=bad, headers=H)
|
||||
assert r.status_code == 422
|
||||
assert len(c.get("/v1/context", params={"company_key": "abc123"}, headers=H).json()["entries"]) == 2 # nothing partially stored
|
||||
assert c.get("/v1/context", params={"company_key": "other"}, headers=H).json()["entries"] == []
|
||||
@@ -0,0 +1,71 @@
|
||||
from central.app.fiscal_decision import ERP_FIELD_MAP, decide_product
|
||||
|
||||
REFS = {"cfop_valid": {"5102"}, "csosn": {"102", "500"}, "cst_icms": {"00", "40", "60"}, "cst_pis": {"01"}, "cst_cofins": {"01"},
|
||||
"origem": set("012345678"), "fcp_uf": {"BA": {"aliq_1": "Fixo:2.00"}}, "cbs_rate_2026": 0.009, "cclasstrib_count": 164}
|
||||
COMPANY = {"uf": "BA", "crt": "1"}
|
||||
|
||||
|
||||
def product(**kw):
|
||||
base = {"CODIGO": "1", "PRODUTO": "X", "CLASSIFICACAO_FISCAL": "22030000", "CFOP": "5102", "CST": "040", "CSOSN": "102",
|
||||
"CEST": None, "IPI": None, "EX_TIPI": None, "ORIGEM": None, "COD_SAIDA_PIS": None, "COD_SAIDA_COFINS": None, "ALIQ_PIS": None}
|
||||
base.update(kw)
|
||||
return base
|
||||
|
||||
|
||||
def decision(**kw):
|
||||
base = {"status": "EXACT_NCM", "ipi_status": "IPI_RESOLVED", "cest_status": "CEST_CANDIDATES", "ba_st_mva_status": "MVA_CANDIDATES",
|
||||
"dataset_versions": {}, "classification": {"ncm": "22030000", "ipi": {"rate_kind": "NUMERIC", "rate_numeric": 3.9}, "cest_candidates": [1, 2]}}
|
||||
base.update(kw)
|
||||
return base
|
||||
|
||||
|
||||
def test_numeric_ipi_change_is_the_only_auto_apply():
|
||||
out = decide_product(product(IPI=5102.0), decision(), COMPANY, REFS)
|
||||
assert out["IPI"]["action_class"] == "AUTO_APPLY_SAFE" and out["IPI"]["write_eligible"] and out["IPI"]["proposed_value"] == 3.9
|
||||
assert [d for d, r in out.items() if r["write_eligible"]] == ["IPI"]
|
||||
|
||||
|
||||
def test_same_ipi_is_already_correct_not_write():
|
||||
out = decide_product(product(IPI=3.9), decision(), COMPANY, REFS)
|
||||
assert out["IPI"]["action_class"] == "ALREADY_CORRECT" and not out["IPI"]["write_eligible"]
|
||||
|
||||
|
||||
def test_non_numeric_and_blank_ipi_never_write():
|
||||
d = decision(classification={"ipi": {"rate_kind": "NON_NUMERIC", "rate_classification": "NT"}})
|
||||
assert not decide_product(product(), d, COMPANY, REFS)["IPI"]["write_eligible"]
|
||||
d = decision(classification={"ipi": {"rate_kind": "BLANK"}})
|
||||
r = decide_product(product(), d, COMPANY, REFS)["IPI"]
|
||||
assert not r["write_eligible"] and r["status"] == "RULE_UNRESOLVED"
|
||||
|
||||
|
||||
def test_operation_dependent_fields_never_resolved_or_written():
|
||||
out = decide_product(product(), decision(), COMPANY, REFS)
|
||||
for dom in ("CFOP", "CSOSN", "CST_ICMS", "PIS_CST", "COFINS_CST", "FCP", "RTC_CCLASSTRIB", "RTC_CBS_RATE"):
|
||||
assert out[dom]["status"] == "OPERATION_CONTEXT_REQUIRED" and not out[dom]["write_eligible"]
|
||||
assert out["CFOP"]["domain_valid"] is True and out["FCP"]["fcp_rate_available"] is True
|
||||
assert out["RTC_CBS_RATE"]["proposed_value"] is None and out["RTC_CBS_RATE"]["reference_rate_2026"] == 0.009
|
||||
|
||||
|
||||
def test_ncm_legacy_is_review_and_never_write():
|
||||
out = decide_product(product(), decision(status="LEGACY_NCM"), COMPANY, REFS)
|
||||
assert out["NCM"]["action_class"] == "REVIEW_REQUIRED" and not any(r["write_eligible"] for r in out.values())
|
||||
|
||||
|
||||
def test_origin_only_candidate_and_unconfirmed_mapping():
|
||||
out = decide_product(product(), decision(), COMPANY, REFS)
|
||||
assert out["ORIGEM"]["status"] == "RULE_CANDIDATE" and not out["ORIGEM"]["write_eligible"]
|
||||
assert {f["ERP_FIELD"] for f in ERP_FIELD_MAP if f["MAPPING"] == "MAPPING_UNCONFIRMED"} >= {"ORIGEM", "CFOP_ENTRADA", "CST_ENTRADA"}
|
||||
|
||||
|
||||
def test_mva_never_written_even_when_resolved():
|
||||
d = decision(ba_st_mva_status="MVA_RESOLVED", classification={"ipi": {"rate_kind": "NUMERIC", "rate_numeric": 1}, "ba_st_mva": {"mva_original": "140%"}})
|
||||
r = decide_product(product(IPI=1), d, COMPANY, REFS)["MVA"]
|
||||
assert r["status"] == "RULE_RESOLVED" and not r["write_eligible"]
|
||||
|
||||
|
||||
def test_single_ex_without_description_evidence_is_candidate_not_write():
|
||||
cls = {"ipi": {"rate_kind": "NUMERIC", "rate_numeric": 3.9}, "ipi_candidates": [{"ex": "1", "description": "Chope"}]}
|
||||
out = decide_product(product(PRODUTO="CERVEJA LATA", IPI=3.9), decision(classification=cls), COMPANY, REFS)
|
||||
assert out["EX_TIPI"]["status"] == "RULE_CANDIDATE" and not out["EX_TIPI"]["write_eligible"]
|
||||
out = decide_product(product(PRODUTO="CHOPE ESCURO", IPI=3.9), decision(classification=cls), COMPANY, REFS)
|
||||
assert out["EX_TIPI"]["status"] == "RULE_RESOLVED" and out["EX_TIPI"]["write_eligible"]
|
||||
@@ -0,0 +1,9 @@
|
||||
from central.app.fiscal_determination import FiscalDetermination
|
||||
|
||||
|
||||
def test_returns_unresolved_csosn_and_cfop_without_official_rule():
|
||||
result = FiscalDetermination().determine({"ncm": "08043000", "cest_candidates": []})
|
||||
|
||||
assert result["csosn"] is None
|
||||
assert result["cfop"] is None
|
||||
assert "OFFICIAL_RULE_NOT_IMPLEMENTED" in result["reason_codes"]
|
||||
@@ -0,0 +1,29 @@
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from central.app.main import create_app
|
||||
|
||||
|
||||
def test_health_reports_database_dependency():
|
||||
client = TestClient(create_app(database_probe=lambda: True))
|
||||
|
||||
assert client.get("/health").json() == {"status": "ok"}
|
||||
|
||||
|
||||
def test_health_is_unavailable_when_database_is_down():
|
||||
client = TestClient(create_app(database_probe=lambda: False))
|
||||
|
||||
assert client.get("/health").status_code == 503
|
||||
|
||||
|
||||
def test_version_endpoint_reports_release_and_fiscal_versions():
|
||||
body = TestClient(create_app(database_probe=lambda: True)).get("/version").json()
|
||||
assert body["version"] == "1.0.0-rc1" and body["api_compat"] == "1.0" and body["engine_version"] and body["decision_version"]
|
||||
|
||||
|
||||
def test_production_refuses_the_dev_token(monkeypatch):
|
||||
import pytest
|
||||
from central.app import main as main_module
|
||||
monkeypatch.setattr(main_module.settings.__class__, "environment", "production", raising=False) if False else None
|
||||
monkeypatch.setattr(main_module, "settings", type("S", (), {"environment": "production", "agent_dev_token": "dev", "log_level": "INFO"})())
|
||||
with pytest.raises(RuntimeError):
|
||||
create_app(database_probe=lambda: True)
|
||||
@@ -0,0 +1,58 @@
|
||||
from central.app.icms_context import csosn_v2, product_role, st_conflict_class, st_status, withheld_status
|
||||
|
||||
DOM = {"102", "500", "101", "400"}
|
||||
|
||||
|
||||
def test_cest_candidate_never_confirms_st():
|
||||
assert st_status("EXACT_NCM", "MVA_CANDIDATES", "CEST_CANDIDATES", "", None, 4)[0] == "ST_CANDIDATE"
|
||||
assert st_status("EXACT_NCM", "MVA_RESOLVED", "CEST_CANDIDATES", "", "03.021.03", 4)[0] == "ST_CANDIDATE"
|
||||
|
||||
|
||||
def test_st_confirmed_needs_erp_cest_corroboration():
|
||||
assert st_status("EXACT_NCM", "MVA_RESOLVED", "CEST_CANDIDATES", "0302103", "03.021.03", 4)[0] == "ST_CONFIRMED"
|
||||
assert st_status("EXACT_NCM", "MVA_RESOLVED", "CEST_CONFIRMED", "", "03.021.03", 4)[0] == "ST_CONFIRMED"
|
||||
|
||||
|
||||
def test_st_not_applicable_needs_both_lists_silent_and_valid_ncm():
|
||||
assert st_status("EXACT_NCM", "NO_MVA_MAPPING_FOUND", "NO_CEST_MAPPING_FOUND", None, None, None)[0] == "ST_NOT_APPLICABLE"
|
||||
assert st_status("EXACT_NCM", "NO_MVA_MAPPING_FOUND", "CEST_CANDIDATES", None, None, None)[0] == "ST_UNKNOWN"
|
||||
assert st_status("LEGACY_NCM", "NO_MVA_MAPPING_FOUND", "NO_CEST_MAPPING_FOUND", None, None, None)[0] == "ST_UNKNOWN"
|
||||
|
||||
|
||||
def test_withholding_needs_positive_purchase_evidence():
|
||||
assert withheld_status([])[0] == "ST_WITHHOLDING_UNKNOWN"
|
||||
assert withheld_status([{"icms_retido": "N", "valor_retido": 0, "sub_valor": 0}])[0] == "ST_ALREADY_WITHHELD_NOT_CONFIRMED"
|
||||
assert withheld_status([{"icms_retido": "S", "valor_retido": 0, "sub_valor": 0}])[0] == "ST_ALREADY_WITHHELD_CONFIRMED"
|
||||
assert withheld_status([{"icms_retido": "N", "valor_retido": 5, "sub_valor": 0}])[0] == "ST_ALREADY_WITHHELD_CONFIRMED"
|
||||
|
||||
|
||||
def test_role_uses_objective_evidence_only():
|
||||
assert product_role("MATERIA-PRIMA", False, False, False, False, False)[0] == "INPUT"
|
||||
assert product_role("VENDA", True, False, False, False, False)[0] == "OWN_PRODUCTION"
|
||||
assert product_role("VENDA", False, False, False, False, True)[0] == "RESALE"
|
||||
assert product_role("VENDA", False, False, False, False, False)[0] == "UNKNOWN"
|
||||
assert product_role("VENDA", True, False, False, False, True)[0] == "UNKNOWN"
|
||||
|
||||
|
||||
def test_csosn_never_assumes_102_and_never_writes():
|
||||
for st in ("ST_CANDIDATE", "ST_UNKNOWN"):
|
||||
r = csosn_v2("102", "102", "EXACT_NCM", st, "ST_WITHHOLDING_UNKNOWN", "RESALE", DOM)
|
||||
assert r["status"] == "CSOSN_CONTEXT_REQUIRED" and r["proposed_csosn"] is None
|
||||
r = csosn_v2("102", "102", "EXACT_NCM", "ST_NOT_APPLICABLE", "ST_WITHHOLDING_UNKNOWN", "RESALE", DOM)
|
||||
assert r["status"] == "CSOSN_CANDIDATE" and not r["write_eligible"]
|
||||
assert csosn_v2("102", "102", "EXACT_NCM", "ST_NOT_APPLICABLE", "ST_WITHHOLDING_UNKNOWN", "UNKNOWN", DOM)["status"] == "CSOSN_CONTEXT_REQUIRED"
|
||||
assert csosn_v2("102", "102", "LEGACY_NCM", "ST_NOT_APPLICABLE", "ST_WITHHOLDING_UNKNOWN", "RESALE", DOM)["status"] == "CSOSN_UNRESOLVED"
|
||||
|
||||
|
||||
def test_csosn_resolved_only_with_st_withholding_and_role_proven():
|
||||
r = csosn_v2("102", "102", "EXACT_NCM", "ST_CONFIRMED", "ST_ALREADY_WITHHELD_CONFIRMED", "RESALE", DOM)
|
||||
assert r["status"] == "CSOSN_RESOLVED" and r["proposed_csosn"] == "500" and r["write_eligible"] is False
|
||||
assert csosn_v2("102", "102", "EXACT_NCM", "ST_CONFIRMED", "ST_WITHHOLDING_UNKNOWN", "RESALE", DOM)["status"] == "CSOSN_CONTEXT_REQUIRED"
|
||||
assert csosn_v2("500", "102", "EXACT_NCM", "ST_CONFIRMED", "ST_ALREADY_WITHHELD_NOT_CONFIRMED", "RESALE", DOM)["status"] == "CSOSN_CONFLICT"
|
||||
|
||||
|
||||
def test_conflict_classes():
|
||||
assert st_conflict_class("CEST_CANDIDATES", "MVA_RESOLVED", 4, "", "03.021.03", "5102", True, False)[0] == "ERP_HISTORY_CONFLICT"
|
||||
assert st_conflict_class(None, "NO_MVA_MAPPING_FOUND", None, "", None, "5405", False, True)[0] == "BA_ST_RULE_CONFLICT"
|
||||
assert st_conflict_class("CEST_DESCRIPTION_MISMATCH", "MVA_RESOLVED", 6, "", None, "5102", False, False)[0] == "DESCRIPTION_MISMATCH"
|
||||
assert st_conflict_class(None, "MVA_RESOLVED", 6, "", None, "5102", False, False)[0] == "NO_EVIDENCE"
|
||||
@@ -0,0 +1,132 @@
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from central.app.jobs import claim_next_job, complete_job, create_job, fail_job
|
||||
from central.app.main import create_app
|
||||
from central.app.models import Agent, Base, Job, Tenant
|
||||
|
||||
|
||||
def _make_agent(session):
|
||||
tenant = Tenant(name="development")
|
||||
session.add(tenant); session.flush()
|
||||
agent = Agent(tenant_id=tenant.id, name="agent-a", token_hash="hash-a")
|
||||
other = Agent(tenant_id=tenant.id, name="agent-b", token_hash="hash-b")
|
||||
session.add_all([agent, other]); session.commit()
|
||||
return agent, other
|
||||
|
||||
|
||||
def test_claim_is_scoped_to_owning_agent_and_marks_running():
|
||||
engine = create_engine("sqlite://")
|
||||
Base.metadata.create_all(engine)
|
||||
with Session(engine) as session:
|
||||
agent, other = _make_agent(session)
|
||||
job = create_job(session, agent, "REPROCESS_ALL")
|
||||
assert claim_next_job(session, other) is None # other agent never sees it
|
||||
claimed = claim_next_job(session, agent)
|
||||
assert claimed.id == job.id
|
||||
assert claimed.status == "RUNNING"
|
||||
assert claimed.attempts == 1
|
||||
|
||||
|
||||
def test_claim_is_not_reentrant_once_running():
|
||||
engine = create_engine("sqlite://")
|
||||
Base.metadata.create_all(engine)
|
||||
with Session(engine) as session:
|
||||
agent, _ = _make_agent(session)
|
||||
create_job(session, agent, "REPROCESS_ALL")
|
||||
first = claim_next_job(session, agent)
|
||||
second = claim_next_job(session, agent) # repeated polling must not duplicate consumption
|
||||
assert first is not None
|
||||
assert second is None
|
||||
|
||||
|
||||
def test_completed_job_can_never_run_again():
|
||||
engine = create_engine("sqlite://")
|
||||
Base.metadata.create_all(engine)
|
||||
with Session(engine) as session:
|
||||
agent, _ = _make_agent(session)
|
||||
create_job(session, agent, "REPROCESS_ALL")
|
||||
job = claim_next_job(session, agent)
|
||||
assert complete_job(session, job, {"writes": 0}) is True
|
||||
assert session.get(Job, job.id).status == "COMPLETED"
|
||||
assert claim_next_job(session, agent) is None
|
||||
assert complete_job(session, job, {"writes": 1}) is False # already terminal
|
||||
|
||||
|
||||
def test_failed_job_is_terminal_with_sanitized_error():
|
||||
engine = create_engine("sqlite://")
|
||||
Base.metadata.create_all(engine)
|
||||
with Session(engine) as session:
|
||||
agent, _ = _make_agent(session)
|
||||
create_job(session, agent, "REPROCESS_ALL")
|
||||
job = claim_next_job(session, agent)
|
||||
assert fail_job(session, job, "boom " * 1000) is True
|
||||
stored = session.get(Job, job.id)
|
||||
assert stored.status == "FAILED"
|
||||
assert len(stored.error) <= 2000
|
||||
|
||||
|
||||
def _client():
|
||||
engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
|
||||
Base.metadata.create_all(engine)
|
||||
app = create_app(session_factory=lambda: Session(engine), bootstrap_token="test-token")
|
||||
return TestClient(app), engine
|
||||
|
||||
|
||||
def test_job_api_end_to_end_flow():
|
||||
client, engine = _client()
|
||||
headers = {"Authorization": "Bearer test-token"}
|
||||
|
||||
heartbeat = client.post("/v1/agents/heartbeat", json={"status": "IDLE", "schema_fingerprint": "abc", "agent_version": "0.1.0"}, headers=headers)
|
||||
assert heartbeat.status_code == 200
|
||||
assert heartbeat.json()["enabled"] is True
|
||||
|
||||
created = client.post("/v1/jobs", json={"kind": "REPROCESS_PRODUCTS", "payload": {"codigos": ["000026"]}}, headers=headers)
|
||||
assert created.status_code == 200
|
||||
job_id = created.json()["job_id"]
|
||||
assert created.json()["status"] == "PENDING"
|
||||
|
||||
claimed = client.get("/v1/jobs/next", headers=headers)
|
||||
assert claimed.json()["job_id"] == job_id
|
||||
assert claimed.json()["status"] == "RUNNING"
|
||||
|
||||
empty = client.get("/v1/jobs/next", headers=headers)
|
||||
assert empty.json() is None # no more pending work; not the same job again
|
||||
|
||||
completed = client.post(f"/v1/jobs/{job_id}/complete", json={"result_summary": {"writes_succeeded": 1}}, headers=headers)
|
||||
assert completed.json()["status"] == "COMPLETED"
|
||||
assert completed.json()["result_summary"]["writes_succeeded"] == 1
|
||||
|
||||
fetched = client.get(f"/v1/jobs/{job_id}", headers=headers)
|
||||
assert fetched.json()["status"] == "COMPLETED"
|
||||
|
||||
|
||||
def test_agent_status_derives_online_offline():
|
||||
client, engine = _client()
|
||||
headers = {"Authorization": "Bearer test-token"}
|
||||
|
||||
offline = client.get("/v1/agents/me", headers=headers)
|
||||
assert offline.json()["status"] == "OFFLINE" # never heartbeated yet
|
||||
|
||||
client.post("/v1/agents/heartbeat", json={"status": "IDLE"}, headers=headers)
|
||||
online = client.get("/v1/agents/me", headers=headers)
|
||||
body = online.json()
|
||||
assert body["status"] == "ONLINE"
|
||||
assert body["enabled"] is True
|
||||
assert "company_id" in body
|
||||
|
||||
|
||||
def test_job_creation_rejects_unknown_kind():
|
||||
client, _ = _client()
|
||||
headers = {"Authorization": "Bearer test-token"}
|
||||
response = client.post("/v1/jobs", json={"kind": "DELETE_EVERYTHING"}, headers=headers)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_reprocess_products_requires_codigos():
|
||||
client, _ = _client()
|
||||
headers = {"Authorization": "Bearer test-token"}
|
||||
response = client.post("/v1/jobs", json={"kind": "REPROCESS_PRODUCTS", "payload": {}}, headers=headers)
|
||||
assert response.status_code == 400
|
||||
@@ -0,0 +1,38 @@
|
||||
from central.app.field_contract import ALREADY, CHANGE, MANUAL, NA, sped_pis_cofins_match
|
||||
from central.tests.test_field_contract import CO1, CO3, EV, decision, product, run, st
|
||||
|
||||
UNUSED = {**CO1, "pis_cofins_unused_by_emission": True}
|
||||
|
||||
|
||||
def test_sped_table_lookup_is_prefix_based_and_active_only():
|
||||
hits = sped_pis_cofins_match("10063021")
|
||||
assert hits and {h[1] for h in hits} == {"06"} and hits[0][0] == "4.3.13"
|
||||
assert sped_pis_cofins_match("99999999") == []
|
||||
|
||||
|
||||
def test_simples_pis_cofins_written_only_from_official_table_and_never_assumed_zero():
|
||||
r = run(p=product(CLASSIFICACAO_FISCAL="10063021"), co=UNUSED)
|
||||
assert st(r, "COD_SAIDA_PIS") == (CHANGE, "06") and st(r, "COD_SAIDA_COFINS") == (CHANGE, "06")
|
||||
assert r["ALIQ_PIS"]["state"] == NA and r["COD_ENTRADA_PIS"]["state"] == NA
|
||||
plain = run(co=UNUSED) # NCM without any SPED special regime: SGV does not consume the field, nothing is invented
|
||||
assert all(plain[f]["state"] == NA and plain[f]["proposed"] is None for f in ("COD_SAIDA_PIS", "ALIQ_PIS", "COD_ENTRADA_COFINS"))
|
||||
assert run(co=CO1)["COD_SAIDA_PIS"]["state"] == MANUAL # without emission evidence the company question remains
|
||||
assert run(co={**CO3, "pis_cofins_unused_by_emission": True})["COD_SAIDA_PIS"]["state"] == MANUAL # evidence only relaxes CRT 1
|
||||
|
||||
|
||||
def test_cfop_never_contradicts_the_erp_st_declaration():
|
||||
r = run(p=product(CFOP="5405", CSOSN="500", CST="060", SITUACAO_TRIBUTARIA="T"))
|
||||
assert r["CFOP"]["state"] != CHANGE or r["CFOP"]["proposed"] != "5102"
|
||||
|
||||
|
||||
def test_kitchen_flag_alone_and_cst_digit_never_prove_role_or_origin():
|
||||
r = run(p=product(PROD_COZINHA=1, COD_ORIGEM_MERCADORIA=None, CST="040"))
|
||||
assert r["COD_ORIGEM_MERCADORIA"]["state"] == MANUAL and r["COD_ORIGEM_MERCADORIA"]["reason"] == "ORIGIN_NO_EVIDENCE"
|
||||
assert r["IPPT"]["proposed"] != "P" or r["IPPT"]["state"] != CHANGE
|
||||
|
||||
|
||||
def test_store_ipi_profile_is_a_company_decision_never_defaulted():
|
||||
undecided = run(ipi_profile=None)
|
||||
assert undecided["IPI"]["state"] == MANUAL and undecided["IPI"]["input_key"] == "store_ipi_profile"
|
||||
assert run(ipi_profile={"scope": "COMPANY", "scope_key": "", "key": "store_ipi_profile", "value": "LEAVE_NOT_APPLICABLE"})["IPI"]["state"] == NA
|
||||
assert run()["IPI"]["state"] == CHANGE # POPULATE_REFERENCE_RATE keeps the TIPI rule
|
||||
@@ -0,0 +1,66 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from central.app.datasets.ncm import parse_ncm_payload
|
||||
|
||||
|
||||
PAYLOAD = b'{"Data_Ultima_Atualizacao_NCM":"2026-09-17","Ato":"Ato","Nomenclaturas":[{"Codigo":"08043000","Descricao":"Abacaxis frescos","Data_Inicio":"2022-01-01","Data_Fim":"9999-12-31"}]}'
|
||||
|
||||
|
||||
def test_import_promotes_and_is_idempotent():
|
||||
from central.app.datasets.service import DatasetService
|
||||
from central.app.models import Base, Dataset, NcmEntry
|
||||
|
||||
engine = create_engine("sqlite://")
|
||||
Base.metadata.create_all(engine)
|
||||
with Session(engine) as session:
|
||||
service = DatasetService(session)
|
||||
first = service.import_payload(parse_ncm_payload(PAYLOAD), "https://source", datetime.now(timezone.utc), minimum_entries=1)
|
||||
second = service.import_payload(parse_ncm_payload(PAYLOAD), "https://source", datetime.now(timezone.utc), minimum_entries=1)
|
||||
assert first.id == second.id
|
||||
assert session.get(Dataset, first.id).status == "ACTIVE"
|
||||
assert session.query(NcmEntry).filter_by(dataset_id=first.id).count() == 1
|
||||
|
||||
|
||||
def test_classifier_validates_ncm_and_never_proposes_fiscal_fields():
|
||||
from central.app.classification import Classifier
|
||||
|
||||
class Repository:
|
||||
def active_entries(self):
|
||||
return [type("Entry", (), {"code": "08043000", "description": "Abacaxis frescos"})()]
|
||||
|
||||
def dataset_versions(self):
|
||||
return {"ncm": "dataset-1"}
|
||||
|
||||
result = Classifier(Repository()).classify({"description": "abacaxi", "current_ncm": "0804.30.00"})
|
||||
assert result["status"] == "EXACT_NCM"
|
||||
assert result["classification"]["ncm"] == "08043000"
|
||||
assert result["classification"]["cest"] is None
|
||||
assert result["can_auto_apply"] is False
|
||||
|
||||
|
||||
def test_api_auth_and_cache():
|
||||
from central.app.main import create_app
|
||||
from central.app.models import Base, Dataset, NcmEntry
|
||||
|
||||
engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
|
||||
Base.metadata.create_all(engine)
|
||||
with Session(engine) as session:
|
||||
dataset = Dataset(kind="NCM", provider="Siscomex", sha256="a" * 64, version_label="v1", act="act", source_url="url", status="ACTIVE", record_count=1)
|
||||
session.add(dataset)
|
||||
session.flush()
|
||||
session.add(NcmEntry(dataset_id=dataset.id, code="08043000", description="Abacaxis frescos", valid_from="2022", valid_to="9999"))
|
||||
session.commit()
|
||||
app = create_app(session_factory=lambda: Session(engine), bootstrap_token="test-token")
|
||||
client = TestClient(app)
|
||||
payload = {"product": {"product_id": "1", "description": "abacaxi", "current_ncm": "08043000"}}
|
||||
assert client.post("/v1/classify", json=payload).status_code == 401
|
||||
headers = {"Authorization": "Bearer test-token"}
|
||||
first = client.post("/v1/classify", json=payload, headers=headers).json()
|
||||
second = client.post("/v1/classify", json=payload, headers=headers).json()
|
||||
assert first["request_fingerprint"] == second["request_fingerprint"]
|
||||
assert second["provenance"]["cache_hit"] is True
|
||||
@@ -0,0 +1,172 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
|
||||
CEST_HTML = b"""
|
||||
<html><body>
|
||||
<table><tr><td>Ignored</td></tr></table>
|
||||
<h2>ANEXO II</h2>
|
||||
<table>
|
||||
<tr><th>ITEM</th><th>CEST</th><th>NCM/SH</th><th>DESCRICAO</th></tr>
|
||||
<tr><td>1.0</td><td>01.001.00</td><td>0804.30.00</td><td>Abacaxis</td></tr>
|
||||
<tr><td>2.0</td><td>01.002.00</td><td>0804.30.00<br>0207.14.00</td><td>Frutas e aves</td></tr>
|
||||
</table>
|
||||
<h2>ANEXO XXVII</h2>
|
||||
<table><tr><th>CEST</th><th>NCM</th></tr><tr><td>99.999.99</td><td>9999.99.99</td></tr></table>
|
||||
</body></html>
|
||||
"""
|
||||
|
||||
|
||||
def test_cest_parser_accepts_only_covered_annex_tables_and_hashes_raw_html():
|
||||
from central.app.datasets.cest import parse_cest_html
|
||||
|
||||
parsed = parse_cest_html(CEST_HTML)
|
||||
|
||||
assert parsed.version == "CV142_18"
|
||||
assert parsed.sha256
|
||||
assert parsed.entries[0].annex == "II"
|
||||
assert parsed.entries[1].ncm_codes == ("08043000", "02071400")
|
||||
assert len(parsed.entries) == 2
|
||||
|
||||
|
||||
def test_cest_parser_fails_closed_when_an_annex_table_has_no_required_columns():
|
||||
from central.app.datasets.cest import parse_cest_html
|
||||
|
||||
with pytest.raises(ValueError, match="required columns"):
|
||||
parse_cest_html(b"<h2>ANEXO II</h2><table><tr><th>CEST</th></tr><tr><td>01.001.00</td></tr></table>")
|
||||
|
||||
|
||||
def test_cest_parser_accepts_official_four_digit_ncm_headings():
|
||||
from central.app.datasets.cest import parse_cest_html
|
||||
|
||||
parsed = parse_cest_html(b"<h2>ANEXO III</h2><table><tr><th>CEST</th><th>NCM/SH</th></tr><tr><td>02.001.00</td><td>2205</td></tr></table>")
|
||||
|
||||
assert parsed.entries[0].ncm_codes == ("2205",)
|
||||
|
||||
|
||||
def test_cest_parser_ignores_official_editorial_rows_without_cest_data():
|
||||
from central.app.datasets.cest import parse_cest_html
|
||||
|
||||
parsed = parse_cest_html(b"<h2>ANEXO II</h2><table><tr><th>CEST</th><th>NCM/SH</th></tr><tr><td>Nova redacao dada ao item.</td></tr><tr><td>01.001.00</td><td>3815.12.10</td></tr></table>")
|
||||
|
||||
assert [entry.code for entry in parsed.entries] == ["01.001.00"]
|
||||
|
||||
|
||||
def test_cest_parser_preserves_chapter_ncm_prefixes_and_empty_official_ncm_cells():
|
||||
from central.app.datasets.cest import parse_cest_html
|
||||
|
||||
parsed = parse_cest_html(b"<h2>ANEXO XXVI</h2><table><tr><th>CEST</th><th>NCM/SH</th></tr><tr><td>28.055.00</td><td>Capitulo 33</td></tr><tr><td>28.056.00</td><td></td></tr></table>")
|
||||
|
||||
assert parsed.entries[0].ncm_codes == ("33",)
|
||||
assert parsed.entries[1].ncm_codes == ()
|
||||
|
||||
|
||||
def test_classifier_distinguishes_exact_legacy_and_invalid_ncm_and_uses_cest_matches():
|
||||
from central.app.classification import Classifier
|
||||
|
||||
class Repository:
|
||||
def active_entries(self):
|
||||
return [type("Entry", (), {"code": "08043000", "description": "Abacaxis"})()]
|
||||
|
||||
def cest_for_ncm(self, code):
|
||||
return [type("Cest", (), {"code": "01.001.00", "description": "Abacaxis"})()] if code == "08043000" else []
|
||||
|
||||
def legacy_ncm(self, code):
|
||||
return {"code": code, "source": "Siscomex", "source_date": "2026-09-17", "reason": "NCM not present in current official nomenclature; no official successor."} if code == "02071400" else None
|
||||
|
||||
def dataset_versions(self):
|
||||
return {"ncm": "ncm-version", "cest": "cest-version"}
|
||||
|
||||
classifier = Classifier(Repository())
|
||||
|
||||
exact = classifier.classify({"current_ncm": "0804.30.00", "description": "abacaxi"})
|
||||
legacy = classifier.classify({"current_ncm": "0207.14.00"})
|
||||
invalid = classifier.classify({"current_ncm": "0000.00.00"})
|
||||
|
||||
assert exact["status"] == "EXACT_NCM"
|
||||
assert exact["classification"]["cest"] == ["01.001.00"]
|
||||
assert legacy["status"] == "LEGACY_NCM"
|
||||
assert legacy["classification"]["ncm"] == "02071400"
|
||||
assert legacy["legacy_ncm"]["reason"].endswith("no official successor.")
|
||||
assert invalid["status"] == "INVALID_NCM"
|
||||
|
||||
|
||||
def test_classifier_reports_confirmed_cest_only_for_one_distinct_official_candidate():
|
||||
from central.app.classification import Classifier
|
||||
|
||||
class Repository:
|
||||
def active_entries(self):
|
||||
return [type("Entry", (), {"code": "08043000", "description": "Abacaxi"})()]
|
||||
def cest_for_ncm(self, code):
|
||||
return [type("Cest", (), {"code": "01.001.00", "description": "Abacaxi"})()]
|
||||
def legacy_ncm(self, code): return None
|
||||
def dataset_versions(self): return {"ncm": "n", "cest": "c"}
|
||||
|
||||
result = Classifier(Repository()).classify({"current_ncm": "08043000", "description": "Abacaxi"})
|
||||
assert result["cest_status"] == "CEST_CONFIRMED"
|
||||
|
||||
|
||||
def test_classifier_reports_candidates_for_multiple_distinct_cest_codes():
|
||||
from central.app.classification import Classifier
|
||||
|
||||
class Repository:
|
||||
def active_entries(self):
|
||||
return [type("Entry", (), {"code": "08043000", "description": "Abacaxi"})()]
|
||||
def cest_for_ncm(self, code):
|
||||
return [type("Cest", (), {"code": "01.001.00", "description": "Abacaxi"})(), type("Cest", (), {"code": "01.002.00", "description": "Frutas"})()]
|
||||
def legacy_ncm(self, code): return None
|
||||
def dataset_versions(self): return {"ncm": "n", "cest": "c"}
|
||||
|
||||
result = Classifier(Repository()).classify({"current_ncm": "08043000", "description": "Abacaxi"})
|
||||
assert result["cest_status"] == "CEST_CANDIDATES"
|
||||
assert result["classification"]["cest"] == ["01.001.00", "01.002.00"]
|
||||
|
||||
|
||||
def test_cest_import_creates_many_to_many_ncm_links_and_stores_html_metadata():
|
||||
from central.app.datasets.cest import parse_cest_html
|
||||
from central.app.datasets.service import DatasetService
|
||||
from central.app.models import Base, CestEntry, CestNcmEntry, Dataset
|
||||
|
||||
engine = create_engine("sqlite://")
|
||||
Base.metadata.create_all(engine)
|
||||
with Session(engine) as session:
|
||||
dataset = DatasetService(session).import_cest_payload(
|
||||
parse_cest_html(CEST_HTML), "https://www.confaz.fazenda.gov.br/legislacao/convenios/2018/CV142_18", datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
assert dataset.kind == "CEST"
|
||||
assert dataset.raw_sha256 == dataset.sha256
|
||||
assert session.query(CestEntry).filter_by(dataset_id=dataset.id).count() == 2
|
||||
assert session.query(CestNcmEntry).count() == 3
|
||||
|
||||
|
||||
def test_cest_lookup_matches_official_ncm_prefix_links():
|
||||
from central.app.datasets.cest import parse_cest_html
|
||||
from central.app.datasets.service import DatasetService
|
||||
from central.app.models import Base
|
||||
|
||||
engine = create_engine("sqlite://")
|
||||
Base.metadata.create_all(engine)
|
||||
with Session(engine) as session:
|
||||
service = DatasetService(session)
|
||||
service.import_cest_payload(parse_cest_html(b"<h2>ANEXO III</h2><table><tr><th>CEST</th><th>NCM</th></tr><tr><td>02.001.00</td><td>2205</td></tr></table>"), "https://source", datetime.now(timezone.utc))
|
||||
|
||||
assert [entry.code for entry in service.cest_for_ncm("22051000")] == ["02.001.00"]
|
||||
|
||||
|
||||
def test_cest_import_unions_repeated_official_cest_codes_into_one_link_set():
|
||||
from central.app.datasets.cest import parse_cest_html
|
||||
from central.app.datasets.service import DatasetService
|
||||
from central.app.models import Base, CestEntry, CestNcmEntry
|
||||
|
||||
payload = parse_cest_html(b"<h2>ANEXO II</h2><table><tr><th>CEST</th><th>NCM</th></tr><tr><td>01.042.00</td><td>8421.32.00</td></tr><tr><td>01.042.00</td><td>8421.39.20</td></tr></table>")
|
||||
engine = create_engine("sqlite://")
|
||||
Base.metadata.create_all(engine)
|
||||
with Session(engine) as session:
|
||||
dataset = DatasetService(session).import_cest_payload(payload, "https://source", datetime.now(timezone.utc))
|
||||
|
||||
assert session.query(CestEntry).filter_by(dataset_id=dataset.id).count() == 1
|
||||
assert {link.ncm_code for link in session.query(CestNcmEntry).all()} == {"84213200", "84213920"}
|
||||
@@ -0,0 +1,17 @@
|
||||
import pytest
|
||||
|
||||
from central.app.datasets.ncm import parse_ncm_payload
|
||||
|
||||
|
||||
def test_parser_accepts_verified_siscomex_payload_shape():
|
||||
payload = b'{"Data_Ultima_Atualizacao_NCM":"Vigente em 17/09/2026","Ato":"Resolucao Gecex 926/2026","Nomenclaturas":[{"Codigo":"08043000","Descricao":"Abacaxis.","Data_Inicio":"01/04/2022","Data_Fim":"31/12/9999"}]}'
|
||||
|
||||
parsed = parse_ncm_payload(payload)
|
||||
|
||||
assert parsed.version == "Vigente em 17/09/2026"
|
||||
assert parsed.entries[0].code == "08043000"
|
||||
|
||||
|
||||
def test_parser_rejects_unknown_payload_shape():
|
||||
with pytest.raises(ValueError, match="Nomenclaturas"):
|
||||
parse_ncm_payload(b'{"items": []}')
|
||||
@@ -0,0 +1,93 @@
|
||||
from datetime import datetime, timezone
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from central.app.datasets.nfe_moc_tables import parse_moc_tables
|
||||
from central.app.datasets.service import DatasetService
|
||||
from central.app.models import Base
|
||||
|
||||
SOURCE = Path(__file__).parents[2] / "sources" / "raw" / "bootstrap_v1" / "moc7-anexo-i-leiaute-e-rv.pdf"
|
||||
pytestmark = pytest.mark.skipif(not SOURCE.exists(), reason="official MOC PDF not present locally")
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _payload():
|
||||
# Parsing this 153-page PDF is expensive (~40s); every test in this module
|
||||
# shares one parse instead of re-running it per test.
|
||||
return parse_moc_tables(SOURCE.read_bytes())
|
||||
|
||||
|
||||
def test_parses_all_five_domains_with_expected_minimum_coverage():
|
||||
payload = _payload()
|
||||
from collections import Counter
|
||||
counts = Counter(e.domain for e in payload.entries)
|
||||
assert counts["ORIGEM_MERCADORIA"] == 9
|
||||
assert counts["CST_ICMS"] >= 11
|
||||
assert counts["CSOSN"] >= 10
|
||||
assert counts["CST_PIS"] >= 29
|
||||
assert counts["CST_COFINS"] >= 29
|
||||
|
||||
|
||||
def test_every_entry_has_provenance_and_no_invented_text():
|
||||
payload = _payload()
|
||||
for entry in payload.entries:
|
||||
assert entry.source_section.startswith("MOC7 p.")
|
||||
assert entry.description.strip()
|
||||
assert entry.code.strip()
|
||||
|
||||
|
||||
def test_origem_codes_are_0_through_8():
|
||||
payload = _payload()
|
||||
codes = sorted(e.code for e in payload.entries if e.domain == "ORIGEM_MERCADORIA")
|
||||
assert codes == [str(n) for n in range(9)]
|
||||
|
||||
|
||||
def test_cst_icms_includes_split_group_40_41_50():
|
||||
payload = _payload()
|
||||
by_code = {e.code: e for e in payload.entries if e.domain == "CST_ICMS"}
|
||||
assert "isenta" in by_code["40"].description.lower()
|
||||
assert "tributada" in by_code["41"].description.lower()
|
||||
assert "suspens" in by_code["50"].description.lower()
|
||||
|
||||
|
||||
def test_reimport_is_idempotent():
|
||||
payload = _payload()
|
||||
engine = create_engine("sqlite://")
|
||||
Base.metadata.create_all(engine)
|
||||
with Session(engine) as session:
|
||||
service = DatasetService(session)
|
||||
first = service.import_moc_tables(payload, "file://moc7.pdf", datetime.now(timezone.utc))
|
||||
second = service.import_moc_tables(payload, "file://moc7.pdf", datetime.now(timezone.utc))
|
||||
assert first.id == second.id
|
||||
assert second.status == "ACTIVE"
|
||||
|
||||
|
||||
def test_lookup_smoke_for_each_domain():
|
||||
payload = _payload()
|
||||
engine = create_engine("sqlite://")
|
||||
Base.metadata.create_all(engine)
|
||||
with Session(engine) as session:
|
||||
service = DatasetService(session)
|
||||
service.import_moc_tables(payload, "file://moc7.pdf", datetime.now(timezone.utc))
|
||||
|
||||
assert len(service.fiscal_codes("ORIGEM_MERCADORIA")) == 9
|
||||
assert service.fiscal_codes("ORIGEM_MERCADORIA", code="0")[0].code == "0"
|
||||
assert service.fiscal_codes("CST_ICMS", code="00")
|
||||
assert service.fiscal_codes("CSOSN", code="101")
|
||||
assert service.fiscal_codes("CST_PIS", code="01")
|
||||
assert service.fiscal_codes("CST_COFINS", code="01")
|
||||
assert service.fiscal_codes("CST_ICMS", code="99") == []
|
||||
|
||||
|
||||
def test_dataset_versions_includes_nfe_moc_tables_and_changes_hash_on_reimport():
|
||||
engine = create_engine("sqlite://")
|
||||
Base.metadata.create_all(engine)
|
||||
with Session(engine) as session:
|
||||
service = DatasetService(session)
|
||||
assert service.dataset_versions()["nfe_moc_tables"] is None
|
||||
service.import_moc_tables(_payload(), "file://moc7.pdf", datetime.now(timezone.utc))
|
||||
assert service.dataset_versions()["nfe_moc_tables"] is not None
|
||||
@@ -0,0 +1,73 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from central.app.product_defaults import cfop_default, cst_status, csosn_default, validate_against_history
|
||||
|
||||
CFOPS = {"5102", "5405", "5656", "1102"}
|
||||
CSOSNS = {"101", "102", "103", "500", "400"}
|
||||
|
||||
|
||||
def P(**kw):
|
||||
return {"CFOP": "5102", "CSOSN": "102", "CST": "040", **kw}
|
||||
|
||||
|
||||
def test_cfop_consistent_non_st_is_resolved_but_never_a_write():
|
||||
r = cfop_default(P(), "EXACT_NCM", "NO_MVA_MAPPING_FOUND", CFOPS)
|
||||
assert r["status"] == "CFOP_DEFAULT_RESOLVED" and r["action_class"] == "ALREADY_CORRECT" and not r["write_eligible"]
|
||||
|
||||
|
||||
def test_cfop_st_conflict_is_candidate_review_not_write():
|
||||
r = cfop_default(P(), "EXACT_NCM", "MVA_RESOLVED", CFOPS)
|
||||
assert r["status"] == "CFOP_DEFAULT_CANDIDATE" and r["proposed_value"] == "5405" and r["action_class"] == "REVIEW_REQUIRED" and not r["write_eligible"]
|
||||
|
||||
|
||||
def test_cfop_insufficient_context_never_resolved():
|
||||
assert cfop_default(P(), "EXACT_NCM", "MVA_CANDIDATES", CFOPS)["status"] == "CFOP_OPERATION_CONTEXT_REQUIRED"
|
||||
assert cfop_default(P(CFOP=None), "EXACT_NCM", "NO_MVA_MAPPING_FOUND", CFOPS)["status"] == "CFOP_OPERATION_CONTEXT_REQUIRED"
|
||||
assert cfop_default(P(CFOP="6102"), "EXACT_NCM", "NO_MVA_MAPPING_FOUND", CFOPS | {"6102"})["status"] == "CFOP_OPERATION_CONTEXT_REQUIRED"
|
||||
assert cfop_default(P(), "LEGACY_NCM", "NO_MVA_MAPPING_FOUND", CFOPS)["status"] == "CFOP_UNRESOLVED"
|
||||
assert cfop_default(P(CFOP="9999"), "EXACT_NCM", "NO_MVA_MAPPING_FOUND", CFOPS)["status"] == "CFOP_UNRESOLVED"
|
||||
|
||||
|
||||
def test_csosn_is_never_resolved_and_never_universal_102():
|
||||
for mva in ("MVA_RESOLVED", "MVA_CANDIDATES", "NO_MVA_MAPPING_FOUND"):
|
||||
r = csosn_default(P(), "EXACT_NCM", mva, CSOSNS, "102")
|
||||
assert r["status"] != "CSOSN_DEFAULT_RESOLVED" and not r["write_eligible"]
|
||||
r = csosn_default(P(CSOSN="101"), "EXACT_NCM", "NO_MVA_MAPPING_FOUND", CSOSNS, "102")
|
||||
assert r["proposed_value"] is None
|
||||
|
||||
|
||||
def test_csosn_st_subject_flags_conflict_and_requires_retention_context():
|
||||
r = csosn_default(P(), "EXACT_NCM", "MVA_RESOLVED", CSOSNS, "102")
|
||||
assert r["status"] == "CSOSN_OPERATION_CONTEXT_REQUIRED" and r["consistency_flag"] == "ST_SUBJECT_BUT_CSOSN_NOT_500"
|
||||
|
||||
|
||||
def test_history_validates_but_does_not_create_rules():
|
||||
assert validate_against_history("5102", ["5102", "5102"]) == "MATCH"
|
||||
assert validate_against_history("5102", ["5102", "5405"]) == "MISMATCH"
|
||||
assert validate_against_history("5102", []) == "UNVALIDATED"
|
||||
assert validate_against_history(None, ["5102"]) == "UNVALIDATED" # history alone never proposes a value
|
||||
|
||||
|
||||
def test_cst_is_reference_only_under_crt1():
|
||||
r = cst_status(P(), {"40", "00"}, set("012345678"))
|
||||
assert r["status"] == "CST_REFERENCE_ONLY" and r["origin_digit"] == "0" and not r["write_eligible"]
|
||||
assert cst_status(P(CST="99"), {"40"}, set("0"))["status"] == "CST_INVALID_REVIEW"
|
||||
|
||||
|
||||
def test_engine_is_deterministic_idempotent():
|
||||
a = cfop_default(P(), "EXACT_NCM", "MVA_RESOLVED", CFOPS)
|
||||
assert a == cfop_default(P(), "EXACT_NCM", "MVA_RESOLVED", CFOPS)
|
||||
|
||||
|
||||
REC = Path(__file__).resolve().parents[2] / "docs" / "NCM_HISTORICAL_RECONCILIATION_V1.json"
|
||||
|
||||
|
||||
@pytest.mark.skipif(not REC.exists(), reason="reconciliation artifact not generated")
|
||||
def test_reconciliation_accounts_for_every_mismatch_and_finds_no_technical_bug():
|
||||
d = json.loads(REC.read_text(encoding="utf-8"))
|
||||
assert sum(d["classification"].values()) == d["mismatches_total"]
|
||||
assert d["classification"].get("NORMALIZATION_BUG", 0) == 0
|
||||
assert d["trusted_corpus"]["TRUSTED_HISTORICAL_ITEMS"] > 0
|
||||
@@ -0,0 +1,53 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from central.app.datasets.service import DatasetService
|
||||
from central.app.main import create_app
|
||||
from central.app.models import AnpProductCode, Base, CfopEntry, Dataset, FcpRate, NcmUtribEntry
|
||||
|
||||
|
||||
def _client_with_reference_data():
|
||||
engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
|
||||
Base.metadata.create_all(engine)
|
||||
with Session(engine) as session:
|
||||
dataset = Dataset(kind="CFOP", provider="p", sha256="a" * 64, version_label="v", act="a", source_url="u", status="ACTIVE", record_count=1)
|
||||
session.add(dataset); session.flush()
|
||||
session.add(CfopEntry(dataset_id=dataset.id, code="1101", title="Compra", description="desc"))
|
||||
|
||||
d2 = Dataset(kind="NCM_UTRIB", provider="p", sha256="b" * 64, version_label="v", act="a", source_url="u", status="ACTIVE", record_count=1)
|
||||
session.add(d2); session.flush()
|
||||
session.add(NcmUtribEntry(dataset_id=d2.id, ncm="01012100", utrib_abbreviation="UN", utrib_description="Unidade"))
|
||||
|
||||
d3 = Dataset(kind="ANP", provider="p", sha256="c" * 64, version_label="v", act="a", source_url="u", status="ACTIVE", record_count=1)
|
||||
session.add(d3); session.flush()
|
||||
session.add(AnpProductCode(dataset_id=d3.id, cprod_anp="110203073"))
|
||||
|
||||
d4 = Dataset(kind="FCP", provider="p", sha256="d" * 64, version_label="v", act="a", source_url="u", status="ACTIVE", record_count=1)
|
||||
session.add(d4); session.flush()
|
||||
session.add(FcpRate(dataset_id=d4.id, uf_code="29", uf="BA", uf_name="BAHIA", aliq_1="Fixo:2.00", observacao="obs"))
|
||||
session.commit()
|
||||
app = create_app(session_factory=lambda: Session(engine), bootstrap_token="test-token")
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def test_reference_endpoints_found_and_not_found():
|
||||
client = _client_with_reference_data()
|
||||
headers = {"Authorization": "Bearer test-token"}
|
||||
|
||||
assert client.get("/v1/reference/cfop/1101", headers=headers).json()["found"] is True
|
||||
assert client.get("/v1/reference/cfop/9999", headers=headers).json()["found"] is False
|
||||
|
||||
assert client.get("/v1/reference/ncm_utrib/01012100", headers=headers).json()["found"] is True
|
||||
assert client.get("/v1/reference/ncm_utrib/99999999", headers=headers).json()["found"] is False
|
||||
|
||||
assert client.get("/v1/reference/anp/110203073", headers=headers).json()["found"] is True
|
||||
assert client.get("/v1/reference/anp/000000000", headers=headers).json()["found"] is False
|
||||
|
||||
fcp = client.get("/v1/reference/fcp/BA", headers=headers).json()
|
||||
assert fcp["found"] is True
|
||||
assert fcp["aliq_1"] == "Fixo:2.00"
|
||||
assert client.get("/v1/reference/fcp/ZZ", headers=headers).json()["found"] is False
|
||||
@@ -0,0 +1,77 @@
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from central.app.auth import authenticate, token_hash
|
||||
from central.app.models import Base
|
||||
|
||||
SCRIPT = Path(__file__).parents[1] / "scripts" / "register_agent.py"
|
||||
|
||||
|
||||
def load_module():
|
||||
spec = importlib.util.spec_from_file_location("register_agent", SCRIPT)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
register_agent_module = load_module()
|
||||
|
||||
|
||||
def test_register_agent_creates_tenant_and_hashed_token_only():
|
||||
engine = create_engine("sqlite://")
|
||||
Base.metadata.create_all(engine)
|
||||
with Session(engine) as session:
|
||||
info, token = register_agent_module.register_agent(session, "Restaurante X", "loja-01")
|
||||
found = authenticate(session, token)
|
||||
assert found is not None
|
||||
assert found.id == info["id"]
|
||||
assert found.token_hash == token_hash(token)
|
||||
assert found.token_hash != token # only the hash is ever persisted
|
||||
|
||||
# Mirrors the real CLI's main(): reading the returned info AFTER the
|
||||
# session (and its `with` block) has closed. This is exactly the scenario
|
||||
# that broke when register_agent() returned live ORM objects instead of
|
||||
# plain values (DetachedInstanceError) — must keep working unattached.
|
||||
assert info["enabled"] is True
|
||||
assert isinstance(info["tenant_id"], int)
|
||||
|
||||
|
||||
def test_register_agent_reuses_existing_tenant():
|
||||
engine = create_engine("sqlite://")
|
||||
Base.metadata.create_all(engine)
|
||||
with Session(engine) as session:
|
||||
info1, _ = register_agent_module.register_agent(session, "Restaurante X", "loja-01")
|
||||
info2, _ = register_agent_module.register_agent(session, "Restaurante X", "loja-02")
|
||||
|
||||
assert info1["tenant_id"] == info2["tenant_id"]
|
||||
assert info1["id"] != info2["id"]
|
||||
|
||||
|
||||
def test_register_agent_generates_unique_tokens():
|
||||
engine = create_engine("sqlite://")
|
||||
Base.metadata.create_all(engine)
|
||||
with Session(engine) as session:
|
||||
_, token1 = register_agent_module.register_agent(session, "A", "a1")
|
||||
_, token2 = register_agent_module.register_agent(session, "A", "a2")
|
||||
assert token1 != token2
|
||||
|
||||
|
||||
def test_disabled_agent_cannot_authenticate():
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
from central.app.auth import authenticate, token_hash
|
||||
from central.app.models import Agent, Base, Tenant
|
||||
engine = create_engine("sqlite://")
|
||||
Base.metadata.create_all(engine)
|
||||
with Session(engine) as s:
|
||||
tenant = Tenant(name="t"); s.add(tenant); s.flush()
|
||||
agent = Agent(tenant_id=tenant.id, name="a", token_hash=token_hash("secret-token")); s.add(agent); s.commit()
|
||||
assert authenticate(s, "secret-token").id == agent.id
|
||||
agent.enabled = False; s.commit()
|
||||
assert authenticate(s, "secret-token") is None
|
||||
assert authenticate(s, "wrong") is None
|
||||
@@ -0,0 +1,62 @@
|
||||
from datetime import datetime, timezone
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from central.app.datasets import bootstrap_v1_extra as parsers
|
||||
from central.app.datasets.service import DatasetService
|
||||
from central.app.models import Base, RtcCstIbsCbsEntry
|
||||
|
||||
ROOT = Path(__file__).parents[2] / "sources" / "raw" / "bootstrap_v1"
|
||||
CCLASS = ROOT / "cClassTrib 2026-06-22.xlsx"
|
||||
CBS = ROOT / "IT 2026.002 v.1.00 - Aliquotas da CBS.xlsx"
|
||||
pytestmark = pytest.mark.skipif(not (CCLASS.exists() and CBS.exists()), reason="RTC bootstrap files not present locally")
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def _cclass():
|
||||
return parsers.parse_rtc_cclasstrib(CCLASS.read_bytes())
|
||||
|
||||
|
||||
def _session():
|
||||
engine = create_engine("sqlite://")
|
||||
Base.metadata.create_all(engine)
|
||||
return Session(engine)
|
||||
|
||||
|
||||
def test_cclasstrib_keys_unique_and_rich_fields_kept():
|
||||
p = _cclass()
|
||||
codes = [e["cclasstrib"] for e in p.entries]
|
||||
assert len(codes) == len(set(codes)) >= 100
|
||||
assert all(len(c) == 6 for c in codes)
|
||||
assert p.entries[0]["indicators"] and p.entries[0]["d_ini_vig"]
|
||||
assert {e["cst"] for e in p.entries} <= {e["cst"] for e in p.extra_entries}
|
||||
|
||||
|
||||
def test_cbs_rates_2026_numeric_and_2027_pending():
|
||||
p = parsers.parse_rtc_cbs_rates(CBS.read_bytes())
|
||||
by_year = {e["year"]: e for e in p.entries}
|
||||
assert by_year[2026]["rate"] == pytest.approx(0.009)
|
||||
assert by_year[2027]["rate"] is None and by_year[2027]["rate_text"]
|
||||
|
||||
|
||||
def test_import_idempotent_lookup_and_versions():
|
||||
with _session() as s:
|
||||
svc = DatasetService(s)
|
||||
assert svc.dataset_versions()["rtc_cclasstrib"] is None
|
||||
now = datetime.now(timezone.utc)
|
||||
a = svc.import_rtc_cclasstrib(_cclass(), "file://x", now)
|
||||
b = svc.import_rtc_cclasstrib(_cclass(), "file://x", now)
|
||||
assert a.id == b.id and b.status == "ACTIVE" and b.record_count == len(_cclass().entries)
|
||||
assert s.query(RtcCstIbsCbsEntry).count() == len(_cclass().extra_entries)
|
||||
assert svc.rtc_cclasstrib(code="000001")
|
||||
|
||||
cbs_payload = parsers.parse_rtc_cbs_rates(CBS.read_bytes())
|
||||
c = svc.import_rtc_cbs_rates(cbs_payload, "file://y", now)
|
||||
assert svc.import_rtc_cbs_rates(cbs_payload, "file://y", now).id == c.id
|
||||
assert svc.rtc_cbs_rate(2026).rate == pytest.approx(0.009)
|
||||
v = svc.dataset_versions()
|
||||
assert v["rtc_cclasstrib"] and v["rtc_cbs_rates"]
|
||||
@@ -0,0 +1,29 @@
|
||||
from io import BytesIO
|
||||
|
||||
import openpyxl
|
||||
|
||||
|
||||
def test_tipi_parser_requires_official_columns_and_reads_ncm_rows():
|
||||
from central.app.datasets.tipi import parse_tipi_xlsx
|
||||
|
||||
workbook = openpyxl.Workbook()
|
||||
sheet = workbook.active
|
||||
sheet.append(["NCM", "EX", "DESCRIÇÃO", "ALÍQUOTA (%)"])
|
||||
sheet.append(["0101.21.00", None, "Reprodutores", "0"])
|
||||
raw = BytesIO(); workbook.save(raw)
|
||||
|
||||
payload = parse_tipi_xlsx(raw.getvalue())
|
||||
|
||||
assert payload.entries[0].ncm == "01012100"
|
||||
assert payload.entries[0].rate == "0"
|
||||
|
||||
|
||||
def test_tipi_parser_preserves_ex_in_the_ncm_key():
|
||||
from central.app.datasets.tipi import parse_tipi_xlsx
|
||||
workbook = openpyxl.Workbook(); sheet = workbook.active
|
||||
sheet.append(["NCM", "EX", "DESCRIÇÃO", "ALÍQUOTA (%)"])
|
||||
sheet.append(["0101.21.00", "01", "Com EX", "NT"])
|
||||
raw = BytesIO(); workbook.save(raw)
|
||||
entry = parse_tipi_xlsx(raw.getvalue()).entries[0]
|
||||
assert entry.ex == "01"
|
||||
assert entry.rate == "NT"
|
||||
Reference in New Issue
Block a user