Skip to content

Spedy — Integração Fiscal (NF-e / NFS-e)

O SocialPets integra com a Spedy para emissão automática de NF-e (Nota Fiscal Eletrônica de Produto) e NFS-e (Nota Fiscal de Serviço Eletrônica). A integração utiliza arquitetura event-driven com atualização de status via webhook assíncrono.

Venda Paga
▼ @TransactionalEventListener(AFTER_COMMIT)
VendaCadastrarListener ← async (@Async("spedyExecutor"))
├─ Decompõe itens: PRD → NF-e / SRV,ADC,ATD,PKG → NFS-e
├─ Calcula rateio de desconto proporcional
└─ SpedyService.cadastrarVenda()
▼ POST /api/spedy/orders
API Spedy
▼ Webhook assíncrono
WebhookSpedyController ← POST /webhook/spedy
└─ Atualiza VendaNotaFiscal (status, protocolo, data, erro)

Armazena os dados fiscais diretamente na tabela venda:

ColunaCampo JavaDescrição
id_spedy_nfeidSpedyNfeUUID da nota na Spedy
status_nfestatusNfeStatus atual da NF-e
data_hora_nfedataHoraNfeData/hora de autorização
protocolo_nfeprotocoloNfeProtocolo da SEFAZ
erro_nfeerroNfeMensagem de erro (se rejeitada)
id_spedy_nfseidSpedyNfseUUID do serviço na Spedy
status_nfsestatusNfseStatus atual da NFS-e
data_hora_nfsedataHoraNfseData/hora de emissão
protocolo_nfseprotocoloNfseNúmero RPS
erro_nfseerroNfseMensagem de erro (se rejeitada)

O método atualizarDados() impede regressão de status: uma nota EMITIDA não volta para REJEITADA, mas pode ir para CANCELADA.

ValorDescrição
EMITIDAAutorizada pela SEFAZ/prefeitura — download disponível
REJEITADARecusada — campo erro contém o motivo
CANCELADACancelada após emissão
PROCESSANDOEnviada, aguardando resposta
ValorCódigoDescrição
NFE_NOTA_FISCAL_PRODUTO1Produtos (estoque)
NFCE_NOTA_FISCAL_CONSUMIDOR2Balcão — não utilizado atualmente
NFSE_NOTA_FISCAL_SERVICO3Serviços, adicionais, atendimentos, pacotes

VendaService.realizarSincroniaVendaSpedy() verifica:

// Venda deve estar paga
if (!StatusVendaEnum.PG.equals(venda.getIndStatus())) return;
// Valor final deve ser > R$ 0,00
// (vendas 100% cobertas por pacote não geram NF — regra SP-85)
if (venda.getValorFinal() != null && venda.getValorFinal() <= 0.0) return;
// Loja deve ter fiscal configurado e habilitado
private static boolean realizaSincronizacaoSpedy(Loja loja) {
return loja != null
&& loja.isLojaPreparaParaEmissaoNF()
&& loja.deveSincronizarSpedy();
}

A venda é separada em dois grupos:

  • NF-e: itens com tipo PRD (produto de estoque)
  • NFS-e: itens com tipo SRV, ADC, ATD, PKG (serviços, adicionais, atendimentos, pacotes)

Os métodos Venda.getItensNFe() e Venda.getItensNFSe() fazem esse filtro.

O desconto global da venda (descontos extras + cupons) é distribuído proporcionalmente entre NF-e e NFS-e:

descontoNFe = (totalBrutoProdutos / totalBrutoVenda) × descontoGlobal
descontoNFSe = descontoGlobal - descontoNFe

Usa BigDecimal com RoundingMode.HALF_UP em todos os cálculos. O último item absorve o centavo residual para garantir que descontoNFe + descontoNFSe = descontoGlobal exatamente.

4. transactionId — chave de idempotência

Section titled “4. transactionId — chave de idempotência”

Cada envio à Spedy usa um transactionId único:

<uuidVenda>_PRD<4-chars-random> ← para NF-e
<uuidVenda>_SRV<4-chars-random> ← para NFS-e

O UUID da venda ocupa sempre os 36 primeiros caracteres — usado no webhook para localizar a venda.

Endpoint: POST /webhook/spedy Arquivo: WebhookSpedyController

O payload JSON da Spedy contém:

CampoDescrição
data.transactionIdID usado no envio (36 chars iniciais = UUID da venda)
data.modelTipo: productInvoice (NFE) ou serviceInvoice (NFSE)
data.authorization.protocolProtocolo da SEFAZ
data.processingDetail.onData/hora de processamento
data.processingDetail.messageMensagem de erro (se rejeitada)

Mapeamento de status Spedy → StatusNotaFiscalEnum:

Status SpedyStatus interno
authorized, successEMITIDA
rejected, error, deniedREJEITADA
cancelledCANCELADA
waiting, processing, enqueued, createdPROCESSANDO
MétodoEndpointDescrição
GET/web/vendas/{uuid}/nfe/pdfDownload NF-e em PDF (DANFE)
GET/web/vendas/{uuid}/nfe/xmlDownload NF-e em XML
GET/web/vendas/{uuid}/nfse/pdfDownload NFS-e em PDF
GET/web/vendas/{uuid}/nfse/xmlDownload NFS-e em XML
PUT/web/vendas/{uuid}/notasSincronizar status das notas (polling manual)
PUT/web/vendas/{uuid}/sincronizarReenviar venda para Spedy (fallback manual)

