Cliente Elixir para a NFS-e Nacional: emite, consulta e cancela a nota pela Sefin Nacional, assina a DPS com o certificado A1 do emitente, importa documentos pelo ADN e lê os parâmetros municipais.

Só depende de req, decimal e saxy. Não há processo, Application nem configuração global: tudo passa por um %Nfse.Client{}.

Instalação

def deps do
  [
    {:nfse, "~> 0.1.0"}
  ]
end

Requer Elixir 1.15 e OTP 26 ou mais novos. O openssl do sistema precisa estar instalado, porque é ele que abre o .pfx. Veja o guia Certificado digital.

Exemplo

Emissão do corte de cabelo de R$ 49,90 do "Studio Ana", prestadora do Simples Nacional em Salvador, na produção restrita:

{:ok, certificate} =
  "certificado.pfx"
  |> File.read!()
  |> Nfse.Certificate.from_pfx(System.fetch_env!("NFSE_PFX_PASSWORD"))

{:ok, client} = Nfse.Client.new(certificate: certificate, environment: :restricted)

now = DateTime.truncate(DateTime.utc_now(), :second)

dps = %Nfse.DPS{
  # Um minuto de folga para um relógio adiantado: o dh_emi não pode estar à frente da Sefin.
  dh_emi: DateTime.add(now, -60),
  ver_aplic: "studio-ana-1.0",
  serie: "1",
  n_dps: 42,
  # A competência é a data em Brasília (UTC-3), que perto da meia-noite difere da data em UTC.
  d_compet: now |> DateTime.add(-3 * 3600) |> DateTime.to_date(),
  tp_emit: 1,
  c_loc_emi: "2927408",
  prest: %{
    cnpj: "11222333000181",
    reg_trib: %{op_simp_nac: 3, reg_ap_trib_sn: 1, reg_esp_trib: 0}
  },
  toma: %{cpf: "52998224725", x_nome: "Ana Souza"},
  serv: %{
    loc_prest: %{c_loc_prestacao: "2927408"},
    c_serv: %{c_trib_nac: "060101", x_desc_serv: "Corte de cabelo"}
  },
  valores: %{
    v_serv_prest: %{v_serv: Decimal.new("49.90")},
    trib: %{
      trib_mun: %{trib_issqn: 1, tp_ret_issqn: 1},
      tot_trib: %{p_tot_trib_sn: Decimal.new("6.00")}
    }
  }
}

{:ok, issued} = Nfse.issue(client, dps)

issued.access_key
issued.xml

Os campos seguem o nome do XSD em snake_case, com as siglas NFSe, DFSe, NFe e DFe contadas como uma palavra só (infNFSe vira inf_nfse). Toda função pública devolve {:ok, _} ou {:error, %Nfse.Error{}}, com uma exceção: Nfse.stream_distribution/3 levanta Nfse.Error.

Guias

O que a biblioteca cobre

  • Montagem, validação contra o schema e assinatura da DPS (leiaute 1.01), com o Id calculado
  • Emissão síncrona (Nfse.issue/2) e por decisão administrativa ou judicial (Nfse.issue_by_court_order/2)
  • Consulta de NFS-e, conferência de uma DPS pelo Id e busca da chave de acesso
  • Cancelamento e demais eventos do contribuinte (Nfse.cancel/3, Nfse.register_event/2, Nfse.fetch_event/4)
  • Distribuição de documentos pelo ADN, com cursor de NSU, stream_distribution/3 e leitura dos eventos de uma nota
  • Parâmetros municipais do ADN: convênio, alíquotas, regimes especiais, retenções e benefícios
  • Leitura estrita de NFS-e e eventos nos leiautes 1.00 e 1.01
  • Reenvio seguro: uma escrita só é repetida quando o pedido nunca saiu da máquina; o resto volta com o dps_id para conferir (veja Conferir após timeout)

O que fica de fora

  • As APIs de cada município. Esta biblioteca fala só com o sistema nacional.
  • A geração do DANFSe (o PDF da nota). Para isso existe a biblioteca nfse_danfse.
  • As operações de alteração dos parâmetros municipais (POST .../{idManut}), que pertencem ao município.

O que ainda não está confirmado

Estes pontos dependem da execução contra a produção restrita, descrita na próxima seção, e estão também nas seções "O que não está confirmado" de cada módulo:

  • O caminho da Sefin na produção restrita: /API/SefinNacional, como no portal gov.br, ou /SefinNacional.
  • A assinatura da DPS em RSA-SHA1 ou RSA-SHA256.
  • Se o ME/EPP do Simples Nacional informa pAliq.
  • A série da DPS com ^ ou $, que o padrão do XSD permite ao pé da letra.
  • Emoji no xNome. O XSD não tem o padrão Latin-1 nesse campo e a biblioteca o segue; se a Sefin recusa, a recusa chega como :rejected.
  • Campos de texto só com espaços em branco.
  • A recusa de uma DPS repetida pelo Id, de que depende o reenvio depois de uma conferência negativa (veja Conferir após timeout). Se a Sefin a aceitar, a nota sai duas vezes.
  • O formato da resposta de GET /nfse/{chave}/eventos/{tipo}/{sequência} (Nfse.fetch_event/4), documentado com o mesmo campo eventoXmlGZipB64 do registro, e o de GET /contribuintes/NFSe/{chave}/Eventos (Nfse.events/2).
  • O fluxo de decisão judicial, que só se prova com um município que cadastre a decisão e autorize o contribuinte. Dentro dele, o nDFSe "0" que o manual exige e o padrão do XSD recusa ([1-9]{1}[0-9]{0,12}): a biblioteca segue o manual.
  • As chaves beneficio e regimesEspeciais dos parâmetros municipais, que o swagger não mostra.
  • A cadeia do certificado no handshake. A biblioteca lê a cadeia do .pfx, mas o TLS envia só a folha. Se a Sefin recusar o handshake pedindo a cadeia, enviar cert: [folha | cadeia] (veja "O que não está confirmado" em Nfse.Client).

Rodando contra a produção restrita

A suíte test/restricted/journey_test.exs conversa com a produção restrita da Sefin e do ADN, onde os documentos não têm validade jurídica, usando um certificado ICP-Brasil A1 de verdade. Ela fica fora do mix test e precisa de três variáveis de ambiente:

VariávelConteúdo
NFSE_PFX_PATHcaminho do arquivo .pfx do emitente
NFSE_PFX_PASSWORDsenha do .pfx
NFSE_C_LOC_EMIcódigo IBGE de sete dígitos do município do emitente
NFSE_PFX_PATH=/caminho/certificado.pfx NFSE_PFX_PASSWORD=... NFSE_C_LOC_EMI=2927408 mix test --only restricted

O CNPJ do emitente sai do certificado. Sem as variáveis, a suíte falha e diz quais faltam. Cada execução emite e cancela uma nota de teste; o tomador é fictício.