From 44941b04e340125c1f4fde752683bb4958d04f8a Mon Sep 17 00:00:00 2001 From: jonatanritter Date: Wed, 22 Jul 2026 15:10:06 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20golden=20corpus=20=E2=80=94=20DadosEmis?= =?UTF-8?q?sao=20JSON=20->=20canonical=20pre-signature=20XML?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds golden_helpers (dados_from_json/serialize_infnfe, package modules so the auto's parity test and the future service can import them directly) and 6 golden cases under src/sowai_fiscal/goldens/ as package data: - caso_padrao_intra: preset Padrao, PR->PR, CSOSN 102/CFOP 5102, 2 itens - caso_padrao_inter: PR->SP, CFOP 6102, idDest=2 - caso_st_intra: Oleos, CSOSN 500/CFOP 5405, grupo ICMSSN500 (vBCSTRet/pST/vICMSSTRet) - caso_devolucao_intra: tipo_operacao devolucao, CFOP 1202 - caso_com_ub: FiscalResult com linhas ibs/cbs (grupo UB presente -- builder suporta estruturalmente mesmo com o guard de emissao do auto bloqueando hoje) - caso_fracao_centavo: qty 1.5 x 0.01, 2 linhas -- prova o arredondamento por-item (soma_itens_quantizados) contra a soma bruta Every input pins dh_emi/cnf/chave_acesso for determinism. serialize_infnfe replicates the exact SerializerConfig(xml_declaration=False, indent=None) + ns_map used by the auto's emissao.py::_serialize_nfe before signing. scripts/gen_goldens.py regenerates the .expected.xml files; test_goldens.py compares byte-for-byte (parametrized per case, readable first-divergent-byte diff on failure). Verified the comparison has real detection power by corrupting one expected file and confirming the test fails, then restored. 62 tests pass locally via `uv run pytest`. --- scripts/gen_goldens.py | 29 +++ src/sowai_fiscal/golden_helpers.py | 173 +++++++++++++++++ .../goldens/caso_com_ub.expected.xml | 1 + .../goldens/caso_com_ub.input.json | 128 +++++++++++++ .../goldens/caso_devolucao_intra.expected.xml | 1 + .../goldens/caso_devolucao_intra.input.json | 115 +++++++++++ .../goldens/caso_fracao_centavo.expected.xml | 1 + .../goldens/caso_fracao_centavo.input.json | 180 ++++++++++++++++++ .../goldens/caso_padrao_inter.expected.xml | 1 + .../goldens/caso_padrao_inter.input.json | 115 +++++++++++ .../goldens/caso_padrao_intra.expected.xml | 1 + .../goldens/caso_padrao_intra.input.json | 180 ++++++++++++++++++ .../goldens/caso_st_intra.expected.xml | 1 + .../goldens/caso_st_intra.input.json | 114 +++++++++++ tests/test_goldens.py | 56 ++++++ 15 files changed, 1096 insertions(+) create mode 100644 scripts/gen_goldens.py create mode 100644 src/sowai_fiscal/golden_helpers.py create mode 100644 src/sowai_fiscal/goldens/caso_com_ub.expected.xml create mode 100644 src/sowai_fiscal/goldens/caso_com_ub.input.json create mode 100644 src/sowai_fiscal/goldens/caso_devolucao_intra.expected.xml create mode 100644 src/sowai_fiscal/goldens/caso_devolucao_intra.input.json create mode 100644 src/sowai_fiscal/goldens/caso_fracao_centavo.expected.xml create mode 100644 src/sowai_fiscal/goldens/caso_fracao_centavo.input.json create mode 100644 src/sowai_fiscal/goldens/caso_padrao_inter.expected.xml create mode 100644 src/sowai_fiscal/goldens/caso_padrao_inter.input.json create mode 100644 src/sowai_fiscal/goldens/caso_padrao_intra.expected.xml create mode 100644 src/sowai_fiscal/goldens/caso_padrao_intra.input.json create mode 100644 src/sowai_fiscal/goldens/caso_st_intra.expected.xml create mode 100644 src/sowai_fiscal/goldens/caso_st_intra.input.json create mode 100644 tests/test_goldens.py diff --git a/scripts/gen_goldens.py b/scripts/gen_goldens.py new file mode 100644 index 0000000..eed24fb --- /dev/null +++ b/scripts/gen_goldens.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +"""Regenera `src/sowai_fiscal/goldens/caso_*.expected.xml` a partir dos +`caso_*.input.json` correspondentes. Uso ÚNICO hoje (fixar o corpus golden, +Task 2 do plano F1) e sempre que uma NT mudar o leiaute DE PROPÓSITO -- +nunca para "corrigir" um teste vermelho sem entender por que o XML mudou. + + uv run python scripts/gen_goldens.py +""" +from pathlib import Path + +from sowai_fiscal.golden_helpers import dados_from_json, serialize_infnfe + +_GOLDENS_DIR = Path(__file__).resolve().parent.parent / "src" / "sowai_fiscal" / "goldens" + + +def main() -> None: + inputs = sorted(_GOLDENS_DIR.glob("caso_*.input.json")) + if not inputs: + raise SystemExit(f"nenhum caso_*.input.json encontrado em {_GOLDENS_DIR}") + for input_path in inputs: + expected_path = input_path.with_name(input_path.name.replace(".input.json", ".expected.xml")) + dados = dados_from_json(input_path) + xml_bytes = serialize_infnfe(dados) + expected_path.write_bytes(xml_bytes) + print(f"{input_path.name} -> {expected_path.name} ({len(xml_bytes)} bytes)") + + +if __name__ == "__main__": + main() diff --git a/src/sowai_fiscal/golden_helpers.py b/src/sowai_fiscal/golden_helpers.py new file mode 100644 index 0000000..26c7249 --- /dev/null +++ b/src/sowai_fiscal/golden_helpers.py @@ -0,0 +1,173 @@ +"""Helpers do corpus golden (`sowai_fiscal.goldens/`): carregam um +`DadosEmissao` a partir de um JSON determinístico e serializam o `Nfe` +resultante do `build_nfe` na MESMA configuração canônica que o auto usa em +`fiscal/emissao.py::_serialize_nfe` antes de assinar (`SerializerConfig +(xml_declaration=False, indent=None)`, `ns_map={None: NFE_NAMESPACE}`). + +Módulo do PACOTE (não de `tests/`) de propósito: tanto o teste de goldens +desta lib quanto o teste de paridade do produto consumidor (auto, Task 4 do +plano F1) importam `sowai_fiscal.golden_helpers` para ler exatamente os +mesmos arquivos via `importlib.resources` -- zero cópia que possa divergir. + +O JSON de input espelha 1:1 os dataclasses de `sowai_fiscal.xml_builder` +(`DadosEmissao`, `EmitenteData`, `DestinatarioData`, `ItemData`, +`PagamentoData`) e `sowai_fiscal.resolver` (`FiscalResult`, `TributoLinha`) +-- é o `DadosEmissao` JÁ RESOLVIDO (o motor de regras não roda aqui; cada +item já traz seu `fiscal_result`), exatamente o formato que o orquestrador +de emissão do auto monta antes de chamar `build_nfe`. +""" +import json +from decimal import Decimal +from pathlib import Path +from typing import Any + +from sowai_fiscal.resolver import FiscalResult, TributoLinha +from sowai_fiscal.xml_builder import ( + DadosEmissao, + DestinatarioData, + EmitenteData, + ItemData, + PagamentoData, + build_nfe, +) +from xsdata.formats.dataclass.serializers import XmlSerializer +from xsdata.formats.dataclass.serializers.config import SerializerConfig + +# A MESMA config/namespace que `app/modules/fiscal/emissao.py::_serialize_nfe` +# usa no auto -- conferida lá antes de fixar aqui (ver docstring do módulo). +NFE_NAMESPACE = "http://www.portalfiscal.inf.br/nfe" + + +def _dec(value: Any) -> Decimal | None: + if value is None: + return None + return Decimal(str(value)) + + +def _tributo_from_dict(data: dict) -> TributoLinha: + return TributoLinha( + tax_domain=data["tax_domain"], + cst=data.get("cst"), + csosn=data.get("csosn"), + base_calc=_dec(data["base_calc"]), + base_calc_percent=_dec(data["base_calc_percent"]), + aliquota=_dec(data.get("aliquota")), + valor=_dec(data["valor"]), + mva=_dec(data.get("mva")), + aliquota_st=_dec(data.get("aliquota_st")), + fcp_percent=_dec(data.get("fcp_percent")), + codigo_beneficio=data.get("codigo_beneficio"), + rule_id=data["rule_id"], + ) + + +def _fiscal_result_from_dict(data: dict) -> FiscalResult: + return FiscalResult( + cfop=data["cfop"], + cst=data.get("cst"), + csosn=data.get("csosn"), + origem=data.get("origem"), + consumidor_final=data["consumidor_final"], + indicador_ie=data["indicador_ie"], + tributos=[_tributo_from_dict(t) for t in data["tributos"]], + ) + + +def _item_from_dict(data: dict) -> ItemData: + return ItemData( + codigo=data["codigo"], + descricao=data["descricao"], + ncm=data["ncm"], + cfop=data["cfop"], + unidade_comercial=data["unidade_comercial"], + unidade_tributavel=data["unidade_tributavel"], + quantidade=_dec(data["quantidade"]), + valor_unitario=_dec(data["valor_unitario"]), + fiscal_result=_fiscal_result_from_dict(data["fiscal_result"]), + gtin=data.get("gtin"), + cest=data.get("cest"), + peso_liquido_kg=_dec(data.get("peso_liquido_kg")), + peso_bruto_kg=_dec(data.get("peso_bruto_kg")), + ) + + +def _emitente_from_dict(data: dict) -> EmitenteData: + return EmitenteData( + cnpj=data["cnpj"], + razao_social=data["razao_social"], + nome_fantasia=data.get("nome_fantasia"), + ie=data["ie"], + crt=data["crt"], + address_street=data["address_street"], + address_number=data["address_number"], + address_complement=data.get("address_complement"), + address_district=data["address_district"], + address_city=data["address_city"], + address_state=data["address_state"], + address_zip=data["address_zip"], + address_city_ibge_code=data["address_city_ibge_code"], + fone=data.get("fone"), + ) + + +def _destinatario_from_dict(data: dict | None) -> DestinatarioData | None: + if data is None: + return None + return DestinatarioData( + nome=data["nome"], + cnpj=data.get("cnpj"), + cpf=data.get("cpf"), + indicador_ie=data["indicador_ie"], + ie=data.get("ie"), + address_street=data.get("address_street"), + address_number=data.get("address_number"), + address_complement=data.get("address_complement"), + address_district=data.get("address_district"), + address_city=data.get("address_city"), + address_state=data.get("address_state"), + address_zip=data.get("address_zip"), + address_city_ibge_code=data.get("address_city_ibge_code"), + email=data.get("email"), + ) + + +def _pagamento_from_dict(data: dict) -> PagamentoData: + return PagamentoData( + tpag=data["tpag"], + valor=_dec(data["valor"]), + indpag=data.get("indpag", "0"), + ) + + +def dados_from_json(path: Path) -> DadosEmissao: + """Carrega um `DadosEmissao` completo (já resolvido) a partir do JSON + golden em `path`. Campos opcionais do dataclass que o JSON omite caem + no default do próprio `DadosEmissao`.""" + raw = json.loads(Path(path).read_text(encoding="utf-8")) + kwargs: dict[str, Any] = dict( + emitente=_emitente_from_dict(raw["emitente"]), + itens=[_item_from_dict(i) for i in raw["itens"]], + pagamento=_pagamento_from_dict(raw["pagamento"]), + ambiente=raw["ambiente"], + chave_acesso=raw["chave_acesso"], + numero=raw["numero"], + serie=raw["serie"], + cnf=raw["cnf"], + dh_emi=raw["dh_emi"], + uf_destino_tipo=raw["uf_destino_tipo"], + destinatario=_destinatario_from_dict(raw.get("destinatario")), + ) + for optional in ("nat_op", "tp_emis", "ind_final", "ind_pres", "fin_nfe", "ver_proc"): + if optional in raw: + kwargs[optional] = raw[optional] + return DadosEmissao(**kwargs) + + +def serialize_infnfe(dados: DadosEmissao) -> bytes: + """`DadosEmissao` -> XML pré-assinatura, bytes UTF-8. Config e namespace + IDÊNTICOS a `emissao.py::_serialize_nfe` no auto -- é o que torna este + XML comparável byte a byte com o gerado pelo produto consumidor.""" + nfe = build_nfe(dados) + config = SerializerConfig(xml_declaration=False, indent=None) + xml_str = XmlSerializer(config=config).render(nfe, ns_map={None: NFE_NAMESPACE}) + return xml_str.encode("utf-8") diff --git a/src/sowai_fiscal/goldens/caso_com_ub.expected.xml b/src/sowai_fiscal/goldens/caso_com_ub.expected.xml new file mode 100644 index 0000000..a4b1952 --- /dev/null +++ b/src/sowai_fiscal/goldens/caso_com_ub.expected.xml @@ -0,0 +1 @@ +4110000005Venda55110052026-07-16T10:20:00-03:0011410690211021110sowai-auto/1b.112345678000190AUTOPECAS THIAGO LTDAThiago Auto CenterRua das Autopecas1500Centro4106902CuritibaPR800000001058Brasil41333344441234567890199887766000155NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCALAv Cliente50Bairro4106902CuritibaPR800001001058Brasil9PC001SEM GTINFiltro de oleo842123005102UN1100.00100.00SEM GTINUN1100.00101020808000000001100.000.100010.000.900090.000.000.000.000.000.000.000.000.00100.000.000.000.000.000.000.000.000.000.00100.009001100.00 \ No newline at end of file diff --git a/src/sowai_fiscal/goldens/caso_com_ub.input.json b/src/sowai_fiscal/goldens/caso_com_ub.input.json new file mode 100644 index 0000000..3aa1aa4 --- /dev/null +++ b/src/sowai_fiscal/goldens/caso_com_ub.input.json @@ -0,0 +1,128 @@ +{ + "emitente": { + "cnpj": "12345678000190", + "razao_social": "AUTOPECAS THIAGO LTDA", + "nome_fantasia": "Thiago Auto Center", + "ie": "1234567890", + "crt": "1", + "address_street": "Rua das Autopecas", + "address_number": "1500", + "address_complement": null, + "address_district": "Centro", + "address_city": "Curitiba", + "address_state": "PR", + "address_zip": "80000000", + "address_city_ibge_code": "4106902", + "fone": "4133334444" + }, + "destinatario": { + "nome": "CLIENTE BALCAO LTDA", + "cnpj": "99887766000155", + "cpf": null, + "indicador_ie": "9", + "ie": null, + "address_street": "Av Cliente", + "address_number": "50", + "address_complement": null, + "address_district": "Bairro", + "address_city": "Curitiba", + "address_state": "PR", + "address_zip": "80000100", + "address_city_ibge_code": "4106902", + "email": null + }, + "itens": [ + { + "codigo": "PC001", + "descricao": "Filtro de oleo", + "ncm": "84212300", + "cfop": "5102", + "unidade_comercial": "UN", + "unidade_tributavel": "UN", + "quantidade": "1", + "valor_unitario": "100.00", + "gtin": null, + "cest": null, + "peso_liquido_kg": null, + "peso_bruto_kg": null, + "fiscal_result": { + "cfop": "5102", + "cst": null, + "csosn": "102", + "origem": "0", + "consumidor_final": true, + "indicador_ie": "9", + "tributos": [ + { + "tax_domain": "pis", + "cst": "08", + "csosn": null, + "base_calc": "100.00", + "base_calc_percent": "100", + "aliquota": "0", + "valor": "0.00", + "mva": null, + "aliquota_st": null, + "fcp_percent": null, + "codigo_beneficio": null, + "rule_id": "7867351e-f79e-59c5-9e83-882563853fdd" + }, + { + "tax_domain": "cofins", + "cst": "08", + "csosn": null, + "base_calc": "100.00", + "base_calc_percent": "100", + "aliquota": "0", + "valor": "0.00", + "mva": null, + "aliquota_st": null, + "fcp_percent": null, + "codigo_beneficio": null, + "rule_id": "050677c1-38dc-5246-a41c-d1b58017b0d0" + }, + { + "tax_domain": "ibs", + "cst": null, + "csosn": null, + "base_calc": "100.00", + "base_calc_percent": "100", + "aliquota": "0.1000", + "valor": "10.00", + "mva": null, + "aliquota_st": null, + "fcp_percent": null, + "codigo_beneficio": null, + "rule_id": "f926eaa3-87a6-5acc-91dd-cbb9d52567f8" + }, + { + "tax_domain": "cbs", + "cst": null, + "csosn": null, + "base_calc": "100.00", + "base_calc_percent": "100", + "aliquota": "0.9000", + "valor": "90.00", + "mva": null, + "aliquota_st": null, + "fcp_percent": null, + "codigo_beneficio": null, + "rule_id": "20f2bed4-07b3-5c41-b2ca-b659e0ae76a8" + } + ] + } + } + ], + "pagamento": { + "tpag": "01", + "valor": "100.00", + "indpag": "0" + }, + "ambiente": "homologacao", + "chave_acesso": "41260712345678000190550010000010051100000050", + "numero": 1005, + "serie": 1, + "cnf": "10000005", + "dh_emi": "2026-07-16T10:20:00-03:00", + "uf_destino_tipo": "interna" +} diff --git a/src/sowai_fiscal/goldens/caso_devolucao_intra.expected.xml b/src/sowai_fiscal/goldens/caso_devolucao_intra.expected.xml new file mode 100644 index 0000000..4b1e5fd --- /dev/null +++ b/src/sowai_fiscal/goldens/caso_devolucao_intra.expected.xml @@ -0,0 +1 @@ +4110000004Devolucao de venda55110042026-07-16T10:15:00-03:0011410690211521110sowai-auto/1b.112345678000190AUTOPECAS THIAGO LTDAThiago Auto CenterRua das Autopecas1500Centro4106902CuritibaPR800000001058Brasil41333344441234567890199887766000155NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCALAv Cliente50Bairro4106902CuritibaPR800001001058Brasil9PC001SEM GTINFiltro de oleo842123001202UN175.0075.00SEM GTINUN175.0010102080875.000.000.000.000.000.000.000.0075.000.000.000.000.000.000.000.000.000.0075.00909075.00 \ No newline at end of file diff --git a/src/sowai_fiscal/goldens/caso_devolucao_intra.input.json b/src/sowai_fiscal/goldens/caso_devolucao_intra.input.json new file mode 100644 index 0000000..d1e65fe --- /dev/null +++ b/src/sowai_fiscal/goldens/caso_devolucao_intra.input.json @@ -0,0 +1,115 @@ +{ + "emitente": { + "cnpj": "12345678000190", + "razao_social": "AUTOPECAS THIAGO LTDA", + "nome_fantasia": "Thiago Auto Center", + "ie": "1234567890", + "crt": "1", + "address_street": "Rua das Autopecas", + "address_number": "1500", + "address_complement": null, + "address_district": "Centro", + "address_city": "Curitiba", + "address_state": "PR", + "address_zip": "80000000", + "address_city_ibge_code": "4106902", + "fone": "4133334444" + }, + "destinatario": { + "nome": "CLIENTE BALCAO LTDA", + "cnpj": "99887766000155", + "cpf": null, + "indicador_ie": "9", + "ie": null, + "address_street": "Av Cliente", + "address_number": "50", + "address_complement": null, + "address_district": "Bairro", + "address_city": "Curitiba", + "address_state": "PR", + "address_zip": "80000100", + "address_city_ibge_code": "4106902", + "email": null + }, + "itens": [ + { + "codigo": "PC001", + "descricao": "Filtro de oleo", + "ncm": "84212300", + "cfop": "1202", + "unidade_comercial": "UN", + "unidade_tributavel": "UN", + "quantidade": "1", + "valor_unitario": "75.00", + "gtin": null, + "cest": null, + "peso_liquido_kg": null, + "peso_bruto_kg": null, + "fiscal_result": { + "cfop": "1202", + "cst": null, + "csosn": "102", + "origem": "0", + "consumidor_final": true, + "indicador_ie": "9", + "tributos": [ + { + "tax_domain": "icms", + "cst": null, + "csosn": "102", + "base_calc": "75.00", + "base_calc_percent": "100", + "aliquota": "0", + "valor": "0.00", + "mva": null, + "aliquota_st": null, + "fcp_percent": null, + "codigo_beneficio": null, + "rule_id": "38192a7c-594c-52c0-a911-29ab0385d848" + }, + { + "tax_domain": "pis", + "cst": "08", + "csosn": null, + "base_calc": "75.00", + "base_calc_percent": "100", + "aliquota": "0", + "valor": "0.00", + "mva": null, + "aliquota_st": null, + "fcp_percent": null, + "codigo_beneficio": null, + "rule_id": "48265596-d65d-5ce0-a201-06bfe961a4ef" + }, + { + "tax_domain": "cofins", + "cst": "08", + "csosn": null, + "base_calc": "75.00", + "base_calc_percent": "100", + "aliquota": "0", + "valor": "0.00", + "mva": null, + "aliquota_st": null, + "fcp_percent": null, + "codigo_beneficio": null, + "rule_id": "94781077-adee-5dbd-aef4-b4aca3803a8a" + } + ] + } + } + ], + "pagamento": { + "tpag": "90", + "valor": "75.00", + "indpag": "0" + }, + "ambiente": "homologacao", + "chave_acesso": "41260712345678000190550010000010041100000045", + "numero": 1004, + "serie": 1, + "cnf": "10000004", + "dh_emi": "2026-07-16T10:15:00-03:00", + "uf_destino_tipo": "interna", + "nat_op": "Devolucao de venda" +} diff --git a/src/sowai_fiscal/goldens/caso_fracao_centavo.expected.xml b/src/sowai_fiscal/goldens/caso_fracao_centavo.expected.xml new file mode 100644 index 0000000..39f3b99 --- /dev/null +++ b/src/sowai_fiscal/goldens/caso_fracao_centavo.expected.xml @@ -0,0 +1 @@ +4110000006Venda55110062026-07-16T10:25:00-03:0011410690211621110sowai-auto/1b.112345678000190AUTOPECAS THIAGO LTDAThiago Auto CenterRua das Autopecas1500Centro4106902CuritibaPR800000001058Brasil41333344441234567890199887766000155NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCALAv Cliente50Bairro4106902CuritibaPR800001001058Brasil9PC900SEM GTINArruela avulsa731819005102UN1.50.010.02SEM GTINUN1.50.01101020808PC901SEM GTINArruela avulsa 2731819005102UN1.50.010.02SEM GTINUN1.50.011010208080.040.000.000.000.000.000.000.000.040.000.000.000.000.000.000.000.000.000.0490010.04 \ No newline at end of file diff --git a/src/sowai_fiscal/goldens/caso_fracao_centavo.input.json b/src/sowai_fiscal/goldens/caso_fracao_centavo.input.json new file mode 100644 index 0000000..709b80a --- /dev/null +++ b/src/sowai_fiscal/goldens/caso_fracao_centavo.input.json @@ -0,0 +1,180 @@ +{ + "emitente": { + "cnpj": "12345678000190", + "razao_social": "AUTOPECAS THIAGO LTDA", + "nome_fantasia": "Thiago Auto Center", + "ie": "1234567890", + "crt": "1", + "address_street": "Rua das Autopecas", + "address_number": "1500", + "address_complement": null, + "address_district": "Centro", + "address_city": "Curitiba", + "address_state": "PR", + "address_zip": "80000000", + "address_city_ibge_code": "4106902", + "fone": "4133334444" + }, + "destinatario": { + "nome": "CLIENTE BALCAO LTDA", + "cnpj": "99887766000155", + "cpf": null, + "indicador_ie": "9", + "ie": null, + "address_street": "Av Cliente", + "address_number": "50", + "address_complement": null, + "address_district": "Bairro", + "address_city": "Curitiba", + "address_state": "PR", + "address_zip": "80000100", + "address_city_ibge_code": "4106902", + "email": null + }, + "itens": [ + { + "codigo": "PC900", + "descricao": "Arruela avulsa", + "ncm": "73181900", + "cfop": "5102", + "unidade_comercial": "UN", + "unidade_tributavel": "UN", + "quantidade": "1.5", + "valor_unitario": "0.01", + "gtin": null, + "cest": null, + "peso_liquido_kg": null, + "peso_bruto_kg": null, + "fiscal_result": { + "cfop": "5102", + "cst": null, + "csosn": "102", + "origem": "0", + "consumidor_final": true, + "indicador_ie": "9", + "tributos": [ + { + "tax_domain": "icms", + "cst": null, + "csosn": "102", + "base_calc": "0.02", + "base_calc_percent": "100", + "aliquota": "0", + "valor": "0.00", + "mva": null, + "aliquota_st": null, + "fcp_percent": null, + "codigo_beneficio": null, + "rule_id": "0662ed57-174b-599c-9568-02b524b66edf" + }, + { + "tax_domain": "pis", + "cst": "08", + "csosn": null, + "base_calc": "0.02", + "base_calc_percent": "100", + "aliquota": "0", + "valor": "0.00", + "mva": null, + "aliquota_st": null, + "fcp_percent": null, + "codigo_beneficio": null, + "rule_id": "25880960-e53b-584a-b446-d0fe3fb79761" + }, + { + "tax_domain": "cofins", + "cst": "08", + "csosn": null, + "base_calc": "0.02", + "base_calc_percent": "100", + "aliquota": "0", + "valor": "0.00", + "mva": null, + "aliquota_st": null, + "fcp_percent": null, + "codigo_beneficio": null, + "rule_id": "431b87af-2fa6-5413-a8b5-056a92035429" + } + ] + } + }, + { + "codigo": "PC901", + "descricao": "Arruela avulsa 2", + "ncm": "73181900", + "cfop": "5102", + "unidade_comercial": "UN", + "unidade_tributavel": "UN", + "quantidade": "1.5", + "valor_unitario": "0.01", + "gtin": null, + "cest": null, + "peso_liquido_kg": null, + "peso_bruto_kg": null, + "fiscal_result": { + "cfop": "5102", + "cst": null, + "csosn": "102", + "origem": "0", + "consumidor_final": true, + "indicador_ie": "9", + "tributos": [ + { + "tax_domain": "icms", + "cst": null, + "csosn": "102", + "base_calc": "0.02", + "base_calc_percent": "100", + "aliquota": "0", + "valor": "0.00", + "mva": null, + "aliquota_st": null, + "fcp_percent": null, + "codigo_beneficio": null, + "rule_id": "863b3e8d-ab7f-5cab-b699-c443668a2d41" + }, + { + "tax_domain": "pis", + "cst": "08", + "csosn": null, + "base_calc": "0.02", + "base_calc_percent": "100", + "aliquota": "0", + "valor": "0.00", + "mva": null, + "aliquota_st": null, + "fcp_percent": null, + "codigo_beneficio": null, + "rule_id": "5e787f8f-1968-569b-8e2d-c7126a8d1e73" + }, + { + "tax_domain": "cofins", + "cst": "08", + "csosn": null, + "base_calc": "0.02", + "base_calc_percent": "100", + "aliquota": "0", + "valor": "0.00", + "mva": null, + "aliquota_st": null, + "fcp_percent": null, + "codigo_beneficio": null, + "rule_id": "2c094bb0-fc8c-598a-9af8-3202fb24bba1" + } + ] + } + } + ], + "pagamento": { + "tpag": "01", + "valor": "0.04", + "indpag": "0" + }, + "ambiente": "homologacao", + "chave_acesso": "41260712345678000190550010000010061100000066", + "numero": 1006, + "serie": 1, + "cnf": "10000006", + "dh_emi": "2026-07-16T10:25:00-03:00", + "uf_destino_tipo": "interna" +} diff --git a/src/sowai_fiscal/goldens/caso_padrao_inter.expected.xml b/src/sowai_fiscal/goldens/caso_padrao_inter.expected.xml new file mode 100644 index 0000000..960174b --- /dev/null +++ b/src/sowai_fiscal/goldens/caso_padrao_inter.expected.xml @@ -0,0 +1 @@ +4110000002Venda55110022026-07-16T10:05:00-03:0012410690211421110sowai-auto/1b.112345678000190AUTOPECAS THIAGO LTDAThiago Auto CenterRua das Autopecas1500Centro4106902CuritibaPR800000001058Brasil41333344441234567890111222333000144NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCALAv Paulista1000Sala 10Bela Vista3550308Sao PauloSP013100001058Brasil11122334455compras@clienteinter.com.brPC001SEM GTINFiltro de oleo842123006102UN1075.00750.00SEM GTINUN1075.00101020808750.000.000.000.000.000.000.000.00750.000.000.000.000.000.000.000.000.000.00750.009017750.00 \ No newline at end of file diff --git a/src/sowai_fiscal/goldens/caso_padrao_inter.input.json b/src/sowai_fiscal/goldens/caso_padrao_inter.input.json new file mode 100644 index 0000000..69af378 --- /dev/null +++ b/src/sowai_fiscal/goldens/caso_padrao_inter.input.json @@ -0,0 +1,115 @@ +{ + "emitente": { + "cnpj": "12345678000190", + "razao_social": "AUTOPECAS THIAGO LTDA", + "nome_fantasia": "Thiago Auto Center", + "ie": "1234567890", + "crt": "1", + "address_street": "Rua das Autopecas", + "address_number": "1500", + "address_complement": null, + "address_district": "Centro", + "address_city": "Curitiba", + "address_state": "PR", + "address_zip": "80000000", + "address_city_ibge_code": "4106902", + "fone": "4133334444" + }, + "destinatario": { + "nome": "CLIENTE INTERESTADUAL LTDA", + "cnpj": "11222333000144", + "cpf": null, + "indicador_ie": "1", + "ie": "1122334455", + "address_street": "Av Paulista", + "address_number": "1000", + "address_complement": "Sala 10", + "address_district": "Bela Vista", + "address_city": "Sao Paulo", + "address_state": "SP", + "address_zip": "01310000", + "address_city_ibge_code": "3550308", + "email": "compras@clienteinter.com.br" + }, + "itens": [ + { + "codigo": "PC001", + "descricao": "Filtro de oleo", + "ncm": "84212300", + "cfop": "6102", + "unidade_comercial": "UN", + "unidade_tributavel": "UN", + "quantidade": "10", + "valor_unitario": "75.00", + "gtin": null, + "cest": null, + "peso_liquido_kg": null, + "peso_bruto_kg": null, + "fiscal_result": { + "cfop": "6102", + "cst": null, + "csosn": "102", + "origem": "0", + "consumidor_final": true, + "indicador_ie": "1", + "tributos": [ + { + "tax_domain": "icms", + "cst": null, + "csosn": "102", + "base_calc": "750.00", + "base_calc_percent": "100", + "aliquota": "0", + "valor": "0.00", + "mva": null, + "aliquota_st": null, + "fcp_percent": null, + "codigo_beneficio": null, + "rule_id": "033f9888-3e26-5b16-bf6f-3d8573d02fb1" + }, + { + "tax_domain": "pis", + "cst": "08", + "csosn": null, + "base_calc": "750.00", + "base_calc_percent": "100", + "aliquota": "0", + "valor": "0.00", + "mva": null, + "aliquota_st": null, + "fcp_percent": null, + "codigo_beneficio": null, + "rule_id": "2028ef78-a3cd-59ed-b2b9-09f04a3e433f" + }, + { + "tax_domain": "cofins", + "cst": "08", + "csosn": null, + "base_calc": "750.00", + "base_calc_percent": "100", + "aliquota": "0", + "valor": "0.00", + "mva": null, + "aliquota_st": null, + "fcp_percent": null, + "codigo_beneficio": null, + "rule_id": "a0c97f3b-7938-5168-94c1-f43011671136" + } + ] + } + } + ], + "pagamento": { + "tpag": "17", + "valor": "750.00", + "indpag": "0" + }, + "ambiente": "homologacao", + "chave_acesso": "41260712345678000190550010000010021100000024", + "numero": 1002, + "serie": 1, + "cnf": "10000002", + "dh_emi": "2026-07-16T10:05:00-03:00", + "uf_destino_tipo": "interestadual", + "ind_final": "1" +} diff --git a/src/sowai_fiscal/goldens/caso_padrao_intra.expected.xml b/src/sowai_fiscal/goldens/caso_padrao_intra.expected.xml new file mode 100644 index 0000000..31e77ea --- /dev/null +++ b/src/sowai_fiscal/goldens/caso_padrao_intra.expected.xml @@ -0,0 +1 @@ +4110000001Venda55110012026-07-16T10:00:00-03:0011410690211921110sowai-auto/1b.112345678000190AUTOPECAS THIAGO LTDAThiago Auto CenterRua das Autopecas1500Centro4106902CuritibaPR800000001058Brasil41333344441234567890199887766000155NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCALAv Cliente50Bairro4106902CuritibaPR800001001058Brasil9PC001SEM GTINFiltro de oleo842123005102UN275.00150.00SEM GTINUN275.00101020808PC002SEM GTINPastilha de freio870830905102PC445.50182.00SEM GTINPC445.50101020808332.000.000.000.000.000.000.000.00332.000.000.000.000.000.000.000.000.000.00332.009001332.00 \ No newline at end of file diff --git a/src/sowai_fiscal/goldens/caso_padrao_intra.input.json b/src/sowai_fiscal/goldens/caso_padrao_intra.input.json new file mode 100644 index 0000000..5a724db --- /dev/null +++ b/src/sowai_fiscal/goldens/caso_padrao_intra.input.json @@ -0,0 +1,180 @@ +{ + "emitente": { + "cnpj": "12345678000190", + "razao_social": "AUTOPECAS THIAGO LTDA", + "nome_fantasia": "Thiago Auto Center", + "ie": "1234567890", + "crt": "1", + "address_street": "Rua das Autopecas", + "address_number": "1500", + "address_complement": null, + "address_district": "Centro", + "address_city": "Curitiba", + "address_state": "PR", + "address_zip": "80000000", + "address_city_ibge_code": "4106902", + "fone": "4133334444" + }, + "destinatario": { + "nome": "CLIENTE BALCAO LTDA", + "cnpj": "99887766000155", + "cpf": null, + "indicador_ie": "9", + "ie": null, + "address_street": "Av Cliente", + "address_number": "50", + "address_complement": null, + "address_district": "Bairro", + "address_city": "Curitiba", + "address_state": "PR", + "address_zip": "80000100", + "address_city_ibge_code": "4106902", + "email": null + }, + "itens": [ + { + "codigo": "PC001", + "descricao": "Filtro de oleo", + "ncm": "84212300", + "cfop": "5102", + "unidade_comercial": "UN", + "unidade_tributavel": "UN", + "quantidade": "2", + "valor_unitario": "75.00", + "gtin": null, + "cest": null, + "peso_liquido_kg": null, + "peso_bruto_kg": null, + "fiscal_result": { + "cfop": "5102", + "cst": null, + "csosn": "102", + "origem": "0", + "consumidor_final": true, + "indicador_ie": "9", + "tributos": [ + { + "tax_domain": "icms", + "cst": null, + "csosn": "102", + "base_calc": "150.00", + "base_calc_percent": "100", + "aliquota": "0", + "valor": "0.00", + "mva": null, + "aliquota_st": null, + "fcp_percent": null, + "codigo_beneficio": null, + "rule_id": "388a102f-f935-5f9f-ba70-42db4062f171" + }, + { + "tax_domain": "pis", + "cst": "08", + "csosn": null, + "base_calc": "150.00", + "base_calc_percent": "100", + "aliquota": "0", + "valor": "0.00", + "mva": null, + "aliquota_st": null, + "fcp_percent": null, + "codigo_beneficio": null, + "rule_id": "a4471177-52bf-5b5f-a69d-53b8af65bcef" + }, + { + "tax_domain": "cofins", + "cst": "08", + "csosn": null, + "base_calc": "150.00", + "base_calc_percent": "100", + "aliquota": "0", + "valor": "0.00", + "mva": null, + "aliquota_st": null, + "fcp_percent": null, + "codigo_beneficio": null, + "rule_id": "07604b6a-287d-5314-9fbe-c68495166181" + } + ] + } + }, + { + "codigo": "PC002", + "descricao": "Pastilha de freio", + "ncm": "87083090", + "cfop": "5102", + "unidade_comercial": "PC", + "unidade_tributavel": "PC", + "quantidade": "4", + "valor_unitario": "45.50", + "gtin": null, + "cest": null, + "peso_liquido_kg": null, + "peso_bruto_kg": null, + "fiscal_result": { + "cfop": "5102", + "cst": null, + "csosn": "102", + "origem": "0", + "consumidor_final": true, + "indicador_ie": "9", + "tributos": [ + { + "tax_domain": "icms", + "cst": null, + "csosn": "102", + "base_calc": "182.00", + "base_calc_percent": "100", + "aliquota": "0", + "valor": "0.00", + "mva": null, + "aliquota_st": null, + "fcp_percent": null, + "codigo_beneficio": null, + "rule_id": "51ff4cd8-651d-5b26-a068-3996791a3af9" + }, + { + "tax_domain": "pis", + "cst": "08", + "csosn": null, + "base_calc": "182.00", + "base_calc_percent": "100", + "aliquota": "0", + "valor": "0.00", + "mva": null, + "aliquota_st": null, + "fcp_percent": null, + "codigo_beneficio": null, + "rule_id": "893ccc32-9cb6-5dbc-b5e3-427461de4cb7" + }, + { + "tax_domain": "cofins", + "cst": "08", + "csosn": null, + "base_calc": "182.00", + "base_calc_percent": "100", + "aliquota": "0", + "valor": "0.00", + "mva": null, + "aliquota_st": null, + "fcp_percent": null, + "codigo_beneficio": null, + "rule_id": "9c2c1b35-3f67-56bb-a109-e78ab1a6f7f1" + } + ] + } + } + ], + "pagamento": { + "tpag": "01", + "valor": "332.00", + "indpag": "0" + }, + "ambiente": "homologacao", + "chave_acesso": "41260712345678000190550010000010011100000019", + "numero": 1001, + "serie": 1, + "cnf": "10000001", + "dh_emi": "2026-07-16T10:00:00-03:00", + "uf_destino_tipo": "interna" +} diff --git a/src/sowai_fiscal/goldens/caso_st_intra.expected.xml b/src/sowai_fiscal/goldens/caso_st_intra.expected.xml new file mode 100644 index 0000000..e190465 --- /dev/null +++ b/src/sowai_fiscal/goldens/caso_st_intra.expected.xml @@ -0,0 +1 @@ +4110000003Venda55110032026-07-16T10:10:00-03:0011410690211021110sowai-auto/1b.112345678000190AUTOPECAS THIAGO LTDAThiago Auto CenterRua das Autopecas1500Centro4106902CuritibaPR800000001058Brasil41333344441234567890112345678909NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCALRua Oficina22Industrial4106902CuritibaPR800002001058Brasil9OL500SEM GTINOleo lubrificante 15W40 1L2710125906001005405UN1100.00100.00SEM GTINUN1100.0010500140.0018.000013.2004040.000.000.000.00140.0013.200.000.00100.000.000.000.000.000.000.000.000.000.00100.009001100.00 \ No newline at end of file diff --git a/src/sowai_fiscal/goldens/caso_st_intra.input.json b/src/sowai_fiscal/goldens/caso_st_intra.input.json new file mode 100644 index 0000000..badeddb --- /dev/null +++ b/src/sowai_fiscal/goldens/caso_st_intra.input.json @@ -0,0 +1,114 @@ +{ + "emitente": { + "cnpj": "12345678000190", + "razao_social": "AUTOPECAS THIAGO LTDA", + "nome_fantasia": "Thiago Auto Center", + "ie": "1234567890", + "crt": "1", + "address_street": "Rua das Autopecas", + "address_number": "1500", + "address_complement": null, + "address_district": "Centro", + "address_city": "Curitiba", + "address_state": "PR", + "address_zip": "80000000", + "address_city_ibge_code": "4106902", + "fone": "4133334444" + }, + "destinatario": { + "nome": "CLIENTE BALCAO LTDA", + "cnpj": null, + "cpf": "12345678909", + "indicador_ie": "9", + "ie": null, + "address_street": "Rua Oficina", + "address_number": "22", + "address_complement": null, + "address_district": "Industrial", + "address_city": "Curitiba", + "address_state": "PR", + "address_zip": "80000200", + "address_city_ibge_code": "4106902", + "email": null + }, + "itens": [ + { + "codigo": "OL500", + "descricao": "Oleo lubrificante 15W40 1L", + "ncm": "27101259", + "cfop": "5405", + "unidade_comercial": "UN", + "unidade_tributavel": "UN", + "quantidade": "1", + "valor_unitario": "100.00", + "gtin": null, + "cest": "0600100", + "peso_liquido_kg": null, + "peso_bruto_kg": null, + "fiscal_result": { + "cfop": "5405", + "cst": null, + "csosn": "500", + "origem": "0", + "consumidor_final": true, + "indicador_ie": "9", + "tributos": [ + { + "tax_domain": "icmsst", + "cst": null, + "csosn": "500", + "base_calc": "140.00", + "base_calc_percent": "100", + "aliquota": "18", + "valor": "13.20", + "mva": "40", + "aliquota_st": "18", + "fcp_percent": null, + "codigo_beneficio": null, + "rule_id": "dd66fbca-c7d0-5d3e-a231-1a2afd3c1c74" + }, + { + "tax_domain": "pis", + "cst": "04", + "csosn": null, + "base_calc": "100.00", + "base_calc_percent": "100", + "aliquota": "0", + "valor": "0.00", + "mva": null, + "aliquota_st": null, + "fcp_percent": null, + "codigo_beneficio": null, + "rule_id": "ac7c5793-b5a8-58b5-b060-af95db499e78" + }, + { + "tax_domain": "cofins", + "cst": "04", + "csosn": null, + "base_calc": "100.00", + "base_calc_percent": "100", + "aliquota": "0", + "valor": "0.00", + "mva": null, + "aliquota_st": null, + "fcp_percent": null, + "codigo_beneficio": null, + "rule_id": "5b0b52d8-50e1-57b9-a473-0911572b040b" + } + ] + } + } + ], + "pagamento": { + "tpag": "01", + "valor": "100.00", + "indpag": "0" + }, + "ambiente": "homologacao", + "chave_acesso": "41260712345678000190550010000010031100000030", + "numero": 1003, + "serie": 1, + "cnf": "10000003", + "dh_emi": "2026-07-16T10:10:00-03:00", + "uf_destino_tipo": "interna" +} diff --git a/tests/test_goldens.py b/tests/test_goldens.py new file mode 100644 index 0000000..31d9d0f --- /dev/null +++ b/tests/test_goldens.py @@ -0,0 +1,56 @@ +"""Corpus golden: para cada `caso_*.input.json` em `sowai_fiscal.goldens`, +carrega o `DadosEmissao`, monta+serializa o XML pré-assinatura e compara +BYTE A BYTE com o `caso_*.expected.xml` irmão. Estes dados são PACOTE +(`importlib.resources`), não fixtures de `tests/` -- o produto consumidor +(auto) e o futuro serviço leem os MESMOS arquivos da lib instalada. + +Regenerar os `.expected.xml` (nunca à mão) com `scripts/gen_goldens.py` -- +só quando uma NT mudar o leiaute DE PROPÓSITO.""" +import importlib.resources + +import pytest + +from sowai_fiscal.golden_helpers import dados_from_json, serialize_infnfe + +_GOLDENS = importlib.resources.files("sowai_fiscal") / "goldens" + + +def _casos() -> list[str]: + with importlib.resources.as_file(_GOLDENS) as goldens_dir: + return sorted( + p.name.removesuffix(".input.json") + for p in goldens_dir.glob("caso_*.input.json") + ) + + +_CASOS = _casos() + + +def test_at_least_six_golden_cases_exist(): + assert len(_CASOS) >= 6, f"esperado >=6 casos golden, achei {len(_CASOS)}: {_CASOS}" + + +@pytest.mark.parametrize("caso", _CASOS) +def test_golden_case_matches_expected_xml_byte_a_byte(caso): + with importlib.resources.as_file(_GOLDENS) as goldens_dir: + input_path = goldens_dir / f"{caso}.input.json" + expected_path = goldens_dir / f"{caso}.expected.xml" + + dados = dados_from_json(input_path) + actual = serialize_infnfe(dados) + expected = expected_path.read_bytes() + + if actual != expected: + # Diff legível: aponta o primeiro byte divergente em vez de despejar + # os dois XMLs inteiros no assert. + first_diff = next( + (i for i, (a, e) in enumerate(zip(actual, expected)) if a != e), + min(len(actual), len(expected)), + ) + window = 60 + start = max(0, first_diff - window) + assert actual == expected, ( + f"caso {caso!r} diverge no byte {first_diff}:\n" + f" actual: ...{actual[start:first_diff + window]!r}...\n" + f" expected: ...{expected[start:first_diff + window]!r}..." + )