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