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
- 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.
- Atualize o pedido para
pagode forma idempotente. Se ele já estava pago, não faça nada de novo. - 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.
- Coloque a emissão numa fila. O webhook responde rápido ao gateway; quem fala com a Sefaz ou com o emissor é o worker.
- No worker, confira o estado do pedido antes de emitir. Se foi cancelado entre o pagamento e o job, não emita.
- Grave chave, protocolo e XML autorizado, gere o DANFE e libere a etiqueta de envio.
- 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?
Referência: https://arquivei.com.br/blog/como-emitir-notas-fiscais-em-lojas-virtuais/