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>
6.3 KiB
Phase 2 Backend Contract — Canonical Orders, Event Bus, Notification Center
Companion to PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md Phase 2 (Sprints 2.1–2.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
AdminOrderWatcherServicefrom pollingAdminOrdersLocalGatewayto 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.