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
+303
View File
@@ -0,0 +1,303 @@
"""1b.1 Task 5: `xml_builder.build_nfe` -- montagem PURA do XML da NF-e 55
(NT 2025.002 v1.40). Todos os testes montam `DadosEmissao` em memória
(nenhum banco, nenhum I/O) e inspecionam o objeto `Nfe` resultante
diretamente OU sua serialização via `xsdata.formats.dataclass.serializers.
XmlSerializer` (o mesmo serializador que `emissao.py`, Task 6, usará antes
de assinar)."""
import uuid
from decimal import Decimal
import pytest
from xsdata.formats.dataclass.serializers import XmlSerializer
from xsdata.formats.dataclass.serializers.config import SerializerConfig
from sowai_fiscal.resolver import FiscalResult, TributoLinha
from sowai_fiscal.xml_builder import (
TEXTO_HOMOLOGACAO,
DadosEmissao,
DestinatarioData,
EmitenteData,
ItemData,
PagamentoData,
build_nfe,
)
_NFE_NAMESPACE = "http://www.portalfiscal.inf.br/nfe"
def _emitente(**overrides) -> EmitenteData:
base = dict(
cnpj="12345678000190",
razao_social="EMPRESA TESTE LTDA",
nome_fantasia="Empresa Teste",
ie="1234567890",
crt="1",
address_street="Rua Teste",
address_number="100",
address_complement=None,
address_district="Centro",
address_city="Curitiba",
address_state="PR",
address_zip="80000000",
address_city_ibge_code="4106902",
)
base.update(overrides)
return EmitenteData(**base)
def _destinatario(**overrides) -> DestinatarioData:
base = dict(
nome="CLIENTE TESTE LTDA",
cnpj="99887766000155",
cpf=None,
indicador_ie="9",
address_street="Av Cliente",
address_number="50",
address_district="Bairro",
address_city="Curitiba",
address_state="PR",
address_zip="80000000",
address_city_ibge_code="4106902",
)
base.update(overrides)
return DestinatarioData(**base)
def _tributo(tax_domain: str, *, cst=None, csosn=None, aliquota=None, valor="0.00", base_calc="100.00") -> TributoLinha:
return TributoLinha(
tax_domain=tax_domain,
cst=cst,
csosn=csosn,
base_calc=Decimal(base_calc),
base_calc_percent=Decimal("100"),
aliquota=Decimal(aliquota) if aliquota is not None else None,
valor=Decimal(valor),
mva=None,
aliquota_st=None,
fcp_percent=None,
codigo_beneficio=None,
rule_id=uuid.uuid4(),
)
def _fiscal_result(cfop: str, *, csosn="102", cst=None, tributos=None, origem="0") -> FiscalResult:
return FiscalResult(
cfop=cfop,
cst=cst,
csosn=csosn,
origem=origem,
consumidor_final=True,
indicador_ie="9",
tributos=tributos if tributos is not None else [
_tributo("pis", cst="08"),
_tributo("cofins", cst="08"),
],
)
def _item(cfop: str, **overrides) -> ItemData:
base = dict(
codigo="PC001",
descricao="Filtro de oleo",
ncm="84212300",
cfop=cfop,
unidade_comercial="UN",
unidade_tributavel="UN",
quantidade=Decimal("2"),
valor_unitario=Decimal("50.00"),
fiscal_result=_fiscal_result(cfop),
)
base.update(overrides)
return ItemData(**base)
def _pagamento(**overrides) -> PagamentoData:
base = dict(tpag="01", valor=Decimal("100.00"))
base.update(overrides)
return PagamentoData(**base)
def _dados(**overrides) -> DadosEmissao:
base = dict(
emitente=_emitente(),
itens=[_item("5102")],
pagamento=_pagamento(),
ambiente="homologacao",
chave_acesso="4" * 44,
numero=1014,
serie=1,
cnf="87654321",
dh_emi="2026-07-16T10:00:00-03:00",
uf_destino_tipo="interna",
destinatario=_destinatario(),
)
base.update(overrides)
return DadosEmissao(**base)
def _render(nfe) -> str:
config = SerializerConfig(xml_declaration=False, indent=None)
return XmlSerializer(config=config).render(nfe, ns_map={None: _NFE_NAMESPACE})
# --- venda Padrão intra-PR ---------------------------------------------
def test_venda_padrao_intra_estado_cfop_5102_csosn_102_e_origem_do_item():
dados = _dados(uf_destino_tipo="interna", itens=[_item("5102", fiscal_result=_fiscal_result("5102", csosn="102", origem="1"))])
nfe = build_nfe(dados)
det = nfe.infNFe.det[0]
assert det.prod.CFOP == "5102"
assert det.imposto.ICMS.ICMSSN102.CSOSN == "102"
assert det.imposto.ICMS.ICMSSN102.orig == "1" # origem SEMPRE do item, nunca de regra
assert nfe.infNFe.ide.idDest == "1"
def test_totais_batem_com_a_soma_dos_itens_decimal_2_casas():
itens = [
_item("5102", quantidade=Decimal("2"), valor_unitario=Decimal("50.00")),
_item("5102", quantidade=Decimal("3"), valor_unitario=Decimal("10.005")),
]
dados = _dados(itens=itens)
nfe = build_nfe(dados)
# 2*50.00 = 100.00 ; 3*10.005 = 30.015 -> arredonda p/ 30.02 (ROUND_HALF_UP)
assert nfe.infNFe.total.ICMSTot.vProd == "130.02"
assert nfe.infNFe.total.ICMSTot.vNF == "130.02"
# Decimal com 2 casas sempre (nunca "100.0" ou "100")
assert nfe.infNFe.total.ICMSTot.vProd.count(".") == 1
assert len(nfe.infNFe.total.ICMSTot.vProd.split(".")[1]) == 2
# --- interestadual --------------------------------------------------------
def test_venda_interestadual_cfop_6102_iddest_2():
dados = _dados(
uf_destino_tipo="interestadual",
itens=[_item("6102", fiscal_result=_fiscal_result("6102", csosn="102"))],
ind_final="1",
destinatario=_destinatario(indicador_ie="1", address_state="SP"),
)
nfe = build_nfe(dados)
det = nfe.infNFe.det[0]
assert det.prod.CFOP == "6102"
assert nfe.infNFe.ide.idDest == "2"
assert nfe.infNFe.ide.indFinal == "1"
assert nfe.infNFe.dest.indIEDest == "1"
# --- pagamento -------------------------------------------------------------
def test_pag_detpag_tpag_do_pagamento():
dados = _dados(pagamento=_pagamento(tpag="03", valor=Decimal("55.50")))
nfe = build_nfe(dados)
det_pag = nfe.infNFe.pag.detPag[0]
assert det_pag.tPag == "03"
assert det_pag.vPag == "55.50"
def test_pag_tpag_17_para_pix():
dados = _dados(pagamento=_pagamento(tpag="17", valor=Decimal("100.00")))
nfe = build_nfe(dados)
assert nfe.infNFe.pag.detPag[0].tPag == "17"
# --- homologação -------------------------------------------------------------
def test_homologacao_forca_xnome_do_destinatario_para_texto_oficial():
dados = _dados(ambiente="homologacao", destinatario=_destinatario(nome="Cliente Real Ltda"))
nfe = build_nfe(dados)
assert nfe.infNFe.dest.xNome == TEXTO_HOMOLOGACAO
assert nfe.infNFe.ide.tpAmb == "2"
def test_producao_mantem_xnome_real_do_destinatario():
dados = _dados(ambiente="producao", destinatario=_destinatario(nome="Cliente Real Ltda"))
nfe = build_nfe(dados)
assert nfe.infNFe.dest.xNome == "Cliente Real Ltda"
assert nfe.infNFe.ide.tpAmb == "1"
# --- grupo UB (IBS/CBS) -----------------------------------------------------
def test_grupo_ibscbs_ausente_sem_linhas_ibs_cbs_crt1_2026():
dados = _dados(itens=[_item("5102", fiscal_result=_fiscal_result("5102"))])
nfe = build_nfe(dados)
assert nfe.infNFe.det[0].imposto.IBSCBS is None
def test_grupo_ibscbs_presente_com_linhas_ibs_cbs():
fr = _fiscal_result(
"5102",
tributos=[
_tributo("pis", cst="08"),
_tributo("cofins", cst="08"),
_tributo("ibs", aliquota="0.1000", valor="10.00", base_calc="100.00"),
_tributo("cbs", aliquota="0.9000", valor="90.00", base_calc="100.00"),
],
)
dados = _dados(itens=[_item("5102", fiscal_result=fr)])
nfe = build_nfe(dados)
ibscbs = nfe.infNFe.det[0].imposto.IBSCBS
assert ibscbs is not None
assert ibscbs.gIBSCBS.gIBSUF.vIBSUF == "10.00"
assert ibscbs.gIBSCBS.gCBS.vCBS == "90.00"
# --- serialização -----------------------------------------------------------
def test_serializacao_gera_xml_bem_formado_com_namespace_correto():
dados = _dados()
nfe = build_nfe(dados)
xml = _render(nfe)
assert xml.startswith(f'<NFe xmlns="{_NFE_NAMESPACE}">')
assert f'Id="NFe{dados.chave_acesso}"' in xml
assert "<infNFe" in xml and "</infNFe>" in xml
# bem-formado: um parser XML real não estoura
from lxml import etree
etree.fromstring(xml.encode("utf-8"))
def test_unsupported_icms_situation_raises_clear_error():
from sowai_fiscal.xml_builder import UnsupportedIcmsSituationError
dados = _dados(itens=[_item("5102", fiscal_result=_fiscal_result("5102", csosn="201"))])
with pytest.raises(UnsupportedIcmsSituationError):
build_nfe(dados)
def test_origem_none_levanta_valueerror_sem_fallback_silencioso():
"""I3 (review Opus, 2026-07-16): origem None chegando no builder é erro
de PROGRAMAÇÃO (a validação de completude da emissão barra peça sem
`icms_origem` com 409 antes) -- jamais um default '0' silencioso, que
emitiria origem nacional para peça importada (imposto errado)."""
dados = _dados(itens=[_item("5102", fiscal_result=_fiscal_result("5102", origem=None))])
with pytest.raises(ValueError, match="origem"):
build_nfe(dados)