# Phase 2 Backend Contract — Canonical Orders, Event Bus, Notification Center Companion to [PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md](../PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md) Phase 2 (Sprints 2.1–2.2). Depends on [Phase 1](PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md) (Money/PriceSnapshot/PaymentIntent) being implemented first — an Order line references a `priceSnapshotId` from that contract. **Status: ready to build.** No open decisions block this phase. --- ## 1. Why this exists Today `AdminOrdersLocalGateway` is a static 24-row in-memory seed with no create path — a real order can never appear. `AdminOrderWatcherService` already polls for new orders to toast/badge the admin, but is functionally inert against the mock. This contract makes both real. ## 2. Multi-seller model — Sprint 0.1 decision: unified **One `Order` per checkout, regardless of how many sellers are represented.** Lines are grouped into per-seller `Fulfillment` entries internally. There is no parent/child order splitting, no separate order-per-seller. A seller only ever sees their own `Fulfillment` group within a shared order (see [Phase 5 contract](PHASE-5-SELLER-PORTAL-CONTRACT.md) for the seller-scoped view). ## 3. Entities ```ts interface Order { id: string; marketplaceId: string; source: 'storefront' | 'external' | 'backoffice' | 'api_partner'; externalOrderRef?: string; // set when source === 'external', see Phase 4 customerId?: string; currency: string; subtotal: Money; discount: Money; delivery: Money; total: Money; paymentStatus: 'pending_payment' | 'paid' | 'failed' | 'refunded' | 'partially_refunded'; orderStatus: 'pending_payment' | 'paid' | 'processing' | 'fulfilled' | 'completed' | 'cancelled'; createdAt: string; paidAt?: string; } interface OrderLine { id: string; orderId: string; offerId: string; // see Phase 3 contract sellerId: string; skuSnapshot: string; titleSnapshot: string; qty: number; unitPrice: Money; lineTotal: Money; priceSnapshotId: string; // references Phase 1's PriceSnapshot } interface Fulfillment { id: string; orderId: string; sellerId: string; // the seller-scoping unit for the unified-order model type: 'manual' | 'warehouse' | 'pickup' | 'digital'; status: 'pending' | 'assigned' | 'in_progress' | 'issued' | 'shipped' | 'cancelled'; assignedTo?: string; issuedAt?: string; shippedAt?: string; evidence?: { type: string; url: string }[]; // e.g. shipment proof, digital delivery receipt } interface OrderEvent { id: string; orderId: string; type: 'created' | 'paid' | 'seller_notified' | 'accepted' | 'fulfilled' | 'cancelled' | 'refunded'; actor?: string; // user/system id, null for automated system events occurredAt: string; metadata?: Record; } interface OrderContactSnapshot { orderId: string; name: string; email?: string; phone?: string; preferredChannel?: 'telegram' | 'vk' | 'max' | 'email' | 'sms'; capturedAt: string; // immutable after order creation, independent of later Customer profile edits } ``` ## 4. Endpoints ``` GET /api/admin/v2/orders?marketplaceId=&status=&source=&page=&pageSize= GET /api/admin/v2/orders/{id} PATCH /api/admin/v2/orders/{id}/status { status } POST /api/admin/v2/orders/{id}/refund-request { reason } POST /api/admin/v2/orders/{id}/notes { note, internal: boolean } POST /api/admin/v2/orders/{id}/archive POST /api/admin/v2/orders/{id}/restore DELETE /api/admin/v2/orders/{id} GET /api/seller/v1/orders?fulfillmentStatus=&page=&pageSize= -> returns Order + only the Fulfillment groups belonging to the authenticated seller, OrderLines filtered to that seller's lines. Never the full order's other-seller lines. ``` Replaces `AdminOrdersLocalGateway` behind the `ADMIN_ORDERS_GATEWAY` token already wired this session (see [BACKEND-API-REFERENCE.md §8](../../BACKEND-API-REFERENCE.md)) — no facade change needed, only binding a real `AdminOrdersApiGateway`. ## 5. Event bus ```ts type PlatformEvent = | { type: 'order.created'; orderId: string; marketplaceId: string } | { type: 'order.paid'; orderId: string; marketplaceId: string } | { type: 'payment.failed'; orderId: string; reason: string } | { type: 'webhook.error'; source: string; traceId: string } | { type: 'stock.low'; offerId: string; available: number } | { type: 'oversell'; offerId: string; requested: number; available: number } | { type: 'refund.requested'; orderId: string; refundId: string } | { type: 'refund.completed'; orderId: string; refundId: string } | { type: 'external_order.imported'; orderId: string; connectorId: string }; ``` Backend owns the bus implementation (queue, pub/sub, whatever fits existing infra). Frontend's only contract: the Notification entity below, and the requirement that `order.paid` always produces a backoffice notification **even if every external channel is down** (see [Phase 8](PHASE-8-IDENTITY-MESSAGING-CONTRACT.md) §5 for the messenger-side orchestration). ## 6. Notification Center ```ts interface Notification { id: string; marketplaceId: string; entityType: 'order' | 'payment' | 'offer' | 'connector' | 'refund'; entityId: string; severity: 'info' | 'warning' | 'critical'; eventType: PlatformEvent['type']; read: boolean; deepLink: string; // e.g. /admin/orders/{id} createdAt: string; } interface DeliveryAttempt { notificationId: string; channel: 'telegram' | 'email' | 'sms' | 'vk' | 'max'; status: 'sent' | 'failed'; error?: string; attemptedAt: string; } ``` ``` GET /api/admin/v2/notifications?marketplaceId=&unreadOnly=&eventType= PATCH /api/admin/v2/notifications/{id}/read ``` Invariant: a `DeliveryAttempt` failure on an external channel **never** prevents the `Notification` row itself from being created and visible in the backoffice unread queue. ## 7. What the frontend will start doing once this ships - Repoint `AdminOrderWatcherService` from polling `AdminOrdersLocalGateway` to the event stream / `GET /api/admin/v2/notifications?unreadOnly=true`. - Build the backoffice **Notifications** section (unread queue, severity, marketplace/event-type filter) — currently missing from admin nav entirely. - Wire admin order actions (assign, resend notification, replay sync, cancel/refund, comment, export) to the endpoints in §4.