Nfse

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

O que fica de fora

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:

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ável Conteúdo
NFSE_PFX_PATH caminho do arquivo .pfx do emitente
NFSE_PFX_PASSWORD senha do .pfx
NFSE_C_LOC_EMI có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.