# 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<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](../../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.
