Programação Por Equipe DevBras

Capa do artigo: Nota fiscal na loja virtual: emitir no evento certo e uma vez só

Nota fiscal na loja virtual: emitir no evento certo e uma vez só

Ligue a emissão da NF-e ao pagamento confirmado, com idempotência por pedido, trate cancelamento e devolução como eventos próprios e guarde o XML autorizado para reenviar sem emitir de novo.

A Torra Fina, loja fictícia de cafés especiais, emitia a nota no clique de "finalizar compra". Num mês, 38 notas foram para pedidos cujo Pix nunca foi pago, e o contador passou uma tarde cancelando uma por uma. No mês seguinte, mudaram para "emitir quando o gateway avisar o pagamento", e o gateway mandou o mesmo aviso três vezes para alguns pedidos. A resposta curta: emita a nota quando o pagamento for confirmado e a mercadoria for sair, nunca antes, e garanta no banco que cada pedido gera uma emissão só, não importa quantas vezes o evento chegue.

Este guia trata do gatilho e do fluxo no código da loja. Natureza da operação, CFOP, impostos e regras de venda para outro estado são decisões do contador. Isto não substitui a orientação de um profissional de contabilidade.

Por que o momento da emissão importa tanto

A NF-e de venda acompanha a saída da mercadoria: o DANFE vai junto com o pacote, a transportadora pode recusar a coleta sem ele, e a nota registra para o fisco que aquela mercadoria saiu. Emitir cedo demais cria nota de venda que não aconteceu. Emitir tarde demais trava a expedição.

O ponto saudável, para produto físico, fica entre dois eventos: pagamento confirmado e separação do pedido. E "confirmado" depende do meio de pagamento:

  • Pix: quando o provedor confirma o recebimento, não quando o QR Code é gerado.
  • Boleto: quando a compensação é informada, o que pode levar dias.
  • Cartão: quando a transação é capturada e passou pelo antifraude. Uma autorização ainda pendente de análise não é venda garantida.

Serviço é outra história. Assinatura, curso online e consultoria costumam pedir nota fiscal de serviço, emitida pela prefeitura, com regras próprias. Confirme com o contador qual documento cada produto da loja exige.

Como ligar a emissão ao pagamento, passo a passo

  1. Receba o aviso do gateway com assinatura conferida. Um webhook sem verificação deixa qualquer pessoa marcar pedido como pago; o guia de webhook com segredo mostra a conferência.
  2. Atualize o pedido para pago de forma idempotente. Se ele já estava pago, não faça nada de novo.
  3. Registre a intenção de emitir numa tabela com chave única por pedido. É essa restrição do banco que garante uma emissão só, mesmo com três avisos simultâneos.
  4. Coloque a emissão numa fila. O webhook responde rápido ao gateway; quem fala com a Sefaz ou com o emissor é o worker.
  5. No worker, confira o estado do pedido antes de emitir. Se foi cancelado entre o pagamento e o job, não emita.
  6. Grave chave, protocolo e XML autorizado, gere o DANFE e libere a etiqueta de envio.
  7. Se o emissor não responder, consulte a nota antes de tentar de novo. A lógica é a mesma do guia NF-e no seu sistema.

A emissão idempotente em Node.js

Exemplo com mysql2 e uma fila BullMQ. A tabela tem chave única em pedido_id; o INSERT IGNORE só insere na primeira vez, e o affectedRows diz se foi essa vez.

CREATE TABLE emissao_nota (
  pedido_id BIGINT PRIMARY KEY,
  status VARCHAR(20) NOT NULL,
  chave CHAR(44) NULL,
  protocolo VARCHAR(20) NULL,
  xml_autorizado MEDIUMTEXT NULL,
  erro VARCHAR(255) NULL,
  criada_em DATETIME NOT NULL,
  atualizada_em DATETIME NOT NULL
);
async function aoConfirmarPagamento(db, filaNotas, pedidoId) {
  const [resultado] = await db.execute(
    "INSERT IGNORE INTO emissao_nota (pedido_id, status, criada_em, atualizada_em) VALUES (?, 'pendente', NOW(), NOW())",
    [pedidoId]
  );
  if (resultado.affectedRows === 0) {
    return;
  }
  await filaNotas.add('emitir', { pedidoId }, { jobId: 'nota-' + pedidoId });
}

