Files
sowai-fiscal/tests/test_resolver.py
T
jonatanritter 032151af34 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.
2026-07-22 15:05:10 -03:00

218 lines
7.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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")