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
| Concern | Status |
|---|---|
| PedidosYa adapter | Implemented and registered |
| Uber Eats | Enum value only (uber_eats); not registered |
| Authenticated merchant API | /delivery-marketplace/* |
| PedidosYa webhooks | Public /marketplace/pedidosya/* |
| Background queues | marketplace-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_eatsMarketplaceConnectionStatus:active,disconnected,error,pausedMarketplaceOrderStatus:received,accepted,rejected,cancelled,completed,error,pending_session
Core workflows
Connect a location
- Merchant calls
POST /delivery-marketplace/connections?businessId=.... ConnectMarketplaceUseCaserejects an existing non-disconnected connection for the same location+provider with409.- Registry resolves the adapter and calls
testConnection. - Credentials are encrypted via
CryptoServicebefore persistence. - 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.
PedidosYaIpAllowlistGuardchecksPEDIDOSYA_WEBHOOK_IP_ALLOWLIST(empty allowlist = allow all, intended for local/dev).- Payload is validated with PedidosYa Zod schemas and mapped to a domain-friendly inbound order.
IngestMarketplaceOrderUseCase:- resolves connection by
externalStoreId+ provider - optionally validates
webhookSecretwhen 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"andorderSource: "marketplace" - adds mapped items, sends to kitchen
- enqueues FEL issuance (
marketplace-fel-retry) - when
autoAcceptis true, enqueues outboundacceptonmarketplace-outbound-order-action
- resolves connection by
Status updates arrive on
POST /marketplace/pedidosya/webhook/order-status-update.
Accept / deny from POS
Authenticated routes:
POST /delivery-marketplace/ingested-orders/:id/acceptPOST /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.
Menu push and 86
POST /delivery-marketplace/connections/:id/menu/pushacquires a Redis lock (marketplace:menu-push-lock:{connectionId}, TTL 180s) and enqueuesmarketplace-push-menu. Concurrent pushes withoutforcereturn409.POST /delivery-marketplace/connections/:id/item-availabilitylooks up the menu mapping by FlowPOS product ID, then callsadapter.setItemAvailabilitywith 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
| Method | Path | Purpose |
|---|---|---|
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)
| Method | Path | Purpose |
|---|---|---|
POST | /webhook/order-dispatch | New order |
POST | /webhook/order-status-update | Provider status / cancel |
GET | /health | Liveness for plugin health checks |
Queues and side effects
| Queue | Default attempts | Role |
|---|---|---|
marketplace-push-menu | 3, exponential backoff | Full menu sync worker |
marketplace-outbound-order-action | 5, exponential backoff | Accept/deny/cancel/status/availability outbound calls |
marketplace-fel-retry | 3, fixed 30s backoff | FEL issuance after successful ingest |
Redis also caches PedidosYa auth tokens (MARKETPLACE_REDIS) and holds the
menu-push lock.
Constraints and pitfalls
- Open cash register required — without an open session at the connection
location, ingest stops at
pending_sessionand does not create a restaurant order. - Menu mappings required — missing item/modifier mappings mark the ingested
order
errorand skip POS creation. - Dedup is external-order scoped — duplicate webhooks for the same
externalOrderIdon a connection are ignored. webhookSecretvs PedidosYa controller — ingest validatesconnection.webhookSecretwhen set, but the PedidosYa webhook controller does not currently forward a token intoIngestMarketplaceOrderUseCase. Prefer IP allowlisting for PedidosYa, and leavewebhookSecretunset unless a token forwarding path is added.- Auto-accept delay — when
autoAcceptis true, ingest enqueues accept withdelay: defaultPrepTimeMinutes * 60 * 1000. PedidosYa acceptance windows are short; verify this delay against provider SLA before enabling auto-accept in production. - Spec path drift — clients must use
/delivery-marketplace, not the older/marketplaceplanning base path (except PedidosYa public webhooks, which intentionally use/marketplace/pedidosya).
Related codepaths
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