Files
marketplaces/docs/backend/PHASE-2-ORDERS-NOTIFICATIONS-CONTRACT.md
sdarbinyan c91b75c036 docs: backend contracts for Phases 2-4 (orders/notifications, catalog/offer, connectors)
Continues the Phase 1 contract doc with the same wire-contract-only style.
All three build on Sprint 0.1's answered decisions - no further business
input needed to start implementation once backend ownership is confirmed:

- Phase 2: canonical Order/OrderLine/Fulfillment/OrderEvent per the unified
  multi-seller decision (one Order, per-seller Fulfillment groups), event
  bus, Notification Center contract.
- Phase 3: Product/Offer split, InventoryRecord, publish-time executability
  validation (the mechanism behind "no branch may distinguish an inspector
  from a normal buyer").
- Phase 4: generic config-driven connector framework per the "no fixed
  marketplace list" decision - onboarding a new partner is configuration
  against a fixed pipeline, not a bespoke integration.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 23:02:52 +04:00

6.3 KiB
Raw Blame History

Phase 2 Backend Contract — Canonical Orders, Event Bus, Notification Center

Companion to PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md Phase 2 (Sprints 2.12.2). Depends on Phase 1 (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 for the seller-scoped view).

3. Entities

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<string, unknown>;
}

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) — no facade change needed, only binding a real AdminOrdersApiGateway.

5. Event bus

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 §5 for the messenger-side orchestration).

6. Notification Center

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.