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.
Visão geral da arquitetura
Section titled “Visão geral da arquitetura”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íncronoWebhookSpedyController ← POST /webhook/spedy │ └─ Atualiza VendaNotaFiscal (status, protocolo, data, erro)Domínio: entidades principais
Section titled “Domínio: entidades principais”VendaNotaFiscal (embeddable em Venda)
Section titled “VendaNotaFiscal (embeddable em Venda)”Armazena os dados fiscais diretamente na tabela venda:
| Coluna | Campo Java | Descrição |
|---|---|---|
id_spedy_nfe | idSpedyNfe | UUID da nota na Spedy |
status_nfe | statusNfe | Status atual da NF-e |
data_hora_nfe | dataHoraNfe | Data/hora de autorização |
protocolo_nfe | protocoloNfe | Protocolo da SEFAZ |
erro_nfe | erroNfe | Mensagem de erro (se rejeitada) |
id_spedy_nfse | idSpedyNfse | UUID do serviço na Spedy |
status_nfse | statusNfse | Status atual da NFS-e |
data_hora_nfse | dataHoraNfse | Data/hora de emissão |
protocolo_nfse | protocoloNfse | Número RPS |
erro_nfse | erroNfse | Mensagem 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.
StatusNotaFiscalEnum
Section titled “StatusNotaFiscalEnum”| Valor | Descrição |
|---|---|
EMITIDA | Autorizada pela SEFAZ/prefeitura — download disponível |
REJEITADA | Recusada — campo erro contém o motivo |
CANCELADA | Cancelada após emissão |
PROCESSANDO | Enviada, aguardando resposta |
TipoNotaFiscalEnum
Section titled “TipoNotaFiscalEnum”| Valor | Código | Descrição |
|---|---|---|
NFE_NOTA_FISCAL_PRODUTO | 1 | Produtos (estoque) |
NFCE_NOTA_FISCAL_CONSUMIDOR | 2 | Balcão — não utilizado atualmente |
NFSE_NOTA_FISCAL_SERVICO | 3 | Serviços, adicionais, atendimentos, pacotes |
Fluxo de emissão detalhado
Section titled “Fluxo de emissão detalhado”1. Condições para emissão
Section titled “1. Condições para emissão”VendaService.realizarSincroniaVendaSpedy() verifica:
// Venda deve estar pagaif (!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 habilitadoprivate static boolean realizaSincronizacaoSpedy(Loja loja) { return loja != null && loja.isLojaPreparaParaEmissaoNF() && loja.deveSincronizarSpedy();}2. Decomposição fiscal dos itens
Section titled “2. Decomposição fiscal dos itens”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.
3. Rateio de desconto
Section titled “3. Rateio de desconto”O desconto global da venda (descontos extras + cupons) é distribuído proporcionalmente entre NF-e e NFS-e:
descontoNFe = (totalBrutoProdutos / totalBrutoVenda) × descontoGlobaldescontoNFSe = descontoGlobal - descontoNFeUsa 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-eO UUID da venda ocupa sempre os 36 primeiros caracteres — usado no webhook para localizar a venda.
5. Webhook de atualização de status
Section titled “5. Webhook de atualização de status”Endpoint: POST /webhook/spedy
Arquivo: WebhookSpedyController
O payload JSON da Spedy contém:
| Campo | Descrição |
|---|---|
data.transactionId | ID usado no envio (36 chars iniciais = UUID da venda) |
data.model | Tipo: productInvoice (NFE) ou serviceInvoice (NFSE) |
data.authorization.protocol | Protocolo da SEFAZ |
data.processingDetail.on | Data/hora de processamento |
data.processingDetail.message | Mensagem de erro (se rejeitada) |
Mapeamento de status Spedy → StatusNotaFiscalEnum:
| Status Spedy | Status interno |
|---|---|
authorized, success | EMITIDA |
rejected, error, denied | REJEITADA |
cancelled | CANCELADA |
waiting, processing, enqueued, created | PROCESSANDO |
Endpoints REST (VendaWebController)
Section titled “Endpoints REST (VendaWebController)”| Método | Endpoint | Descrição |
|---|---|---|
GET | /web/vendas/{uuid}/nfe/pdf | Download NF-e em PDF (DANFE) |
GET | /web/vendas/{uuid}/nfe/xml | Download NF-e em XML |
GET | /web/vendas/{uuid}/nfse/pdf | Download NFS-e em PDF |
GET | /web/vendas/{uuid}/nfse/xml | Download NFS-e em XML |
PUT | /web/vendas/{uuid}/notas | Sincronizar status das notas (polling manual) |
PUT | /web/vendas/{uuid}/sincronizar | Reenviar venda para Spedy (fallback manual) |
Todos os endpoints exigem header UUID-Loja (multi-tenant).
Download de notas
Section titled “Download de notas”VendaService.dowloadNota() faz:
- Valida formato (
pdfouxml) - Sincroniza o status antes de baixar (chama Spedy para garantir que está atualizado)
- Verifica se
statusNfe/statusNfse == EMITIDA— lançaNotaFiscalServiceExceptioncaso contrário - Chama
SpedyService.downloadNota()e retornabyte[]
Headers de resposta:
Content-Type: application/pdfouapplication/xmlContent-Disposition: attachment; filename="nfe_<uuid>.pdf"Cache-Control: no-cache, no-store, must-revalidate
SpedyService — client HTTP
Section titled “SpedyService — client HTTP”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 arquivobyte[] downloadNota(TipoNotaFiscalEnum tipo, String idNota, String formato, String token)
// Buscar venda na Spedy por transactionIdOptional<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.
Configuração de loja (Loja entity)
Section titled “Configuração de loja (Loja entity)”Campos relevantes para o fiscal:
| Campo | Descrição |
|---|---|
tokenSpedy | API token da loja na Spedy |
idSpedy | ID da empresa na Spedy |
indNfeAtivo | Flag: emite NF-e (produtos) |
indNfseAtivo | Flag: emite NFS-e (serviços) |
regimeTributario | NORMAL, SIMPLES_NACIONAL, MEI |
regimeEspecialTributario | Regime especial (se aplicável) |
codigoIbgeCidade | Código IBGE do município (7 dígitos) |
inscricaoMunicipal | Inscrição municipal (obrigatória para NFS-e) |
linkCertificadoDigital | URL do certificado no S3 |
senhaCertificadoDigital | Senha do certificado A1 |
Métodos de verificação:
loja.isLojaPreparaParaEmissaoNF() // dados fiscais completos e válidosloja.deveSincronizarSpedy() // flag de sincronização habilitadaloja.deveEmitirNFe() // indNfeAtivo + tem itens produtoloja.deveEmitirNFSe() // indNfseAtivo + tem itens serviçoEstrutura de arquivos (backend)
Section titled “Estrutura de arquivos (backend)”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 SpedyTratamento de erros
Section titled “Tratamento de erros”Exception: NotaFiscalServiceException (RuntimeException)
Lançada quando:
- Nota não encontrada na Spedy pelo
transactionId byte[]do download veio vazio- Status não é
EMITIDAao tentar baixar o arquivo
Logs relevantes:
INFO VendaCadastrarListener - Sincronizando venda <uuid> com SpedyWARN SpedyService - Rate limit Spedy: apenas 4 requisições restantesERROR VendaCadastrarListener - Erro ao sincronizar venda <uuid> com Spedy: <msg>INFO VendaService - Venda zerada — emissão de NF ignorada para <uuid>Idempotência
Section titled “Idempotência”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.
Sincronização manual
Section titled “Sincronização manual”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.
Ambiente de homologação
Section titled “Ambiente de homologação”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.