Todos os endpoints exigem header UUID-Loja (multi-tenant).

VendaService.dowloadNota() faz:

  1. Valida formato (pdf ou xml)
  2. Sincroniza o status antes de baixar (chama Spedy para garantir que está atualizado)
  3. Verifica se statusNfe/statusNfse == EMITIDA — lança NotaFiscalServiceException caso contrário
  4. Chama SpedyService.downloadNota() e retorna byte[]

Headers de resposta:

  • Content-Type: application/pdf ou application/xml
  • Content-Disposition: attachment; filename="nfe_<uuid>.pdf"
  • Cache-Control: no-cache, no-store, must-revalidate

Arquivo: br.com.socialpets.financeiro.spedy.SpedyService

Métodos relevantes para o fluxo fiscal:

// Cadastrar venda (dispara emissão da NF)
VendaDTO cadastrarVenda(VendaDTO vendaDTO, String token)
// Buscar nota por transactionId (polling de status)
Optional<NotaFiscalDTO> buscarNotasPorTransactionId(
TipoNotaFiscalEnum tipo, String transactionId, String token)
// Download do arquivo
byte[] downloadNota(TipoNotaFiscalEnum tipo, String idNota,
String formato, String token)
// Buscar venda na Spedy por transactionId
Optional<VendaDTO> buscarVendaPorTransactionId(String transactionId, String token)

O client monitora rate limiting via headers da resposta e loga WARN se restar menos de 5 requisições no bucket.

Campos relevantes para o fiscal:

CampoDescrição
tokenSpedyAPI token da loja na Spedy
idSpedyID da empresa na Spedy
indNfeAtivoFlag: emite NF-e (produtos)
indNfseAtivoFlag: emite NFS-e (serviços)
regimeTributarioNORMAL, SIMPLES_NACIONAL, MEI
regimeEspecialTributarioRegime especial (se aplicável)
codigoIbgeCidadeCódigo IBGE do município (7 dígitos)
inscricaoMunicipalInscrição municipal (obrigatória para NFS-e)
linkCertificadoDigitalURL do certificado no S3
senhaCertificadoDigitalSenha do certificado A1

Métodos de verificação:

loja.isLojaPreparaParaEmissaoNF() // dados fiscais completos e válidos
loja.deveSincronizarSpedy() // flag de sincronização habilitada
loja.deveEmitirNFe() // indNfeAtivo + tem itens produto
loja.deveEmitirNFSe() // indNfseAtivo + tem itens serviço
social-pets-service/src/main/java/br/com/socialpets/
├── financeiro/
│ ├── entity/
│ │ ├── Venda.java ← Entity principal (embedded VendaNotaFiscal)
│ │ ├── embeddable/
│ │ │ └── VendaNotaFiscal.java ← Dados fiscais da venda
│ │ └── StatusNotaFiscalEnum.java ← EMITIDA, REJEITADA, CANCELADA, PROCESSANDO
│ ├── service/
│ │ └── VendaService.java ← Orquestra emissão, sincronização e download
│ └── spedy/
│ ├── SpedyService.java ← Client HTTP Spedy
│ ├── SpedyClient.java ← RestTemplate com retry e rate-limit
│ ├── dto/
│ │ ├── VendaDTO.java ← Payload de venda para Spedy
│ │ ├── ItemVendaDTO.java ← Item de venda para Spedy
│ │ ├── NotaFiscalDTO.java ← Resposta de nota da Spedy
│ │ └── DecomposicaoFiscalDTO.java ← Separação produto vs serviço
│ ├── enums/
│ │ ├── TipoNotaFiscalEnum.java ← NFE, NFCE, NFSE
│ │ ├── TipoEmissaoNotaFiscalEnum.java ← IMEDIATAMENTE, APOS_PAGAMENTO, etc.
│ │ └── TipoEmissaoNFSEEnum.java ← NORMAL, FS_IA, ANNFS, etc.
│ └── event/
│ └── listener/
│ └── VendaCadastrarListener.java ← @TransactionalEventListener assíncrono
└── controller/
├── web/financeiro/
│ └── VendaWebController.java ← Endpoints de download e sincronização
└── webhook/
└── WebhookSpedyController.java ← Recebe webhooks da Spedy

Exception: NotaFiscalServiceException (RuntimeException)

Lançada quando:

  • Nota não encontrada na Spedy pelo transactionId
  • byte[] do download veio vazio
  • Status não é EMITIDA ao tentar baixar o arquivo

Logs relevantes:

INFO VendaCadastrarListener - Sincronizando venda <uuid> com Spedy
WARN SpedyService - Rate limit Spedy: apenas 4 requisições restantes
ERROR VendaCadastrarListener - Erro ao sincronizar venda <uuid> com Spedy: <msg>
INFO VendaService - Venda zerada — emissão de NF ignorada para <uuid>

Antes de enviar uma venda à Spedy, o listener verifica se idSpedyNfe ou idSpedyNfse já estão preenchidos. Se sim, a venda já foi sincronizada e o envio é ignorado. Isso previne duplicidade em caso de retentativas.

Se a venda não foi sincronizada automaticamente (ex: Spedy estava fora do ar), o endpoint PUT /web/vendas/{uuid}/sincronizar força o reenvio. Isso é útil para recuperação de falhas sem precisar reabrir/reimportar a venda.

A Spedy oferece ambiente de homologação separado da produção. O flag nfce_estado_homologacao na configuração da loja controla qual ambiente é usado. Notas emitidas em homologação não têm validade fiscal.