"""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")