async function emitirNota(db, emissor, pedidoId) {
  const [[pedido]] = await db.execute('SELECT id, status FROM pedido WHERE id = ?', [pedidoId]);
  if (!pedido || pedido.status !== 'pago') {
    await db.execute(
      "UPDATE emissao_nota SET status = 'descartada', atualizada_em = NOW() WHERE pedido_id = ?",
      [pedidoId]
    );
    return;
  }
  const nota = await emissor.emitir(pedidoId);
  await db.execute(
    "UPDATE emissao_nota SET status = 'autorizada', chave = ?, protocolo = ?, xml_autorizado = ?, atualizada_em = NOW() WHERE pedido_id = ?",
    [nota.chave, nota.protocolo, nota.xml, pedidoId]
  );
}

O jobId fixo por pedido é uma segunda camada: a fila não aceita dois jobs com o mesmo id enquanto o primeiro existir. Mas a garantia principal é a chave única no banco, que vale mesmo se você trocar de fila. O objeto emissor representa o seu módulo fiscal ou a API do serviço de emissão que você contratou. O mesmo raciocínio vale para o próprio pagamento, como mostra o guia de idempotência no pagamento.

Cancelamento, estorno e devolução são eventos diferentes

O erro mais caro de e-commerce é tratar tudo como "cancelar o pedido". São pelo menos três coisas:

  • Cancelado antes da nota: o pedido muda de estado e nenhuma nota é emitida. O worker do passo 5 garante isso.
  • Cancelado depois da nota, antes de sair: a NF-e é cancelada por evento junto à Sefaz, dentro do prazo legal. O registro da nota continua no banco, com o protocolo do cancelamento ao lado. Nunca um DELETE.
  • Devolvido depois da entrega: a nota original já não pode ser cancelada, e a devolução costuma exigir outro documento fiscal. O procedimento depende do caso e é definido pelo contador.

O estorno do cartão ou a devolução do Pix é um evento financeiro separado do evento fiscal. Os dois precisam constar no histórico do pedido, cada um com data e comprovante. Uma loja que estorna o dinheiro e esquece a nota fica com saída registrada de uma mercadoria que voltou.

O que guardar e como reenviar ao cliente

Guarde a chave de acesso, o protocolo, o XML autorizado completo e o DANFE gerado a partir dele. Na área do cliente, o botão "baixar nota" lê esse arquivo. O e-mail de "reenviar nota" anexa esse mesmo arquivo. Nenhum desses botões chama o emissor de novo. Quando o serviço de emissão é terceirizado, o XML continua sendo responsabilidade da loja, como lembra o post API de nota não guarda por você.

Erros comuns

  • Emitir no clique de comprar. Pix não pago e cartão recusado viram notas que precisam ser canceladas uma a uma.
  • Confiar que o webhook chega uma vez só. Gateways reenviam avisos, e sem chave única a loja emite duas notas para o mesmo pedido.
  • Emitir dentro da requisição do webhook. A Sefaz demora, o gateway considera o aviso perdido e reenvia, multiplicando o problema.
  • Apagar a nota do banco ao cancelar o pedido. Você perde a prova da emissão e do cancelamento.
  • Gerar nota nova ao reenviar o e-mail. O cliente recebe dois documentos para a mesma compra e a numeração anda à toa.
  • Tratar estorno como cancelamento fiscal. O dinheiro volta e a nota continua valendo.

Checklist

  • A emissão só começa depois do pagamento confirmado pelo provedor.
  • Existe chave única por pedido na tabela de emissão.
  • O webhook confere a assinatura e responde sem esperar a Sefaz.
  • O worker revalida o estado do pedido antes de emitir.
  • Cancelamento fiscal, estorno e devolução aparecem como eventos separados no pedido.
  • Download e reenvio leem o XML guardado, sem nova emissão.
  • A regra fiscal de cada tipo de produto foi definida com o contador.

Abra um pedido de teste pago e outro cancelado antes do pagamento no seu ambiente de homologação. Cada um tem exatamente o documento que deveria ter, e se você disparar o mesmo webhook de pagamento duas vezes, quantas notas aparecem?

Avalie este artigo

Referência: https://arquivei.com.br/blog/como-emitir-notas-fiscais-em-lojas-virtuais/