O ADN entrega a quem tem um CNPJ todo documento fiscal em que ele aparece (notas emitidas, notas recebidas e eventos como o cancelamento) numa fila numerada por NSU, em lotes de até 50. É o caminho para importar para o seu sistema as notas emitidas fora dele, e para saber dos cancelamentos feitos no portal.
O cursor de NSU é seu
A biblioteca não guarda estado. Nfse.distribution/3 lê os documentos depois do NSU pedido e devolve o next_nsu, que é o ponto de partida da próxima chamada. Guarde o último next_nsu no seu banco, junto do CNPJ, e continue dele.
defmodule MeuApp.ImportacaoAdn do
def import_batch(client, cursor) do
case Nfse.distribution(client, cursor) do
{:ok, %Nfse.Distribution{status: :documents} = batch} ->
Enum.each(batch.documents, &store/1)
{:more, batch.next_nsu}
{:ok, %Nfse.Distribution{status: :no_documents}} ->
{:done, cursor}
{:error, %Nfse.Error{} = error} ->
{:error, error, cursor}
end
end
def import_all(client, cursor) do
case import_batch(client, cursor) do
{:more, next_cursor} ->
save_cursor(next_cursor)
import_all(client, next_cursor)
other ->
other
end
end
defp store(%Nfse.DistributedDocument{error: nil} = document) do
{document.nsu, document.type, document.access_key, document.xml}
end
defp store(%Nfse.DistributedDocument{error: %Nfse.Error{}} = document) do
{document.nsu, :unreadable, document.access_key, document.xml}
end
defp save_cursor(_nsu), do: :ok
endGrave o cursor só depois de gravar o lote. Se o processo cair no meio, o lote é lido de novo e a gravação precisa ser idempotente: use o NSU ou a chave de acesso como chave única.
O import_all termina sempre: um lote :documents traz um next_nsu maior que o cursor pedido. Se o ADN mandar documentos sem nenhum NSU acima do cursor, a biblioteca devolve {:error, %Nfse.Error{kind: :unexpected, details: {:stalled_nsu, cursor}}} em vez de um next_nsu igual ao cursor, que faria o laço pedir o mesmo lote para sempre. Trate esse erro como os outros: pare, registre e tente de novo mais tarde a partir do mesmo cursor.
O fim da fila
Quando não há mais nada depois do NSU, o ADN responde NENHUM_DOCUMENTO_LOCALIZADO, às vezes com status 404. A biblioteca devolve isso como sucesso, {:ok, %Nfse.Distribution{status: :no_documents, documents: []}}, e nunca como erro. Agende a próxima rodada para mais tarde, a partir do mesmo cursor.
Percorrendo com stream_distribution/3
Se preferir consumir documento a documento, Nfse.stream_distribution/3 percorre os lotes e termina sozinho no fim da fila:
client
|> Nfse.stream_distribution(cursor)
|> Stream.filter(&(&1.type == :nfse))
|> Enum.each(&MeuApp.Notas.import/1)Ao contrário das outras funções, o stream levanta Nfse.Error quando um lote falha, inclusive no :stalled_nsu. Proteja o laço com try/rescue se o processo não puder cair e, depois de uma falha, retome pelo último cursor que você gravou. Como o stream entrega documentos e não lotes, o cursor a guardar é o maior nsu inteiro já tratado:
last_nsu =
client
|> Nfse.stream_distribution(cursor)
|> Enum.reduce(cursor, fn document, last_nsu ->
MeuApp.Notas.import(document)
if is_integer(document.nsu), do: max(document.nsu, last_nsu), else: last_nsu
end)O que vem em cada documento
%Nfse.DistributedDocument{} traz:
| Campo | Conteúdo |
|---|---|
nsu | posição na fila; um valor que não é inteiro vem como o ADN o mandou, com error em {:invalid_nsu, valor} |
access_key | chave de acesso da nota |
type | :nfse, :evento, :dps, :pedido_registro_evento, :cnc, :nenhum ou :unknown |
event_type | como "CANCELAMENTO"; nil nas notas |
xml | o documento como o ADN o entregou |
document | o mesmo documento lido, só para :nfse e :evento |
error | a falha de leitura, quando houve |
generated_at | DateTime, ou NaiveDateTime quando o ADN não informa o fuso |
Documento ilegível não derruba o lote
Um documento que não se consegue ler (base64 ou gzip inválido, versão desconhecida, XML malformado, NSU que não é inteiro) volta com document: nil, o motivo em error e o xml bruto quando houve algum. O lote continua e o NSU avança, então uma nota estranha não trava a importação. Um NSU que não é inteiro fica fora do next_nsu. Guarde o xml bruto e o nsu para investigar depois:
for %Nfse.DistributedDocument{error: %Nfse.Error{} = error} = document <- batch.documents do
MeuApp.Pendencias.registrar(document.nsu, document.access_key, document.xml, error)
endConsultar os eventos de uma nota
{:ok, documents} = Nfse.events(client, access_key)Outro CNPJ da mesma raiz do certificado se consulta com a opção :cnpj_consulta:
Nfse.distribution(client, cursor, cnpj_consulta: "11222333000262")O formato da resposta de events/2 ainda não foi visto de verdade: veja o README.