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.
218 lines
7.8 KiB
Python
218 lines
7.8 KiB
Python
"""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")
|