Pular para conteúdo

0006 — Validar a alíquota do ISS antes de enviar

Status: Aceita Data: 2026-09-18

Contexto

A pergunta que originou esta decisão foi direta: "o que acontece se eu mandar a alíquota errada na emissão? a apuração não vai pegar?"

Não pega. Quem valida pAliq é a Sefin Nacional, na recepção da DPS, e a resposta é uma rejeição — não uma nota com o imposto recalculado. A nota simplesmente não existe. Pior: até aqui o emissor mandava a alíquota que estivesse no nfse.yaml sem olhar para ela, e o usuário só descobria o erro depois de gastar a viagem até o governo.

As regras estão na planilha oficial docs/anexos/ANEXO_I-SEFIN_ADN-DPS_NFSe-SNNFSe-v1.00-20251226.xlsx, aba "RN DPS_NFS-e". Lidas juntas, elas dizem que quem pode declarar alíquota depende de três campos que já estavam no XML e não conversavam entre si: opSimpNac (situação no Simples), regApTribSN (regime de apuração) e tpRetISSQN (quem retém).

Regra Situação Alíquota
E0595 qualquer um nunca acima de 5%
E0600 MEI não pode informar — o ISS sai no DAS
E0625 / E0631 ME/EPP no Simples, sem retenção não pode informar
E0621 / E0628 ME/EPP no Simples, com retenção precisa informar, mínimo 1,8%
E0635 / E0640 ME/EPP apurando fora do Simples depende do convênio do município

Havia ainda um buraco de entrada: tpRetISSQN era sempre 1 porque não existia nenhuma forma de dizer que o tomador retém. Com isso, o único caso em que declarar alíquota é obrigatório era inalcançável pelo CLI.

Decisão

Três coisas, nesta ordem.

Dar entrada ao que faltava. --retencao (nao | tomador | intermediario) e padroes.valores.retencao_issqn alimentam tpRetISSQN; prestador.regime_apuracao (sn | iss-municipio | fora-do-sn) alimenta regApTribSN e tem sn como padrão, que é o caso de quem está dentro dos limites do Simples.

Validar localmente, em internal/domain/validation. A função recebe os quatro valores e devolve um erro em português dizendo o que fazer e citando o código da regra, para quem quiser conferir na planilha:

erro: sem retencao do ISSQN, um ME/EPP do Simples nao pode informar aliquota
(a Sefin rejeita: E0625). Use iss_aliquota: 0, ou informe --retencao se o
tomador retem

Não opinar onde não dá para saber. E0635 e E0640 dependem de o município ter convênio ativo com o Sistema Nacional. Isso não está na máquina — está numa consulta ao ADN. Nesses casos a validação devolve nil e sai da frente.

O corte é este: a regra é aplicada localmente quando o caso ativo e o inativo concordam. Para MEI e para ME/EPP dentro do Simples eles concordam, e é aí que mora a quase totalidade dos usuários deste emissor.

Consequências

  • Uma classe inteira de rejeição vira mensagem no terminal, antes de assinar. O certificado não é usado para produzir um documento que já se sabe recusado.
  • A validação é ingênua de propósito: só o que a planilha garante. Não há tabela de alíquotas por município embutida no binário, que envelheceria em silêncio.
  • Existe um caminho que o emissor deixa passar e o governo pode recusar (ME/EPP apurando fora do Simples). Está documentado em docs/convenio-municipal.md, com como consultar.
  • validation.ValidateISSRate é uma função pura sobre quatro inteiros e um float. Os testes não precisam de certificado, rede nem XML.

Aprendizado

O padrão que já apareceu nos ADRs 0003 e 0005 apareceu de novo, com outra roupa. Lá, o código estava errado porque ninguém o tinha confrontado com o XSD nem com o swagger. Aqui, o código estava incompleto porque ninguém o tinha confrontado com a planilha de regras de negócio — que estava versionada no repositório desde o começo, em docs/anexos/.

Os três artefatos oficiais (esquema, contrato e regras) respondem perguntas diferentes. O XSD diz que pAliq é um decimal válido. O swagger diz para onde mandar. Só a planilha diz quando mandar. Um emissor que só consulta os dois primeiros gera XML perfeitamente bem formado e perfeitamente rejeitado.

A segunda lição é sobre o que fazer com a incerteza. A tentação era chutar o caso do convênio — provavelmente ativo, Curitiba é cidade grande. A alternativa escolhida foi explicitar a fronteira: validar onde a resposta é a mesma nos dois mundos, calar onde não é, e escrever onde perguntar. Uma validação que às vezes mente é pior que uma validação que não existe, porque a primeira é confiada.