Skip to content

Marketplace

Visão Geral

Centraliza integrações com plataformas de delivery (iFood, Rappi, UberEats). Recebe webhooks de pedidos, normaliza para o formato interno e despacha para o KDS via WebSocket — tudo sem re-keying.

Filosofia: uma única fonte canônica de menu (Popinaflow), propagada para os marketplaces via fila assíncrona.

Localização

CamadaArquivo
Módulobackend/src/marketplace/marketplace.module.ts
Webhooksbackend/src/marketplace/controllers/marketplace-webhook.controller.ts
Syncbackend/src/marketplace/marketplace-sync.service.ts
Processorbackend/src/marketplace/processors/marketplace-order.processor.ts
Normalizersbackend/src/marketplace/normalizers/{ifood,rappi,ubereats}.normalizer.ts
View Adminfrontend-react/src/views/admin/MarketplaceView.tsx
Inbox unificadofrontend-react/src/views/admin/orders/UnifiedInboxView.tsx

Rotas

RotaPropósito
/admin/marketplaceConfigurar credenciais, status de cada integração
/admin/orders/inboxInbox unificado de todos os canais
/admin/marketplace/ifood/healthSLA dashboard (ver ifood-health.md)

Webhooks Públicos

EndpointVerificaçãoHeader de assinatura
POST /t/:slug/webhooks/ifoodHMAC-SHA256x-ifood-hmac-sha256
POST /t/:slug/webhooks/rappiHMAC-SHA256x-rappi-signature
POST /t/:slug/webhooks/ubereatsRS256 JWTAuthorization: Bearer <jwt> (JWKS)

Cada controller verifica a assinatura sobre o raw body (capturado pelo verify callback do express.json em main.ts) — re-serializar via JSON.stringify quebraria HMAC por whitespace/ordering.

Quando IFOOD_WEBHOOK_SECRET (etc.) não está configurado, a verificação é fail-open (modo sandbox).


Idempotência (P2)

Middleware global IdempotencyKeyMiddleware (backend/src/common/middleware/idempotency-key.middleware.ts) aplicado nas três rotas de webhook:

  1. Lê header idempotency-key (RFC-style)
  2. Se ausente, extrai chave do body via per-provider extractor:
    • iFood: body.id
    • Rappi: body.event.id ou body.eventId
    • UberEats: body.event_id
  3. Usa Redis.setnx com TTL 24h (atomic SET NX EX)
  4. Se a chave já existe → curto-circuita 200 com { deduplicated: true }

Fail-open em outage do Redis: preferimos aceitar duplicata a rejeitar webhook (iFood retiraria credenciais por falha repetida).


Fila BullMQ

Duas filas registradas em MarketplaceModule:

  • marketplace-orders — ingestão de pedidos
  • marketplace-catalog-sync — push de catálogo

Configuração: attempts: 3, exponential backoff 2s/5s, removeOnComplete: 100, removeOnFail: 50.

Job-level dedup via deduplication: { id } evita reprocessar o mesmo evento se o webhook chegar duas vezes (cinto + suspensórios com a idempotência HTTP).


Pipeline de Ordem

[iFood webhook] → verify HMAC → enqueue 'ifood' job (com receivedAt: ms)


                  MarketplaceOrderProcessor.ingest()
                  ├─ normalize (provider-specific)
                  ├─ findByExternalId — skip se duplicate
                  ├─ ordersService.createFromMarketplace()
                  ├─ kitchenGateway.emit('newOrder')
                  └─ ifoodSla.record('ack', latencyMs) [iFood only]

receivedAt é stamped no momento do webhook e usado pelo processor para calcular ack-latency (Sprint 2 SLA).


MarketplaceSyncService.enqueueSyncItem(tenantId, itemId) é chamado reativamente pelo MenuService em update — propaga para os 3 providers em paralelo.

enqueueFullSync(tenantId, provider?) força uma re-sincronização completa: enumera todos os menu items ativos e enfileira um job por (item × provider). Útil quando um marketplace fica fora do ar e seu catálogo divergiu do nosso.

Endpoints:

  • POST /t/:slug/marketplace/integrations/sync — full sync, todos os providers
  • POST /t/:slug/marketplace/integrations/:id/sync — single provider

Botão "Sincronizar" por integração na MarketplaceView dispara a versão single.


Status Push (Order → Marketplace)

pushOrderStatus(tenantId, externalOrderId, internalStatus, source) mapeia status interno para status do provider:

InternoiFoodRappiUberEats
confirmedCONFIRMEDTAKENaccept_pos_order
preparingIN_PREPARATIONCOOKING(não suportado)
readyREADY_TO_PICKUPREADY_TO_PICK(não suportado)
deliveredCONCLUDEDDELIVERED(não suportado)
cancelledCANCELLEDREJECTEDcancel

Para iFood, latência da chamada é capturada em IFoodSlaService como status_update ou cancellation (ver ifood-health.md).


Schema MarketplaceIntegration

CampoTipoDescrição
tenantObjectIdTenant
providerenumifood | rappi | ubereats
activebooleanHabilitada para uso
credentialsobject{ clientId, clientSecret, merchantId } (criptografado)
tokensobject{ accessToken, expiresAt } (cache OAuth)
lastSyncAtDateÚltima sync de catálogo bem-sucedida
syncedItemCountnumberItems propagados na última sync

OAuth tokens são criptografados em repouso via token-encrypt.ts (AES-256-GCM).

Lançado sob a licença MIT.