Saltar al contenido principal

Delivery Marketplace Architecture

Source-backed reference for apps/backend/src/delivery-marketplace/.

For incident triage, see Delivery Marketplace Troubleshooting. For the provider port contract, see Delivery Marketplace Provider Port.


Current runtime status

ConcernStatus
PedidosYa adapterImplemented and registered
Uber EatsEnum value only (uber_eats); not registered
Authenticated merchant API/delivery-marketplace/*
PedidosYa webhooksPublic /marketplace/pedidosya/*
Background queuesmarketplace-push-menu, marketplace-outbound-order-action, marketplace-fel-retry

The early feature contract under specs/041-delivery-marketplace/contracts/rest-api.md used base path /marketplace. The shipped Nest controller is @Controller("delivery-marketplace"). Prefer the live controller over the planning contract when writing clients or runbooks.


Module layout

apps/backend/src/delivery-marketplace/
├── domain/ # Port, repository ports, errors, events, menu-mapping service
├── application/
│ ├── use-cases/ # Connect, ingest, accept/deny, menu push, availability
│ ├── marketplace-connection.service.ts
│ ├── marketplace-provider-registry.service.ts
│ └── marketplace-order-lifecycle.listener.ts
├── infrastructure/
│ ├── pedidosya/ # Adapter, auth, HTTP client, Zod schemas, mappers
│ ├── processors/ # BullMQ workers (menu push, outbound actions, FEL retry)
│ └── *.repository.ts # Kysely adapters
└── interfaces/
├── delivery-marketplace.controller.ts
├── webhooks/pedidosya-plugin.controller.ts
├── guards/pedidosya-ip-allowlist.guard.ts
└── dtos/

Shared enums live in packages/global/enums/marketplace.enums.ts:

  • MarketplaceProvider: pedidosya, uber_eats
  • MarketplaceConnectionStatus: active, disconnected, error, paused
  • MarketplaceOrderStatus: received, accepted, rejected, cancelled, completed, error, pending_session

Core workflows

Connect a location

  1. Merchant calls POST /delivery-marketplace/connections?businessId=....
  2. ConnectMarketplaceUseCase rejects an existing non-disconnected connection for the same location+provider with 409.
  3. Registry resolves the adapter and calls testConnection.
  4. Credentials are encrypted via CryptoService before persistence.
  5. A disconnected row for the same pair is deleted so reconnect can succeed.

Credentials never leave the encrypted column in API responses.

Ingest an inbound PedidosYa order

Public webhook: POST /marketplace/pedidosya/webhook/order-dispatch.

  1. PedidosYaIpAllowlistGuard checks PEDIDOSYA_WEBHOOK_IP_ALLOWLIST (empty allowlist = allow all, intended for local/dev).
  2. Payload is validated with PedidosYa Zod schemas and mapped to a domain-friendly inbound order.
  3. IngestMarketplaceOrderUseCase:
    • resolves connection by externalStoreId + provider
    • optionally validates webhookSecret when present on the connection
    • dedups with insertIfNew
    • fails mapping gaps to ingested status error
    • requires an open cash register session (otherwise pending_session)
    • creates a restaurant order with orderType: "delivery_third_party" and orderSource: "marketplace"
    • adds mapped items, sends to kitchen
    • enqueues FEL issuance (marketplace-fel-retry)
    • when autoAccept is true, enqueues outbound accept on marketplace-outbound-order-action

Status updates arrive on POST /marketplace/pedidosya/webhook/order-status-update.

Accept / deny from POS

Authenticated routes:

  • POST /delivery-marketplace/ingested-orders/:id/accept
  • POST /delivery-marketplace/ingested-orders/:id/deny

Accept is allowed only from received or pending_session. The use case updates local status and enqueues an outbound provider call (manual accept job id accept:manual:{ingestedOrderId}).

Kitchen status fan-out

MarketplaceOrderLifecycleListener listens to restaurant realtime events (OnRestaurantOrderUpdatedEvent, OnRestaurantOrderReadyEvent). When the internal order maps to an ingested marketplace order, it enqueues provider status updates through UpdateMarketplaceOrderStatusUseCase.

  • POST /delivery-marketplace/connections/:id/menu/push acquires a Redis lock (marketplace:menu-push-lock:{connectionId}, TTL 180s) and enqueues marketplace-push-menu. Concurrent pushes without force return 409.
  • POST /delivery-marketplace/connections/:id/item-availability looks up the menu mapping by FlowPOS product ID, then calls adapter.setItemAvailability with the provider external item ID.

DTO pitfall: SetItemAvailabilityDto.externalItemId is the FlowPOS product ID passed into Set86ItemUseCase as flowposProductId. Callers must send the FlowPOS product UUID, not the PedidosYa item id.


Authenticated API surface

Base path: /delivery-marketplace
Auth: global Firebase AuthGuard + RolesGuard
Permission resource: PolicyResource.DeliveryMarketplace

MethodPathPurpose
GET/connections?businessId=List connections
GET/connections/:id?businessId=Get connection
POST/connections?businessId=Connect provider
PATCH/connections/:id?businessId=Update settings
DELETE/connections/:id?businessId=Disconnect
POST/connections/:id/test?businessId=Connectivity check
GET/connections/:id/orders?businessId=List ingested orders
POST/ingested-orders/:id/accept?businessId=Accept order
POST/ingested-orders/:id/deny?businessId=Deny order
POST/connections/:id/menu/push?businessId=Enqueue menu push
POST/connections/:id/store-status?businessId=Open/close store
GET/connections/:id/store-status?businessId=Read store status
POST/connections/:id/item-availability?businessId=86 / restore item
GET/connections/:id/menu-mappings?businessId=List mappings
GET/connections/:id/sync-log?businessId=Sync activity
GET/connections/:id/provider-orders?businessId=Provider reconciliation list
GET/connections/:id/provider-orders/:orderId?businessId=Provider single order

Public PedidosYa endpoints

Base path: /marketplace/pedidosya (@IsPublic(), IP allowlist guard)

MethodPathPurpose
POST/webhook/order-dispatchNew order
POST/webhook/order-status-updateProvider status / cancel
GET/healthLiveness for plugin health checks

Queues and side effects

QueueDefault attemptsRole
marketplace-push-menu3, exponential backoffFull menu sync worker
marketplace-outbound-order-action5, exponential backoffAccept/deny/cancel/status/availability outbound calls
marketplace-fel-retry3, fixed 30s backoffFEL issuance after successful ingest

Redis also caches PedidosYa auth tokens (MARKETPLACE_REDIS) and holds the menu-push lock.


Constraints and pitfalls

  1. Open cash register required — without an open session at the connection location, ingest stops at pending_session and does not create a restaurant order.
  2. Menu mappings required — missing item/modifier mappings mark the ingested order error and skip POS creation.
  3. Dedup is external-order scoped — duplicate webhooks for the same externalOrderId on a connection are ignored.
  4. webhookSecret vs PedidosYa controller — ingest validates connection.webhookSecret when set, but the PedidosYa webhook controller does not currently forward a token into IngestMarketplaceOrderUseCase. Prefer IP allowlisting for PedidosYa, and leave webhookSecret unset unless a token forwarding path is added.
  5. Auto-accept delay — when autoAccept is true, ingest enqueues accept with delay: defaultPrepTimeMinutes * 60 * 1000. PedidosYa acceptance windows are short; verify this delay against provider SLA before enabling auto-accept in production.
  6. Spec path drift — clients must use /delivery-marketplace, not the older /marketplace planning base path (except PedidosYa public webhooks, which intentionally use /marketplace/pedidosya).

  • apps/backend/src/delivery-marketplace/
  • packages/global/enums/marketplace.enums.ts
  • Restaurant order creation: apps/backend/src/restaurant/application/orders.service.ts
  • Cash register sessions: apps/backend/src/cash-register/
  • FEL retry processor: infrastructure/processors/marketplace-fel-retry.processor.ts