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

153 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.12.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.