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:
@@ -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")
|
||||
Reference in New Issue
Block a user