124 lines
5.6 KiB
Markdown
124 lines
5.6 KiB
Markdown
|
|
# Phase 4 Backend Contract — External Order Connector Framework
|
|||
|
|
|
|||
|
|
Companion to [PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md](../PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md) Phase 4 (Sprint 4.1, generic framework; Sprint 4.2 retired as "per named marketplace"). Depends on [Phase 3](PHASE-3-CATALOG-OFFER-FULFILLMENT-CONTRACT.md) (`Offer`/`sellerSku` must exist to map onto) and [Phase 2](PHASE-2-ORDERS-NOTIFICATIONS-CONTRACT.md) (`Order` canonical model).
|
|||
|
|
|
|||
|
|
**Status: ready to build, generic by design.** Sprint 0.1 decision (2026-08-17): no fixed marketplace list — "our new ones, partners, new, etc." This contract specifies a config-driven framework, not a per-provider integration. Zero of this exists in the codebase today (`reconcil*`, `idempot*`, `hostinger` all return 0 hits).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. Design principle
|
|||
|
|
|
|||
|
|
**A new partner connector is an onboarding action against this framework, not a code change.** Auth type, field mapping, and rate limits are configuration; the pipeline (ingest → normalize → map → idempotency-check → create/update order → notify) is fixed and shared across every connector.
|
|||
|
|
|
|||
|
|
## 2. Entities
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
interface Connector {
|
|||
|
|
id: string;
|
|||
|
|
marketplaceId: string;
|
|||
|
|
provider: string; // free-text label, e.g. "ozon", "wildberries" - not an enum, new values need no code change
|
|||
|
|
authType: 'webhook_signed' | 'api_key' | 'oauth2';
|
|||
|
|
credentialRef: string; // pointer into secret storage, never the secret itself
|
|||
|
|
pollingIntervalSeconds?: number; // set only when the provider has no webhook
|
|||
|
|
cursorState?: string; // opaque, connector-specific pagination/since cursor
|
|||
|
|
status: 'active' | 'paused' | 'error';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface RawExternalEvent {
|
|||
|
|
id: string;
|
|||
|
|
connectorId: string;
|
|||
|
|
payload: unknown; // stored verbatim, before any parsing - the traceability anchor
|
|||
|
|
receivedAt: string;
|
|||
|
|
processedAt?: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface ExternalOrderMapping {
|
|||
|
|
connectorId: string;
|
|||
|
|
externalSellerId: string;
|
|||
|
|
externalProductId: string;
|
|||
|
|
externalSku: string;
|
|||
|
|
internalSellerId: string;
|
|||
|
|
internalOfferId: string; // references Phase 3's Offer
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface DeadLetter {
|
|||
|
|
id: string;
|
|||
|
|
connectorId: string;
|
|||
|
|
rawEventId: string;
|
|||
|
|
reason: string;
|
|||
|
|
retryCount: number;
|
|||
|
|
lastAttemptAt: string;
|
|||
|
|
resolvedAt?: string;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 3. Pipeline (fixed, shared across every connector)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
1. Connector receives webhook, or polling finds a new event via cursorState.
|
|||
|
|
2. Signature/auth verified. Idempotency key = connectorId + externalOrderId/eventId.
|
|||
|
|
3. Payload persisted as RawExternalEvent BEFORE any parsing.
|
|||
|
|
4. Normalizer maps payload -> canonical ExternalOrderEvent shape (fixed schema, see §4).
|
|||
|
|
5. SKU mapping resolves externalSku -> internal Offer via ExternalOrderMapping.
|
|||
|
|
No mapping found -> event goes to the Unmatched queue (§5), does NOT fail silently.
|
|||
|
|
6. Order created/updated via the Phase 2 Order API, source: 'external', externalOrderRef set.
|
|||
|
|
7. external_order.imported and order.created events emitted (Phase 2 event bus).
|
|||
|
|
8. Fulfillment/status changes pushed back to the external marketplace if its API supports it.
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 4. Canonical external order event (what the normalizer produces)
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
interface ExternalOrderEvent {
|
|||
|
|
connectorId: string;
|
|||
|
|
externalOrderId: string;
|
|||
|
|
externalCreatedAt: string;
|
|||
|
|
customer: { name?: string; contact?: string };
|
|||
|
|
lines: Array<{ externalSku: string; qty: number; unitPriceMinor: number; currency: string }>;
|
|||
|
|
totalMinor: number;
|
|||
|
|
currency: string;
|
|||
|
|
rawEventId: string; // traceability back to §2
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Every provider's adapter is responsible only for producing this shape from its own payload — everything downstream (§3 steps 5–8) is provider-agnostic.
|
|||
|
|
|
|||
|
|
## 5. Unmatched queue + retry
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
GET /api/admin/v2/integrations/{connectorId}/unmatched
|
|||
|
|
POST /api/admin/v2/integrations/{connectorId}/unmatched/{eventId}/resolve { internalOfferId }
|
|||
|
|
POST /api/admin/v2/integrations/{connectorId}/dead-letter/{id}/replay
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Retry policy: exponential backoff, capped attempts, then `DeadLetter` with manual replay from backoffice. No connector is allowed to silently drop an event.
|
|||
|
|
|
|||
|
|
## 6. Connector-agnostic SLA (applies to every provider, per plan §5.2)
|
|||
|
|
|
|||
|
|
- Webhook source: 99% of valid events processed in under 60 seconds.
|
|||
|
|
- Polling source: delay no worse than `pollingIntervalSeconds + 60`.
|
|||
|
|
- **Zero** duplicate orders on repeated event delivery (guaranteed by the idempotency key in §3 step 2).
|
|||
|
|
- Every connector error carries a trace id, visible in backoffice.
|
|||
|
|
|
|||
|
|
## 7. Endpoints
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
POST /api/providers/v1/{connector}/webhook -- generic entrypoint, connector resolved by path + auth
|
|||
|
|
GET /api/admin/v2/integrations -- list all connectors + health (last success, lag, errors, backlog)
|
|||
|
|
POST /api/admin/v2/integrations -- onboard a new connector: { provider, authType, credentialRef, marketplaceId }
|
|||
|
|
PATCH /api/admin/v2/integrations/{id} -- pause/resume, update mapping config
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 8. Onboarding a new partner (replaces the old "one sprint per named marketplace")
|
|||
|
|
|
|||
|
|
1. Register credentials in secret storage, scoped to marketplace/seller.
|
|||
|
|
2. `POST /api/admin/v2/integrations` with the provider's auth type and mapping config.
|
|||
|
|
3. Write the provider-specific adapter (payload → §4 canonical shape) — the only genuinely bespoke piece per partner.
|
|||
|
|
4. Verify in sandbox against the fixed pipeline (§3) — nothing else changes.
|
|||
|
|
|
|||
|
|
## 9. What the frontend will start doing once this ships
|
|||
|
|
|
|||
|
|
- Build the backoffice **Integrations** section (missing from admin nav today): connector list, health (last success/lag/errors/backlog/unmatched), FX sources, messaging providers.
|
|||
|
|
- Trace-id surfacing on connector errors.
|
|||
|
|
- Unmatched-queue resolution UI.
|