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:
@@ -0,0 +1,73 @@
|
||||
"""Bloco B (spec 2026-07-14): catálogo de domínios tributários.
|
||||
|
||||
`tax_domain` é String no banco de propósito (imposto-como-dado): um domínio
|
||||
novo (IBS/CBS/futuro) é um membro novo NO ENUM PYTHON, sem ALTER TYPE, sem
|
||||
migration, sem o gotcha enum-NAME-vs-value. Estes testes travam o contrato
|
||||
que o frontend consome via GET /fiscal/tax-domains e a tabela de pesos/
|
||||
transposição que o resolvedor usa.
|
||||
"""
|
||||
from sowai_fiscal.domains import (
|
||||
DOMAIN_FIELDS,
|
||||
MATCHER_WEIGHTS,
|
||||
TaxDomain,
|
||||
transpose_cfop,
|
||||
)
|
||||
|
||||
|
||||
def test_tax_domain_has_the_reform_domains_without_any_migration():
|
||||
# A prova literal do "imposto-como-dado": IBS/CBS já são domínios aceitos
|
||||
# hoje, e não existe NENHUM tipo enum Postgres para tax_domain.
|
||||
assert TaxDomain.IBS.value == "ibs"
|
||||
assert TaxDomain.CBS.value == "cbs"
|
||||
assert {d.value for d in TaxDomain} >= {
|
||||
"icms", "icmsst", "ipi", "pis", "cofins", "difal", "fcp", "ibs", "cbs", "iss",
|
||||
}
|
||||
|
||||
|
||||
def test_domain_fields_catalog_covers_every_domain():
|
||||
# O frontend monta o form por domínio a partir deste catálogo — cada
|
||||
# domínio precisa declarar campos aplicáveis e obrigatórios.
|
||||
for domain in TaxDomain:
|
||||
spec = DOMAIN_FIELDS[domain.value]
|
||||
assert spec.label
|
||||
assert set(spec.campos_obrigatorios) <= set(spec.campos_aplicaveis)
|
||||
for grupo in spec.campos_um_de:
|
||||
assert set(grupo) <= set(spec.campos_aplicaveis)
|
||||
assert not set(grupo) & set(spec.campos_obrigatorios) # um-de nunca também obrigatório
|
||||
|
||||
|
||||
def test_icms_declares_the_exclusive_situacao_group():
|
||||
# A UI exige "situação tributária" (cst OU csosn) a partir DAQUI — sem
|
||||
# este grupo no catálogo, o form dinâmico deixaria passar regra de ICMS
|
||||
# sem situação e o usuário comeria um 422 imprevisto (review frontend).
|
||||
assert ("cst", "csosn") in DOMAIN_FIELDS["icms"].campos_um_de
|
||||
assert ("cst", "csosn") in DOMAIN_FIELDS["icmsst"].campos_um_de
|
||||
|
||||
|
||||
def test_matcher_weights_are_strict_powers_of_two():
|
||||
# Cada peso supera a SOMA de todos os mais fracos (64 > 63): nenhuma
|
||||
# combinação de matchers genéricos vence um mais seletivo. É a correção
|
||||
# do review do frontend sobre "contagem de matchers".
|
||||
ordered = sorted(MATCHER_WEIGHTS.values())
|
||||
for i, w in enumerate(ordered):
|
||||
assert w > sum(ordered[:i])
|
||||
assert MATCHER_WEIGHTS["ncm_prefix"] == 64
|
||||
assert MATCHER_WEIGHTS["cest"] == 32
|
||||
assert MATCHER_WEIGHTS["consumidor_final"] == 16
|
||||
assert MATCHER_WEIGHTS["indicador_ie"] == 8
|
||||
assert MATCHER_WEIGHTS["uf_destino_tipo"] == 4
|
||||
assert MATCHER_WEIGHTS["tipo_operacao"] == 2
|
||||
assert MATCHER_WEIGHTS["crt"] == 1
|
||||
|
||||
|
||||
def test_transpose_cfop_uses_the_explicit_table_for_st():
|
||||
# 5405 → 6403, NÃO 6405: é por isso que a tabela explícita existe.
|
||||
assert transpose_cfop("5405") == "6403"
|
||||
assert transpose_cfop("5102") == "6102"
|
||||
assert transpose_cfop("1202") == "2202"
|
||||
|
||||
|
||||
def test_transpose_cfop_generic_fallback_swaps_the_first_digit():
|
||||
# CFOP fora da tabela: regra geral 5xxx→6xxx / 1xxx→2xxx.
|
||||
assert transpose_cfop("5949") == "6949"
|
||||
assert transpose_cfop("1949") == "2949"
|
||||
Reference in New Issue
Block a user