feat: extract pure fiscal core from sowai-auto (parity-preserving copy)

Copies the 6 pure fiscal modules (domains, chave_acesso, tpag, presets,
resolver, xml_builder) from sowai-auto's backend/app/modules/fiscal/ into
this standalone lib, with only the two mechanical changes required to drop
the app.* dependency: import prefix rename, and TaxRule (ORM) -> TaxRuleLike
(structural Protocol) in the resolver. No other line changes -- byte-level
parity with the auto is the point.

Ports the pure test suites (domains, chave_acesso, resolver with a local
FakeTaxRule satisfying TaxRuleLike, xml_builder) plus a new test_presets_data
covering the PRESETS dict directly (the auto's equivalent test exercises the
apply-preset HTTP/DB flow, which isn't part of the extracted pure core).

55 tests pass locally via `uv run pytest`, no Postgres/k8s required.
This commit is contained in:
jonatanritter
2026-07-22 15:05:10 -03:00
commit 032151af34
17 changed files with 2413 additions and 0 deletions
+217
View File
@@ -0,0 +1,217 @@
"""Resolvedor puro do Bloco B. Sem banco, sem I/O — TaxRule montada em
memória. Os dois perfis REAIS da Thiago (extração do iCode,
thoughts/2026-07-13-icode-config-fiscal-extraida.md) são casos de teste
literais: CSOSN 102 → 5102/6102; CSOSN 500 → 5405/6403."""
import uuid
from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal
import pytest
from sowai_fiscal.resolver import (
AmbiguousRuleError,
FiscalConfigError,
FiscalItem,
FiscalOperation,
resolve_fiscal,
)
_PROFILE = uuid.uuid4()
_ORG = uuid.uuid4()
@dataclass
class FakeTaxRule:
"""Satisfaz `sowai_fiscal.types.TaxRuleLike` por estrutura — o
substituto, nos testes da lib, do ORM `TaxRule` que o auto usa."""
id: uuid.UUID
organization_id: uuid.UUID
tax_profile_id: uuid.UUID
tax_domain: str
crt: str | None = None
uf_destino_tipo: str | None = None
consumidor_final: bool | None = None
indicador_ie: str | None = None
tipo_operacao: str | None = None
ncm_prefix: str | None = None
cest: str | None = None
cst: str | None = None
csosn: str | None = None
cfop: str | None = None
base_calc_percent: Decimal | None = None
aliquota: Decimal | None = None
mva: Decimal | None = None
aliquota_st: Decimal | None = None
fcp_percent: Decimal | None = None
codigo_beneficio: str | None = None
deleted_at: datetime | None = None
def _rule(**kw) -> FakeTaxRule:
defaults = dict(
id=uuid.uuid4(), organization_id=_ORG, tax_profile_id=_PROFILE,
)
return FakeTaxRule(**{**defaults, **kw})
def _op(**kw) -> FiscalOperation:
defaults = dict(
crt="1", uf_origem="PR", uf_destino="PR",
indicador_ie="nao_contribuinte", consumidor_final=True,
tipo_operacao="venda",
item=FiscalItem(
tax_profile_id=_PROFILE, ncm="87089990", cest=None, origem="0",
quantity=Decimal("2"), unit_price=Decimal("75.00"),
),
)
return FiscalOperation(**{**defaults, **kw})
_PADRAO = [ # preset "Padrão" da Thiago (CSOSN 102)
_rule(tax_domain="icms", crt="1", csosn="102", cfop="5102", aliquota=Decimal("0")),
_rule(tax_domain="pis", cst="08", aliquota=Decimal("0")),
_rule(tax_domain="cofins", cst="08", aliquota=Decimal("0")),
]
def test_padrao_interna_resolves_5102_csosn_102():
result = resolve_fiscal(_PADRAO, _op())
assert result.cfop == "5102"
assert result.csosn == "102"
assert result.cst is None
assert result.origem == "0" # ecoado do ITEM (Part), nunca de regra
icms = next(t for t in result.tributos if t.tax_domain == "icms")
assert icms.base_calc == Decimal("150.00")
assert icms.valor == Decimal("0.00")
assert icms.rule_id == _PADRAO[0].id
def test_padrao_interestadual_transposes_to_6102():
result = resolve_fiscal(_PADRAO, _op(uf_destino="SP"))
assert result.cfop == "6102"
def test_st_profile_resolves_5405_and_6403():
st_rules = [_rule(tax_domain="icmsst", crt="1", csosn="500", cfop="5405")]
assert resolve_fiscal(st_rules, _op()).cfop == "5405"
assert resolve_fiscal(st_rules, _op(uf_destino="SP")).cfop == "6403" # NÃO 6405
def test_more_selective_ncm_rule_beats_generic_rule_with_more_matchers():
# A correção do review: NCM (peso 64) vence 3 matchers genéricos
# (crt=1 + uf=4 + indicador_ie=8 = 13).
generic = _rule(
tax_domain="icms", crt="1", uf_destino_tipo="interna",
indicador_ie="nao_contribuinte", csosn="102", cfop="5102",
)
by_ncm = _rule(tax_domain="icms", ncm_prefix="8708", csosn="500", cfop="5405")
result = resolve_fiscal([generic, by_ncm], _op())
assert result.csosn == "500"
def test_rule_with_non_matching_matcher_is_skipped():
rule_sp = _rule(tax_domain="icms", uf_destino_tipo="interestadual", csosn="102", cfop="5102")
with pytest.raises(FiscalConfigError):
resolve_fiscal([rule_sp], _op()) # operação interna; a regra não casa
def test_tie_same_weight_raises_ambiguous():
a = _rule(tax_domain="icms", crt="1", csosn="102", cfop="5102")
b = _rule(tax_domain="icms", crt="1", csosn="101", cfop="5102")
with pytest.raises(AmbiguousRuleError) as exc:
resolve_fiscal([a, b], _op())
assert {a.id, b.id} == set(exc.value.rule_ids)
def test_fail_closed_lists_the_missing_domain():
with pytest.raises(FiscalConfigError) as exc:
resolve_fiscal([], _op())
assert "icms" in " ".join(exc.value.missing)
def test_optional_domain_absent_does_not_block():
# Sem regra de FCP/IPI → simplesmente não entra no resultado.
result = resolve_fiscal(_PADRAO, _op())
assert {t.tax_domain for t in result.tributos} == {"icms", "pis", "cofins"}
def test_st_value_math_with_mva():
# base 100; MVA 40% → base_st 140; aliq_st 18% → 25.20 ICMS próprio 12.00 = 13.20
rules = [
_rule(tax_domain="icms", cst="00", cfop="5102", aliquota=Decimal("12")),
_rule(
tax_domain="icmsst", cst="10", cfop="5405",
mva=Decimal("40"), aliquota_st=Decimal("18"),
),
]
op = _op(crt="3", item=FiscalItem(
tax_profile_id=_PROFILE, ncm="27101259", cest="0600100", origem="0",
quantity=Decimal("1"), unit_price=Decimal("100.00"),
))
result = resolve_fiscal(rules, op)
st = next(t for t in result.tributos if t.tax_domain == "icmsst")
assert st.valor == Decimal("13.20")
def test_reducao_de_base():
# base 100 com redução p/ 60% e alíquota 18% → 10.80
rules = [_rule(
tax_domain="icms", cst="20", cfop="5102",
base_calc_percent=Decimal("60"), aliquota=Decimal("18"),
)]
result = resolve_fiscal(rules, _op(crt="3", item=FiscalItem(
tax_profile_id=_PROFILE, ncm="87089990", cest=None, origem="0",
quantity=Decimal("1"), unit_price=Decimal("100.00"),
)))
icms = next(t for t in result.tributos if t.tax_domain == "icms")
assert icms.valor == Decimal("10.80")
def test_anchor_matches_without_cfop_names_the_field():
"""M5 (review 2026-07-15): a âncora casa (tem csosn) mas não tem CFOP --
`FiscalConfigError.missing` deve nomear o campo faltante, não só o
domínio."""
rule = _rule(tax_domain="icms", csosn="102", cfop=None)
with pytest.raises(FiscalConfigError) as exc:
resolve_fiscal([rule], _op())
assert "cfop" in " ".join(exc.value.missing)
def test_anchor_matches_without_cst_or_csosn_names_the_field():
"""M5 (review 2026-07-15): a âncora casa (tem cfop) mas não tem cst NEM
csosn -- `FiscalConfigError.missing` deve nomear cst/csosn."""
rule = _rule(tax_domain="icms", cfop="5102", cst=None, csosn=None)
with pytest.raises(FiscalConfigError) as exc:
resolve_fiscal([rule], _op())
assert "cst/csosn" in " ".join(exc.value.missing)
def test_deleted_rule_is_ignored():
from datetime import datetime, timezone
dead = _rule(tax_domain="icms", csosn="102", cfop="5102",
deleted_at=datetime.now(timezone.utc))
with pytest.raises(FiscalConfigError):
resolve_fiscal([dead], _op())
def test_fcp_calculates_from_fcp_percent():
"""I1 (auditoria Fable Bloco B): o domínio `fcp` não tem `aliquota` no
catálogo (só `fcp_percent`, DOMAIN_FIELDS['fcp']) -- `_linha` genérico
só multiplica `aliquota`, então toda linha FCP saía com `valor=0.00`
(preview que mente). Prova de detecção: SEM o fix deste teste, `valor`
vem 0.00 em vez de 2.00 -- FCP 2% sobre base 100."""
rules = [
*_PADRAO,
_rule(tax_domain="fcp", fcp_percent=Decimal("2")),
]
op = _op(item=FiscalItem(
tax_profile_id=_PROFILE, ncm="87089990", cest=None, origem="0",
quantity=Decimal("1"), unit_price=Decimal("100.00"),
))
result = resolve_fiscal(rules, op)
fcp = next(t for t in result.tributos if t.tax_domain == "fcp")
assert fcp.valor == Decimal("2.00")
assert fcp.fcp_percent == Decimal("2")