Files
sowai-fiscal/tests/test_chave_acesso.py
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

114 lines
4.7 KiB
Python

"""Chave de acesso NF-e: cUF(2)+AAMM(4)+CNPJ(14)+mod(2)+série(3)+nNF(9)+
tpEmis(1)+cNF(8)+DV(1) = 44 dígitos. DV = módulo 11 (pesos 2..9, da direita
para a esquerda; resto 0 ou 1 -> DV 0).
`dv_modulo11` é validado contra chaves REAIS (não inventadas) -- mas NÃO as
de `tests/modules/estoque/test_nfe_import.py` como o plano original
cogitava: aquele fixture é hand-written (docstring do próprio arquivo o diz)
e tem só UMA chave de 44 dígitos, cujo último dígito NÃO bate com o DV
módulo 11 (conferido por script antes de escrever este teste -- não é uma
chave real, é um ID de exemplo forjado só para exercitar o parser). Fonte
usada em vez disso: os XMLs de amostra que o próprio pacote `nfelib`
(dependência já pinada em pyproject.toml, versão instalada no pod
confirmada em 2.5.2 na Task 1 Step 0) empacota em
`nfelib/nfe/samples/v4_0/leiauteNFe/*.xml` -- os NOMES desses arquivos SÃO
as chaves de acesso de NF-e reais/realistas de exemplo, com DV
correto -- confirmado por decomposição manual antes de escrever este teste:
`35180834128745000152550010000476711079516696` (mod=55, DV correto=6) e
`35200159594315000157550010000000012062777161` (mod=55, DV correto=1,
também o `Id` do `infNFe` dentro de
`NFe35200159594315000157550010000000012062777161.xml`)."""
import pytest
from sowai_fiscal.chave_acesso import dv_modulo11, gerar_cnf, montar_chave_acesso
def test_dv_modulo11_contra_chave_real_1_amostra_nfelib():
chave44 = "35180834128745000152550010000476711079516696"
assert dv_modulo11(chave44[:43]) == chave44[43]
def test_dv_modulo11_contra_chave_real_2_amostra_nfelib():
chave44 = "35200159594315000157550010000000012062777161"
assert dv_modulo11(chave44[:43]) == chave44[43]
def test_dv_modulo11_rejeita_chave_com_tamanho_errado():
with pytest.raises(ValueError):
dv_modulo11("123")
def test_montar_chave_tem_44_digitos_e_dv_valido():
chave = montar_chave_acesso(
uf_ibge="41", aamm="2607", cnpj="12345678000190",
modelo="55", serie=1, numero=1014, tp_emis="1", cnf="87654321",
)
assert len(chave) == 44 and chave.isdigit()
assert chave[-1] == dv_modulo11(chave[:43])
assert chave[22:25] == "001" and chave[25:34] == "000001014"
def test_gerar_cnf_8_digitos_e_diferente_do_nnf():
for _ in range(50):
cnf = gerar_cnf(numero_nnf=1014)
assert len(cnf) == 8 and cnf.isdigit()
assert int(cnf) != 1014 # regra NT2019.001: cNF != nNF
def test_gerar_cnf_nunca_all_zeros():
# F3a (review 2026-07-16): all-zeros é 8 dígitos válidos no FORMATO mas
# é o chute óbvio de um gerador fraco -- nunca deve sair do sorteio,
# mesmo por acaso estatístico (1 em 10^8 por chamada; 200 chamadas aqui
# deixa a chance de falso-negativo desprezível).
for _ in range(200):
assert gerar_cnf(numero_nnf=1014) != "00000000"
def test_montar_chave_acesso_rejeita_componentes_que_se_compensam_no_total():
# F1 (review 2026-07-16, Important): o bug real -- checar SÓ o tamanho
# agregado (43 dígitos) deixava passar um CNPJ de 13 dígitos (errado,
# deveria ter 14) somado a um cNF de 9 dígitos (errado, deveria ter 8):
# 13 + 9 = 22, o mesmo total de dígitos que o par correto 14 + 8 = 22 --
# a chave "compensada" saía com 43 dígitos e um DV módulo-11 sintaticamente
# válido, mas apontando para o CNPJ ERRADO (um dígito do cNF vazando para
# dentro do que deveria ser o CNPJ). A validação por COMPONENTE (não só o
# total) tem que capturar isto.
with pytest.raises(ValueError, match="cnpj"):
montar_chave_acesso(
uf_ibge="41", aamm="2607", cnpj="1234567800019", # 13 dígitos, não 14
modelo="55", serie=1, numero=1014, tp_emis="1",
cnf="876543210", # 9 dígitos, não 8 -- compensa o total em 43
)
@pytest.mark.parametrize(
"campo,valor",
[
("uf_ibge", "4"),
("uf_ibge", "411"),
("aamm", "260"),
("cnpj", "1234567800019"),
("cnpj", "123456780001900"),
("modelo", "5"),
("tp_emis", "12"),
("cnf", "8765432"),
("cnf", "876543210"),
],
)
def test_montar_chave_acesso_rejeita_cada_componente_fora_do_tamanho(campo, valor):
kwargs = dict(
uf_ibge="41", aamm="2607", cnpj="12345678000190",
modelo="55", serie=1, numero=1014, tp_emis="1", cnf="87654321",
)
kwargs[campo] = valor
with pytest.raises(ValueError):
montar_chave_acesso(**kwargs)
def test_montar_chave_acesso_rejeita_componente_nao_numerico():
with pytest.raises(ValueError, match="cnpj"):
montar_chave_acesso(
uf_ibge="41", aamm="2607", cnpj="1234567800019A",
modelo="55", serie=1, numero=1014, tp_emis="1", cnf="87654321",
)