Compare commits
4 Commits
8d9eb97e9e
...
2e09369345
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e09369345 | ||
|
|
ec6760ac65 | ||
|
|
707db6d43c | ||
|
|
c91b75c036 |
@@ -2,6 +2,8 @@
|
||||
|
||||
One document, everyone reads it: product, backend, frontend, QA. It answers three questions for every domain — **what does the frontend already call**, **what shape does it send/expect**, and **is it real or mocked today**. Generated from the actual Angular frontend source (this repo has no backend code — it is a pure client consuming an external API), cross-checked against the frontend's own tolerant adapters, not aspirational.
|
||||
|
||||
**For what doesn't exist yet:** this doc describes the live surface only. The full set of forward-looking wire contracts for Product Plan v3.1 (money/FX, orders, catalog/offer split, connectors, seller portal, identity, tenant registry, RBAC, analytics — 10 phases + 2 tracks) lives in [docs/backend/](docs/backend/README.md).
|
||||
|
||||
Maturity tags used throughout:
|
||||
|
||||
| Tag | Meaning |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Product Plan v3.1 — Delivery Plan (Phases → Sprints → Todos)
|
||||
|
||||
Companion to [PRODUCT-PLAN-v3.1-GAP-ANALYSIS.md](PRODUCT-PLAN-v3.1-GAP-ANALYSIS.md). Every gap identified there is assigned here exactly once.
|
||||
Companion to [PRODUCT-PLAN-v3.1-GAP-ANALYSIS.md](PRODUCT-PLAN-v3.1-GAP-ANALYSIS.md). Every gap identified there is assigned here exactly once. Wire contracts for every `[BE]`/`[BOTH]` phase and track below are written up in [docs/backend/](backend/README.md) — hand that directory to whoever builds the backend.
|
||||
|
||||
**No calendar dates.** The plan itself (§12) refuses invented dates and fixes *sequence + exit criteria* instead. This document does the same. Sprints are ordered units of work, not two-week promises. Sizes are relative: **S** / **M** / **L** / **XL**.
|
||||
|
||||
|
||||
118
docs/backend/PHASE-10-CONTENT-MODULES-CONTRACT.md
Normal file
118
docs/backend/PHASE-10-CONTENT-MODULES-CONTRACT.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# Phase 10 Backend Contract — Tenant Content Modules (Gorbushka-class tenants)
|
||||
|
||||
Companion to [PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md](../PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md) Phase 10 (Sprints 10.1–10.2). Covers plan §11.
|
||||
|
||||
**Status: ready to build, lowest priority.** Only after Commerce Core (Phases 1–7) is real — the plan is explicit that this tenant type does not define the platform architecture; it is one configuration of the shared runtime, not a separate build.
|
||||
|
||||
---
|
||||
|
||||
## 1. Entities
|
||||
|
||||
```ts
|
||||
interface Shop {
|
||||
id: string;
|
||||
marketplaceId: string;
|
||||
shopCategoryId: string;
|
||||
name: string;
|
||||
floorId?: string;
|
||||
status: 'draft' | 'published';
|
||||
}
|
||||
|
||||
interface ShopCategory {
|
||||
id: string;
|
||||
marketplaceId: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface Service {
|
||||
id: string;
|
||||
marketplaceId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
status: 'draft' | 'published';
|
||||
}
|
||||
|
||||
interface Floor {
|
||||
id: string;
|
||||
marketplaceId: string;
|
||||
order: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface SchemePin {
|
||||
id: string;
|
||||
marketplaceId: string;
|
||||
floorId: string;
|
||||
shopId?: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface RentListing {
|
||||
id: string;
|
||||
marketplaceId: string;
|
||||
title: string;
|
||||
areaSqm: number;
|
||||
floorId?: string;
|
||||
status: 'available' | 'leased';
|
||||
}
|
||||
|
||||
interface Lead {
|
||||
id: string;
|
||||
marketplaceId: string;
|
||||
rentListingId?: string;
|
||||
contactName: string;
|
||||
contactPhone: string;
|
||||
message?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface NewsPromo {
|
||||
id: string;
|
||||
marketplaceId: string;
|
||||
title: string;
|
||||
body: string;
|
||||
publishedAt?: string;
|
||||
}
|
||||
|
||||
interface MallSettings {
|
||||
marketplaceId: string;
|
||||
openingHours: Record<string, string>;
|
||||
contactInfo: Record<string, string>;
|
||||
}
|
||||
```
|
||||
|
||||
Every entity above carries `marketplaceId`, an audit trail, and the same draft/preview/publish flow as [Phase 9's revision model](PHASE-9-TENANT-REGISTRY-DOMAINS-CONTRACT.md) §5 — not a separate content pipeline.
|
||||
|
||||
## 2. Endpoints
|
||||
|
||||
```
|
||||
GET/POST/PATCH/DELETE /api/admin/v2/content/shops
|
||||
GET/POST/PATCH/DELETE /api/admin/v2/content/shop-categories
|
||||
GET/POST/PATCH/DELETE /api/admin/v2/content/services
|
||||
GET/POST/PATCH/DELETE /api/admin/v2/content/floors
|
||||
GET/POST/PATCH/DELETE /api/admin/v2/content/scheme-pins
|
||||
GET/POST/PATCH/DELETE /api/admin/v2/content/rent-listings
|
||||
POST /api/admin/v2/content/rent-listings/{id}/leads
|
||||
GET/POST/PATCH/DELETE /api/admin/v2/content/news
|
||||
PATCH /api/admin/v2/content/mall-settings
|
||||
```
|
||||
|
||||
## 3. Tenant feature configuration (Gorbushka's v1 default, per plan §11.1)
|
||||
|
||||
```json
|
||||
{
|
||||
"cms": true, "shops": true, "services": true, "mallScheme": true,
|
||||
"rentListings": true, "news": true, "seoMedia": true,
|
||||
"catalog": false, "sellerPortal": false,
|
||||
"cart": false, "checkout": false, "payments": false, "orders": false
|
||||
}
|
||||
```
|
||||
|
||||
Commerce modules are **platform-ready but off** — the point of Phase 10 is proving this tenant can flip `catalog`/`cart`/`checkout`/etc. to `true` later via [Phase 9's `MarketplaceFeatureSet`](PHASE-9-TENANT-REGISTRY-DOMAINS-CONTRACT.md) with zero backend or storefront code changes, since the commerce core is already generic by the time Phase 10 starts.
|
||||
|
||||
## 4. What the frontend will start doing once this ships
|
||||
|
||||
- Mall scheme / floor / pin editor UI.
|
||||
- Rent listing + lead capture forms.
|
||||
- Confirm the existing Gorbushka frontend/archive is used as UX reference only — production data and auth route through the shared platform per ADR-0001.
|
||||
152
docs/backend/PHASE-2-ORDERS-NOTIFICATIONS-CONTRACT.md
Normal file
152
docs/backend/PHASE-2-ORDERS-NOTIFICATIONS-CONTRACT.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# 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.
|
||||
144
docs/backend/PHASE-3-CATALOG-OFFER-FULFILLMENT-CONTRACT.md
Normal file
144
docs/backend/PHASE-3-CATALOG-OFFER-FULFILLMENT-CONTRACT.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# Phase 3 Backend Contract — Product/Offer Split, Inventory, Executability
|
||||
|
||||
Companion to [PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md](../PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md) Phase 3 (Sprints 3.1–3.3). The largest structural change in the programme — nothing about multi-seller commerce works without it.
|
||||
|
||||
**Status: ready to build.** No open decisions block this phase.
|
||||
|
||||
---
|
||||
|
||||
## 1. Why this exists
|
||||
|
||||
Today price, stock and currency hang directly off a single admin `Product` mock domain, unrelated to the live storefront `Item` domain. A product cannot have two sellers, two prices, or two stock levels. `Offer/Listing` does not exist in any form.
|
||||
|
||||
## 2. The two-layer split
|
||||
|
||||
`Product` describes the item itself (content). `Offer` describes one seller's commercial proposition against that product (price, stock, currency, status). One product, many offers.
|
||||
|
||||
```ts
|
||||
interface Product {
|
||||
id: string;
|
||||
marketplaceId: string;
|
||||
categoryId: string;
|
||||
brand?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
attributes: Record<string, unknown>;
|
||||
media: string[];
|
||||
status: 'draft' | 'moderation' | 'published' | 'paused' | 'archived';
|
||||
}
|
||||
|
||||
interface Variant {
|
||||
id: string;
|
||||
productId: string;
|
||||
sku: string;
|
||||
barcode?: string;
|
||||
optionValues: Record<string, string>; // e.g. { color: 'red', size: 'M' }
|
||||
dimensions?: { weight?: number; length?: number; width?: number; height?: number };
|
||||
}
|
||||
|
||||
interface Category {
|
||||
id: string;
|
||||
marketplaceId: string;
|
||||
parentId: string | null;
|
||||
slug: string;
|
||||
attributesSchema: Record<string, unknown>;
|
||||
order: number;
|
||||
seo: { title?: string; description?: string };
|
||||
}
|
||||
|
||||
interface Offer {
|
||||
id: string;
|
||||
marketplaceId: string;
|
||||
sellerId: string;
|
||||
variantId: string;
|
||||
sellerSku: string;
|
||||
price: Money; // Money type from Phase 1 contract
|
||||
stockPolicy: 'track' | 'no_track' | 'preorder';
|
||||
status: 'draft' | 'moderation' | 'published' | 'paused' | 'archived';
|
||||
publishedAt?: string;
|
||||
executabilityChecked: boolean; // see §5
|
||||
}
|
||||
|
||||
interface PriceHistory {
|
||||
offerId: string;
|
||||
price: Money;
|
||||
changedBy: string; // user id or 'sync:{connectorId}'
|
||||
changedAt: string;
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Inventory
|
||||
|
||||
```ts
|
||||
interface InventoryRecord {
|
||||
offerId: string;
|
||||
available: number;
|
||||
reserved: number;
|
||||
sold: number;
|
||||
warehouse?: string;
|
||||
source: 'manual' | 'feed_sync' | 'connector';
|
||||
}
|
||||
|
||||
interface StockReservation {
|
||||
id: string;
|
||||
offerId: string;
|
||||
qty: number;
|
||||
reason: 'checkout' | 'pre_payment';
|
||||
expiresAt: string; // TTL
|
||||
released: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
Invariants:
|
||||
- `available`, `reserved`, `sold` are counted separately, never derived from one another implicitly.
|
||||
- Reservations are created at checkout or pre-payment (tenant-configurable strategy) and expire by TTL, releasing `reserved` back to `available`.
|
||||
- Seller feed stock updates are an **idempotent upsert** — a repeated webhook must not double-decrement.
|
||||
- Oversell (a sale exceeding `available`) routes to a dedicated incident queue, never silently hidden or auto-corrected.
|
||||
|
||||
## 4. Lifecycle
|
||||
|
||||
```
|
||||
draft -> moderation -> published -> paused/archived
|
||||
```
|
||||
|
||||
Applies independently to both `Product` and `Offer`. Wires to the already-existing (mock) Admin Moderation module — no new frontend module needed, just a real gateway behind `ADMIN_MODERATION_GATEWAY` (token already added this session).
|
||||
|
||||
## 5. Publish-time executability
|
||||
|
||||
**An offer that cannot actually be fulfilled must not be publishable.** Before allowing `status: 'published'`, the backend validates:
|
||||
- The offer has a valid `Fulfillment` type it can realistically satisfy (see [Phase 2 contract](PHASE-2-ORDERS-NOTIFICATIONS-CONTRACT.md) `Fulfillment.type`).
|
||||
- Stock policy is `track` with `available > 0`, or `no_track`/`preorder` explicitly.
|
||||
- Required attributes for the offer's category (`Category.attributesSchema`) are present.
|
||||
|
||||
This is the mechanism behind the plan's §3.6/§10.2 requirement: **no branch anywhere may distinguish a normal buyer from an inspector.** The only way to guarantee that is to make every published offer genuinely executable at publish time, not to special-case checkout behavior later.
|
||||
|
||||
## 6. Bulk import
|
||||
|
||||
```
|
||||
POST /api/admin/v2/products/bulk-import
|
||||
Content-Type: multipart/form-data (CSV) or application/json (array)
|
||||
```
|
||||
|
||||
Response returns a **preview** of validation errors before anything is applied — required-field validation, category-attribute validation, duplicate-SKU detection — with a separate `POST .../bulk-import/{importId}/apply` to commit after review.
|
||||
|
||||
## 7. Endpoints
|
||||
|
||||
```
|
||||
GET /api/admin/v2/products?marketplaceId=&status=&search=&page=&pageSize=
|
||||
GET /api/admin/v2/products/{id}
|
||||
POST /api/admin/v2/products
|
||||
PATCH /api/admin/v2/products/{id}
|
||||
GET /api/admin/v2/offers?productId=&sellerId=&status=
|
||||
POST /api/admin/v2/offers
|
||||
PATCH /api/admin/v2/offers/{id}
|
||||
POST /api/admin/v2/offers/{id}/publish -> runs §5 executability check, 422 with details[] on failure
|
||||
GET /api/admin/v2/offers/lookup?sku=&sellerSku=&externalId= -- "find any offer by internal SKU, seller SKU, product ID, or external mapping" per plan §2.1
|
||||
```
|
||||
|
||||
Replaces `AdminProductsLocalGateway` behind `ADMIN_PRODUCTS_GATEWAY` (token already wired this session).
|
||||
|
||||
## 8. What the frontend will start doing once this ships
|
||||
|
||||
- Unify the admin mock product domain with the live storefront `Item` domain — currently two unrelated shapes.
|
||||
- Multi-seller product page: same product card, multiple offers/sellers/prices — undefined behaviour today.
|
||||
- Wire the Moderation module to real lifecycle transitions instead of mock data.
|
||||
123
docs/backend/PHASE-4-CONNECTOR-FRAMEWORK-CONTRACT.md
Normal file
123
docs/backend/PHASE-4-CONNECTOR-FRAMEWORK-CONTRACT.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# Phase 4 Backend Contract — External Order Connector Framework
|
||||
|
||||
Companion to [PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md](../PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md) Phase 4 (Sprint 4.1, generic framework; Sprint 4.2 retired as "per named marketplace"). Depends on [Phase 3](PHASE-3-CATALOG-OFFER-FULFILLMENT-CONTRACT.md) (`Offer`/`sellerSku` must exist to map onto) and [Phase 2](PHASE-2-ORDERS-NOTIFICATIONS-CONTRACT.md) (`Order` canonical model).
|
||||
|
||||
**Status: ready to build, generic by design.** Sprint 0.1 decision (2026-08-17): no fixed marketplace list — "our new ones, partners, new, etc." This contract specifies a config-driven framework, not a per-provider integration. Zero of this exists in the codebase today (`reconcil*`, `idempot*`, `hostinger` all return 0 hits).
|
||||
|
||||
---
|
||||
|
||||
## 1. Design principle
|
||||
|
||||
**A new partner connector is an onboarding action against this framework, not a code change.** Auth type, field mapping, and rate limits are configuration; the pipeline (ingest → normalize → map → idempotency-check → create/update order → notify) is fixed and shared across every connector.
|
||||
|
||||
## 2. Entities
|
||||
|
||||
```ts
|
||||
interface Connector {
|
||||
id: string;
|
||||
marketplaceId: string;
|
||||
provider: string; // free-text label, e.g. "ozon", "wildberries" - not an enum, new values need no code change
|
||||
authType: 'webhook_signed' | 'api_key' | 'oauth2';
|
||||
credentialRef: string; // pointer into secret storage, never the secret itself
|
||||
pollingIntervalSeconds?: number; // set only when the provider has no webhook
|
||||
cursorState?: string; // opaque, connector-specific pagination/since cursor
|
||||
status: 'active' | 'paused' | 'error';
|
||||
}
|
||||
|
||||
interface RawExternalEvent {
|
||||
id: string;
|
||||
connectorId: string;
|
||||
payload: unknown; // stored verbatim, before any parsing - the traceability anchor
|
||||
receivedAt: string;
|
||||
processedAt?: string;
|
||||
}
|
||||
|
||||
interface ExternalOrderMapping {
|
||||
connectorId: string;
|
||||
externalSellerId: string;
|
||||
externalProductId: string;
|
||||
externalSku: string;
|
||||
internalSellerId: string;
|
||||
internalOfferId: string; // references Phase 3's Offer
|
||||
}
|
||||
|
||||
interface DeadLetter {
|
||||
id: string;
|
||||
connectorId: string;
|
||||
rawEventId: string;
|
||||
reason: string;
|
||||
retryCount: number;
|
||||
lastAttemptAt: string;
|
||||
resolvedAt?: string;
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Pipeline (fixed, shared across every connector)
|
||||
|
||||
```
|
||||
1. Connector receives webhook, or polling finds a new event via cursorState.
|
||||
2. Signature/auth verified. Idempotency key = connectorId + externalOrderId/eventId.
|
||||
3. Payload persisted as RawExternalEvent BEFORE any parsing.
|
||||
4. Normalizer maps payload -> canonical ExternalOrderEvent shape (fixed schema, see §4).
|
||||
5. SKU mapping resolves externalSku -> internal Offer via ExternalOrderMapping.
|
||||
No mapping found -> event goes to the Unmatched queue (§5), does NOT fail silently.
|
||||
6. Order created/updated via the Phase 2 Order API, source: 'external', externalOrderRef set.
|
||||
7. external_order.imported and order.created events emitted (Phase 2 event bus).
|
||||
8. Fulfillment/status changes pushed back to the external marketplace if its API supports it.
|
||||
```
|
||||
|
||||
## 4. Canonical external order event (what the normalizer produces)
|
||||
|
||||
```ts
|
||||
interface ExternalOrderEvent {
|
||||
connectorId: string;
|
||||
externalOrderId: string;
|
||||
externalCreatedAt: string;
|
||||
customer: { name?: string; contact?: string };
|
||||
lines: Array<{ externalSku: string; qty: number; unitPriceMinor: number; currency: string }>;
|
||||
totalMinor: number;
|
||||
currency: string;
|
||||
rawEventId: string; // traceability back to §2
|
||||
}
|
||||
```
|
||||
|
||||
Every provider's adapter is responsible only for producing this shape from its own payload — everything downstream (§3 steps 5–8) is provider-agnostic.
|
||||
|
||||
## 5. Unmatched queue + retry
|
||||
|
||||
```
|
||||
GET /api/admin/v2/integrations/{connectorId}/unmatched
|
||||
POST /api/admin/v2/integrations/{connectorId}/unmatched/{eventId}/resolve { internalOfferId }
|
||||
POST /api/admin/v2/integrations/{connectorId}/dead-letter/{id}/replay
|
||||
```
|
||||
|
||||
Retry policy: exponential backoff, capped attempts, then `DeadLetter` with manual replay from backoffice. No connector is allowed to silently drop an event.
|
||||
|
||||
## 6. Connector-agnostic SLA (applies to every provider, per plan §5.2)
|
||||
|
||||
- Webhook source: 99% of valid events processed in under 60 seconds.
|
||||
- Polling source: delay no worse than `pollingIntervalSeconds + 60`.
|
||||
- **Zero** duplicate orders on repeated event delivery (guaranteed by the idempotency key in §3 step 2).
|
||||
- Every connector error carries a trace id, visible in backoffice.
|
||||
|
||||
## 7. Endpoints
|
||||
|
||||
```
|
||||
POST /api/providers/v1/{connector}/webhook -- generic entrypoint, connector resolved by path + auth
|
||||
GET /api/admin/v2/integrations -- list all connectors + health (last success, lag, errors, backlog)
|
||||
POST /api/admin/v2/integrations -- onboard a new connector: { provider, authType, credentialRef, marketplaceId }
|
||||
PATCH /api/admin/v2/integrations/{id} -- pause/resume, update mapping config
|
||||
```
|
||||
|
||||
## 8. Onboarding a new partner (replaces the old "one sprint per named marketplace")
|
||||
|
||||
1. Register credentials in secret storage, scoped to marketplace/seller.
|
||||
2. `POST /api/admin/v2/integrations` with the provider's auth type and mapping config.
|
||||
3. Write the provider-specific adapter (payload → §4 canonical shape) — the only genuinely bespoke piece per partner.
|
||||
4. Verify in sandbox against the fixed pipeline (§3) — nothing else changes.
|
||||
|
||||
## 9. What the frontend will start doing once this ships
|
||||
|
||||
- Build the backoffice **Integrations** section (missing from admin nav today): connector list, health (last success/lag/errors/backlog/unmatched), FX sources, messaging providers.
|
||||
- Trace-id surfacing on connector errors.
|
||||
- Unmatched-queue resolution UI.
|
||||
88
docs/backend/PHASE-5-SELLER-PORTAL-CONTRACT.md
Normal file
88
docs/backend/PHASE-5-SELLER-PORTAL-CONTRACT.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Phase 5 Backend Contract — Seller Portal
|
||||
|
||||
Companion to [PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md](../PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md) Phase 5 (Sprints 5.1–5.3). Depends on [Phase 3](PHASE-3-CATALOG-OFFER-FULFILLMENT-CONTRACT.md) (Offer) and [Phase 2](PHASE-2-ORDERS-NOTIFICATIONS-CONTRACT.md) (unified Order + Fulfillment).
|
||||
|
||||
**Status: ready to build behind the launch gate.** Frontend note: Seller Management is currently a static placeholder, feature-flagged off by default, with **zero backend bytes and zero `HttpClient` reference** — this contract is a from-scratch build, not a gateway swap.
|
||||
|
||||
---
|
||||
|
||||
## 1. Multi-seller model reminder
|
||||
|
||||
Per the Phase 2 unified-orders decision: a seller never owns a separate `Order`. They see the `Fulfillment` group(s) that belong to them within shared orders, and the `OrderLine`s scoped to their `sellerId`. All endpoints below are pre-filtered server-side to the authenticated seller — never trust a frontend-supplied `sellerId` filter.
|
||||
|
||||
## 2. Entities
|
||||
|
||||
```ts
|
||||
interface SellerOrganization {
|
||||
id: string;
|
||||
marketplaceId: string;
|
||||
legalName: string;
|
||||
status: 'pending' | 'approved' | 'suspended' | 'rejected';
|
||||
bankDetailsRef: string; // pointer into secret storage, never raw account numbers over the wire
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface SellerUser {
|
||||
id: string;
|
||||
sellerOrganizationId: string;
|
||||
role: 'SELLER_OWNER' | 'SELLER_CATALOG_MANAGER' | 'SELLER_ORDER_MANAGER' | 'SELLER_FINANCE_VIEWER' | 'SELLER_VIEWER';
|
||||
email: string;
|
||||
status: 'active' | 'invited' | 'suspended';
|
||||
}
|
||||
|
||||
interface SellerMarketplaceMembership {
|
||||
sellerOrganizationId: string;
|
||||
marketplaceId: string;
|
||||
status: 'pending' | 'approved' | 'suspended';
|
||||
}
|
||||
|
||||
interface SellerIntegration {
|
||||
sellerOrganizationId: string;
|
||||
apiCredentialRef: string;
|
||||
webhookUrl?: string;
|
||||
lastSyncAt?: string;
|
||||
lastSyncError?: string;
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Endpoints (all scoped server-side to the authenticated seller's org)
|
||||
|
||||
```
|
||||
POST /api/seller/v1/onboarding { legalName, contacts, marketplaceId }
|
||||
GET /api/seller/v1/profile
|
||||
GET /api/seller/v1/offers?status=&page=
|
||||
POST /api/seller/v1/offers
|
||||
PATCH /api/seller/v1/offers/{id}
|
||||
POST /api/seller/v1/offers/bulk-price-update -- mass price/stock edit, see Phase 3 §6 for the shared bulk-import pattern
|
||||
GET /api/seller/v1/orders?fulfillmentStatus=
|
||||
PATCH /api/seller/v1/orders/{orderId}/fulfillment/{fulfillmentId} { status, evidence }
|
||||
GET /api/seller/v1/finance/accruals
|
||||
GET /api/seller/v1/finance/settlements
|
||||
POST /api/seller/v1/finance/bank-details -- step-up auth + audit event required, see §5
|
||||
GET /api/seller/v1/team
|
||||
POST /api/seller/v1/team/invite { email, role }
|
||||
GET /api/seller/v1/integrations
|
||||
```
|
||||
|
||||
## 4. Roles (fixed set, enforced backend-side)
|
||||
|
||||
```
|
||||
SELLER_OWNER - full access within the org
|
||||
SELLER_CATALOG_MANAGER - offers/catalog only
|
||||
SELLER_ORDER_MANAGER - orders/fulfillment only
|
||||
SELLER_FINANCE_VIEWER - read-only finance
|
||||
SELLER_VIEWER - read-only everything
|
||||
```
|
||||
|
||||
No UI-only gating. Every endpoint above checks `SellerUser.role` server-side regardless of what the frontend renders — this is the same principle as [Track S](TRACK-S-SECURITY-RBAC-CONTRACT.md), scoped to the seller domain specifically.
|
||||
|
||||
## 5. Sensitive-action rules
|
||||
|
||||
- Bank/payment detail changes (`POST .../finance/bank-details`) require step-up authentication, produce an audit event, and — if maker/checker mode is enabled for the tenant — require a second approver before taking effect.
|
||||
- A seller can never query, by any endpoint or parameter manipulation, another seller's products, orders, customers, finance data, or API keys. This must be enforced at the query layer (implicit `WHERE sellerOrganizationId = :authenticatedSeller`), not left to the frontend to "not ask for it."
|
||||
|
||||
## 6. What the frontend will start doing once this ships
|
||||
|
||||
- Replace the static Seller Management placeholder with real screens: Onboarding, Catalog, Prices & Stock, Orders, Finance, Team, Integrations (per plan §2.2).
|
||||
- Resolve the two competing seller type shapes flagged in `GAPS-AND-IMPROVEMENTS.md` (`SellerConfig` in bootstrap models vs. `Seller`/`SellerBranding` in the domain layer) against this contract's `SellerOrganization`/`SellerUser` shapes.
|
||||
- First-ever exercise of the `sellerManagement.enabled` flag at `true` — write a fixture test, since it has never been tested at its real-world-eventual value.
|
||||
90
docs/backend/PHASE-6-CART-CHECKOUT-CONTRACT.md
Normal file
90
docs/backend/PHASE-6-CART-CHECKOUT-CONTRACT.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Phase 6 Backend Contract — Server Cart + Checkout Session
|
||||
|
||||
Companion to [PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md](../PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md) Phase 6 (Sprints 6.1–6.2). Extends [Phase 1](PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md) §5 (server-authoritative checkout amount) into a full server-owned cart.
|
||||
|
||||
**Status: ready to build** — payment chain unfrozen per Sprint 0.1.
|
||||
|
||||
---
|
||||
|
||||
## 1. Why this exists
|
||||
|
||||
Cart today is `localStorage` + Telegram CloudStorage — no backend cart exists at all. `features/website/checkout/` is an empty directory; checkout lives entirely inside a 751-line cart popup component. Phase 1 §5 already specifies the server-authoritative *amount* at checkout time; this phase makes the *cart itself* server-owned, from add-to-cart onward.
|
||||
|
||||
## 2. Entities
|
||||
|
||||
```ts
|
||||
interface Cart {
|
||||
id: string;
|
||||
marketplaceId: string;
|
||||
customerId?: string; // set for authenticated customers
|
||||
sessionToken?: string; // set for guest carts
|
||||
createdAt: string;
|
||||
expiresAt: string; // TTL for inactive carts
|
||||
}
|
||||
|
||||
interface CartLine {
|
||||
id: string;
|
||||
cartId: string;
|
||||
offerId: string; // never a client-supplied price - see Phase 1 §5
|
||||
qty: number;
|
||||
addedAt: string;
|
||||
}
|
||||
|
||||
interface CheckoutSession {
|
||||
id: string;
|
||||
cartId: string;
|
||||
customerContact: { email?: string; phone?: string; verified: boolean };
|
||||
deliveryOptionId: string;
|
||||
status: 'open' | 'confirmed' | 'expired';
|
||||
createdAt: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
interface DeliveryOption {
|
||||
id: string;
|
||||
marketplaceId: string;
|
||||
label: string;
|
||||
price: Money;
|
||||
type: 'pickup' | 'courier' | 'digital';
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Cart endpoints
|
||||
|
||||
```
|
||||
POST /api/v2/storefront/cart/lines { offerId, qty }
|
||||
PATCH /api/v2/storefront/cart/lines/{lineId} { qty }
|
||||
DELETE /api/v2/storefront/cart/lines/{lineId}
|
||||
GET /api/v2/storefront/cart
|
||||
```
|
||||
|
||||
Invariants:
|
||||
- Idempotent add/update/remove.
|
||||
- Quantity validated against `Offer`/`InventoryRecord` (Phase 3) on every mutation, not just at checkout.
|
||||
- Guest cart identified by `sessionToken` (cookie or header); authenticated cart bound to `customerId`. Adding to a guest cart, then logging in, must merge into the customer's cart — not silently drop items.
|
||||
- Inactive carts and their `StockReservation`s (Phase 3 §3) clear on `expiresAt`.
|
||||
|
||||
## 4. Price-refresh rule
|
||||
|
||||
If an offer's price changed since it was added to the cart, `GET /api/v2/storefront/cart` returns both the line's captured price and the current price, with a `priceChanged: boolean` flag. The frontend must show this and require explicit confirmation before checkout proceeds if the total moved — this is a UX requirement on the frontend, but the backend must expose the comparison, not silently use whichever price it prefers.
|
||||
|
||||
## 5. Checkout session
|
||||
|
||||
Builds directly on [Phase 1 §5.2](PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md#5-server-authoritative-checkout-amount):
|
||||
|
||||
```
|
||||
POST /api/v2/storefront/checkout { cartId, currency, deliveryOptionId }
|
||||
```
|
||||
|
||||
reads the server-owned `Cart`/`CartLine`s directly (no client-supplied offer list needed anymore, unlike the Phase 1 doc's example which pre-dates the server cart). Response shape unchanged from Phase 1 §5.2.
|
||||
|
||||
Additional checkout-time validation beyond Phase 1:
|
||||
- Contact requirement enforced per tenant policy: email and/or phone must be present and (if the tenant requires it) verified before `CheckoutSession.status` can move to `confirmed`.
|
||||
- Guest checkout allowed/disallowed per tenant policy (`MarketplaceFeatureSet`, see [Phase 9](PHASE-9-TENANT-REGISTRY-DOMAINS-CONTRACT.md)).
|
||||
|
||||
## 6. What the frontend will start doing once this ships
|
||||
|
||||
- Build the `features/website/checkout/` module for real — currently an empty directory.
|
||||
- Retire `localStorage`/Telegram-CloudStorage cart persistence.
|
||||
- Show the price-refresh confirmation UI described in §4.
|
||||
- Delete the client-side offer/qty tracking currently duplicated inside the cart popup component.
|
||||
92
docs/backend/PHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md
Normal file
92
docs/backend/PHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Phase 7 Backend Contract — Refunds + Reconciliation
|
||||
|
||||
Companion to [PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md](../PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md) Phase 7 (Sprints 7.1–7.3). Extends [Phase 1](PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md) §6 (payment state machine).
|
||||
|
||||
**Status: ready to build.** `requestRefund(id)` exists today only as a mock gateway method; `reconcil*` and `settlement*` return zero hits anywhere in the codebase.
|
||||
|
||||
---
|
||||
|
||||
## 1. Refunds
|
||||
|
||||
```ts
|
||||
interface Refund {
|
||||
id: string;
|
||||
orderId: string;
|
||||
orderLineIds: string[]; // which lines this refund covers - partial refunds must specify
|
||||
amount: Money;
|
||||
reason: string;
|
||||
actor: string; // user id who initiated it, never anonymous
|
||||
status: 'requested' | 'approved' | 'processing' | 'completed' | 'failed';
|
||||
requestedAt: string;
|
||||
completedAt?: string;
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
POST /api/admin/v2/orders/{orderId}/refunds { orderLineIds, amount, reason }
|
||||
GET /api/admin/v2/orders/{orderId}/refunds
|
||||
```
|
||||
|
||||
A `Refund` updates `Payment.status` to `refunded` or `partially_refunded` (Phase 1 §6.1) and emits `refund.requested`/`refund.completed` on the Phase 2 event bus.
|
||||
|
||||
## 2. Reconciliation
|
||||
|
||||
```ts
|
||||
interface ReconciliationRecord {
|
||||
id: string;
|
||||
orderId: string;
|
||||
providerPaymentId?: string;
|
||||
internalAmount: Money;
|
||||
providerAmount?: Money;
|
||||
matchStrategy: 'provider_payment_id' | 'merchant_reference' | 'amount_currency_fallback';
|
||||
result: 'matched' | 'unmatched' | 'duplicate' | 'amount_mismatch' | 'status_mismatch';
|
||||
resolvedBy?: string;
|
||||
resolvedAt?: string;
|
||||
resolutionNote?: string;
|
||||
}
|
||||
```
|
||||
|
||||
Process (per plan §7.3):
|
||||
```
|
||||
1. Collect internal paid orders for a period.
|
||||
2. Fetch provider transactions/events for the same period.
|
||||
3. Match by providerPaymentId, falling back to merchant reference, falling back to amount+currency.
|
||||
4. Classify: matched / unmatched / duplicate / amount_mismatch / status_mismatch.
|
||||
5. Surface the non-matched set in backoffice with controlled, audited resolution.
|
||||
```
|
||||
|
||||
```
|
||||
GET /api/admin/v2/reconciliation/queue?marketplaceId=&result=
|
||||
POST /api/admin/v2/reconciliation/{id}/resolve { note }
|
||||
```
|
||||
|
||||
## 3. Settlements
|
||||
|
||||
```ts
|
||||
interface Settlement {
|
||||
id: string;
|
||||
sellerId: string;
|
||||
periodStart: string;
|
||||
periodEnd: string;
|
||||
grossAmount: Money;
|
||||
commission: Money;
|
||||
refunds: Money;
|
||||
netPayout: Money;
|
||||
status: 'pending' | 'paid';
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
GET /api/seller/v1/finance/settlements
|
||||
GET /api/admin/v2/finance/settlements?sellerId=&period=
|
||||
```
|
||||
|
||||
## 4. Provider breadth (open business question)
|
||||
|
||||
Current flow supports QR and card only, via one custom provider integration. Adding wallets/BNPL is an explicit open business decision (not answered in Sprint 0.1) — this contract's `PaymentIntent`/`Payment` shapes from Phase 1 §6 are provider-agnostic already, so a new provider is a new adapter behind the same state machine, not a schema change. No action needed here until that business decision is made.
|
||||
|
||||
## 5. What the frontend will start doing once this ships
|
||||
|
||||
- Wire the mock `requestRefund(id)` to a real endpoint.
|
||||
- Build the backoffice **Payments & Finance** section (missing from admin nav today): payments, refunds, reconciliation queue, unmatched events, settlements.
|
||||
- Reconciliation-queue resolution UI with full audit trail.
|
||||
151
docs/backend/PHASE-8-IDENTITY-MESSAGING-CONTRACT.md
Normal file
151
docs/backend/PHASE-8-IDENTITY-MESSAGING-CONTRACT.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# Phase 8 Backend Contract — Customer Identity, VK ID, MAX/Telegram Messaging
|
||||
|
||||
Companion to [PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md](../PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md) Phase 8 (Sprints 8.1–8.5). Covers plan §2.9, §3.4, and all of §14 (the v3.1-only addition).
|
||||
|
||||
**Status: ready to build. Sprint order fixed by Sprint 0.1 decision: VK ID first, then everything else** ("do all after vk"). Sequence below follows that: identity core → VK ID → email/phone OTP → MAX/Telegram → Notification Orchestrator.
|
||||
|
||||
---
|
||||
|
||||
## 1. Entities
|
||||
|
||||
```ts
|
||||
interface Customer {
|
||||
id: string;
|
||||
marketplaceId: string; // or global identity strategy, tenant-configurable
|
||||
name?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
status: 'active' | 'suspended';
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface ExternalIdentity {
|
||||
customerId: string;
|
||||
provider: 'vk_id' | 'telegram' | 'max';
|
||||
providerUserId: string;
|
||||
verifiedAt: string;
|
||||
metadata: Record<string, unknown>;
|
||||
lastUsedAt: string;
|
||||
}
|
||||
|
||||
interface ContactMethod {
|
||||
customerId: string;
|
||||
type: 'email' | 'phone';
|
||||
value: string;
|
||||
verifiedAt?: string;
|
||||
}
|
||||
|
||||
interface ContactChannel {
|
||||
customerId: string;
|
||||
provider: 'telegram' | 'vk' | 'max';
|
||||
chatId: string;
|
||||
verified: boolean;
|
||||
notificationsEnabled: boolean;
|
||||
deliveryEnabled: boolean;
|
||||
}
|
||||
|
||||
interface MessagingConsent {
|
||||
customerId: string;
|
||||
channel: string;
|
||||
purpose: 'marketing' | 'order_service_messages';
|
||||
grantedAt?: string;
|
||||
revokedAt?: string;
|
||||
}
|
||||
```
|
||||
|
||||
Telegram is demoted from sole identity to one `ExternalIdentity` provider among several — it must remain fully functional, just no longer the only path.
|
||||
|
||||
## 2. Sprint 8.2 — VK ID (build first)
|
||||
|
||||
```
|
||||
GET /api/identity/v1/vk/authorize -> redirects into VK's OAuth 2.1/PKCE flow
|
||||
POST /api/identity/v1/vk/callback { code, codeVerifier } -> completes OAuth **backend-side**,
|
||||
links ExternalIdentity, returns session
|
||||
```
|
||||
|
||||
Invariants:
|
||||
- OAuth completion happens entirely backend-side; the VK client secret never reaches the frontend.
|
||||
- A repeat login for the same `providerUserId` must resolve to the same `Customer`, never create a duplicate.
|
||||
- If `providerUserId` is already linked to a *different* `Customer` than the one currently authenticated (or none), this is an identity conflict — route to controlled resolution, never silently overwrite the existing binding (plan §14.3).
|
||||
|
||||
## 3. Sprint 8.3 — Email/phone OTP (after VK ID)
|
||||
|
||||
Implements the already-approved [email/phone login spec](../superpowers/specs/2026-08-15-email-phone-login-design.md). Per v3.1 §14, position this as **recovery/fallback** when a messenger channel is unavailable — not the primary login path. No new contract beyond that spec; this section exists only to fix its place in the build order relative to VK ID.
|
||||
|
||||
## 4. Sprint 8.4 — MAX + Telegram bot channels
|
||||
|
||||
```ts
|
||||
interface BotConversationBinding {
|
||||
customerId: string;
|
||||
marketplaceId: string;
|
||||
provider: 'telegram' | 'max';
|
||||
chatId: string;
|
||||
state: string; // see §5 state machine
|
||||
orderId?: string;
|
||||
lastMessageAt: string;
|
||||
}
|
||||
```
|
||||
|
||||
MAX linking flow (bot-assisted, one-time code):
|
||||
```
|
||||
POST /api/identity/v1/max/link-code -> { code, expiresAt } (TTL, single-use, bound to marketplace + browser session)
|
||||
```
|
||||
User opens the MAX bot, sends the code; a confirmed bot update on the backend calls:
|
||||
```
|
||||
POST /api/providers/v1/max/bot-webhook -- idempotent; a repeated update must not create a duplicate binding
|
||||
```
|
||||
which links the pending `Customer` session to the MAX `chatId`.
|
||||
|
||||
All three providers' incoming bot updates (VK, MAX, Telegram) normalize into one shape:
|
||||
|
||||
```ts
|
||||
interface MessagingEvent {
|
||||
provider: 'telegram' | 'vk' | 'max';
|
||||
chatId: string;
|
||||
orderId?: string;
|
||||
text?: string;
|
||||
receivedAt: string;
|
||||
}
|
||||
```
|
||||
|
||||
Provider bot tokens/secrets never reach the frontend, ever — only the backend calls each provider's Bot API.
|
||||
|
||||
## 5. Sprint 8.5 — Notification Orchestrator + Delivery Conversation State Machine
|
||||
|
||||
On `order.paid` (Phase 2 event bus), the orchestrator picks the customer's chosen channel (captured at checkout, see [Phase 6](PHASE-6-CART-CHECKOUT-CONTRACT.md) and `OrderContactSnapshot` in [Phase 2](PHASE-2-ORDERS-NOTIFICATIONS-CONTRACT.md)) and drives:
|
||||
|
||||
```
|
||||
not_started -> awaiting_customer -> details_received -> manager_assigned/auto_confirmed -> shipment_planned -> completed
|
||||
```
|
||||
|
||||
```ts
|
||||
interface DeliveryDetailsSnapshot {
|
||||
orderId: string;
|
||||
city?: string;
|
||||
address?: string;
|
||||
recipientName?: string;
|
||||
phone?: string;
|
||||
timeWindow?: string;
|
||||
comment?: string;
|
||||
receivedAt: string;
|
||||
}
|
||||
```
|
||||
|
||||
Hard rules:
|
||||
- **The bot never changes financial statuses.** It can only write `DeliveryDetailsSnapshot` fields via a dedicated Delivery Service — no bot code path touches `Order.paymentStatus`/`orderStatus`.
|
||||
- The backoffice `Notification` (Phase 2 §6) fires unconditionally on `order.paid`, independent of whether the customer's messenger channel is reachable.
|
||||
- If the chosen channel is unavailable, log a `DeliveryAttempt` error (Phase 2 §6) and fall back per tenant-configured policy (e.g. email/SMS) — never block the order itself.
|
||||
- Follow-up messages are rate-limited per tenant policy; after the configured attempt limit, hand off to a human manager instead of continuing to message.
|
||||
|
||||
```
|
||||
POST /api/providers/v1/{provider}/bot-webhook -- generic entrypoint for all three providers
|
||||
GET /api/admin/v2/orders/{orderId}/conversation -- message history + current state, for manager handoff
|
||||
POST /api/admin/v2/orders/{orderId}/conversation/handoff
|
||||
```
|
||||
|
||||
## 6. What the frontend will start doing once this ships
|
||||
|
||||
- VK ID login button + OAuth redirect flow on storefront (primary social login).
|
||||
- MAX/Telegram linking UI (one-time code flow).
|
||||
- Checkout channel-choice step ("where should we send confirmation?") — VK / MAX / Telegram / email/SMS fallback.
|
||||
- Manager-facing conversation view (message history, current delivery state, accept handoff).
|
||||
130
docs/backend/PHASE-9-TENANT-REGISTRY-DOMAINS-CONTRACT.md
Normal file
130
docs/backend/PHASE-9-TENANT-REGISTRY-DOMAINS-CONTRACT.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# Phase 9 Backend Contract — Tenant Registry, Domain Automation, Publish Model
|
||||
|
||||
Companion to [PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md](../PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md) Phase 9 (Sprints 9.1–9.3). Covers plan §4.3, §8.
|
||||
|
||||
**Status: ready to build.** Zero `hostinger` references exist in the codebase today.
|
||||
|
||||
---
|
||||
|
||||
## 1. Entities
|
||||
|
||||
```ts
|
||||
interface Marketplace {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
type: 'commerce' | 'mall_directory' | 'hybrid' | 'single_brand';
|
||||
ownerId: string;
|
||||
countries: string[];
|
||||
locales: string[];
|
||||
currencies: string[];
|
||||
timezone: string;
|
||||
lifecycleState: MarketplaceLifecycleState;
|
||||
}
|
||||
|
||||
type MarketplaceLifecycleState =
|
||||
| 'draft' | 'configured' | 'content_ready' | 'domains_planned'
|
||||
| 'staging_live' | 'qa_passed' | 'production_ready' | 'live' | 'paused' | 'archived';
|
||||
|
||||
interface MarketplaceDomain {
|
||||
marketplaceId: string;
|
||||
domain: string;
|
||||
type: 'production' | 'www' | 'staging' | 'preview' | 'api' | 'seller';
|
||||
status: 'planned' | 'dns_pending' | 'ssl_pending' | 'active' | 'failed';
|
||||
}
|
||||
|
||||
interface MarketplaceFeatureSet {
|
||||
marketplaceId: string;
|
||||
features: Record<string, boolean>; // e.g. { catalog: true, sellers: true, cart: true, checkout: true, payments: true, orders: true, refunds: true, directory: false, ... }
|
||||
}
|
||||
|
||||
interface MarketplaceRevision {
|
||||
id: string;
|
||||
marketplaceId: string;
|
||||
status: 'draft' | 'validated' | 'preview' | 'published';
|
||||
publishedAt?: string;
|
||||
supersedesRevisionId?: string; // rollback creates a NEW revision, never mutates the old one
|
||||
}
|
||||
```
|
||||
|
||||
**Hard invariant:** `Order`, `Payment`, `InventoryRecord`, and every financial ledger row are **not part of a `MarketplaceRevision`**. Rolling back a storefront design revision must never touch commerce data.
|
||||
|
||||
## 2. Lifecycle state machine
|
||||
|
||||
```
|
||||
draft -> configured -> content_ready -> domains_planned -> staging_live -> qa_passed -> production_ready -> live -> paused/archived
|
||||
```
|
||||
|
||||
Every state transition endpoint must return the specific blocker preventing the next transition — not just "not ready."
|
||||
|
||||
```
|
||||
GET /api/admin/v2/marketplaces/{id}/lifecycle -> { currentState, nextState, blockers: string[] }
|
||||
POST /api/admin/v2/marketplaces/{id}/lifecycle/advance
|
||||
```
|
||||
|
||||
## 3. Onboarding wizard (8 steps, plan §4.3)
|
||||
|
||||
```
|
||||
POST /api/admin/v2/marketplaces -- step 1: name/code/type/owner/countries/locales/currencies/timezone
|
||||
PATCH /api/admin/v2/marketplaces/{id}/feature-set -- step 2
|
||||
POST /api/admin/v2/marketplaces/{id}/domains -- step 3
|
||||
PATCH /api/admin/v2/marketplaces/{id}/design -- step 4
|
||||
POST /api/admin/v2/marketplaces/{id}/roles -- step 5
|
||||
PATCH /api/admin/v2/marketplaces/{id}/integrations -- step 6
|
||||
POST /api/admin/v2/marketplaces/{id}/staging-launch -- step 7, runs smoke tests
|
||||
POST /api/admin/v2/marketplaces/{id}/production-launch -- step 8, requires all P0 blockers closed + explicit approval
|
||||
```
|
||||
|
||||
## 4. Domain automation (Hostinger API, per plan §8.2)
|
||||
|
||||
```
|
||||
GET /api/dns/v1/zones/{domain}
|
||||
POST /api/dns/v1/zones/{domain}/validate
|
||||
PUT /api/dns/v1/zones/{domain}
|
||||
DELETE /api/dns/v1/zones/{domain}
|
||||
GET /api/dns/v1/snapshots/{domain}
|
||||
GET /api/dns/v1/snapshots/{domain}/{snapshotId}
|
||||
POST /api/dns/v1/snapshots/{domain}/{snapshotId}/restore
|
||||
```
|
||||
|
||||
Process, strictly in this order:
|
||||
```
|
||||
1. Read current DNS zone.
|
||||
2. Save a snapshot (rollback payload) BEFORE any change.
|
||||
3. Build and validate a DNS plan.
|
||||
4. NEVER touch MX/SPF/DKIM/DMARC/CAA records without a separate, explicitly scoped task.
|
||||
5. Apply records only after production approval.
|
||||
6. Verify propagation, SSL issuance, and health checks.
|
||||
7. Mark the domain 'active' only after all checks in step 6 pass.
|
||||
```
|
||||
|
||||
## 5. Publish model
|
||||
|
||||
```
|
||||
draft -> validation -> preview -> publish
|
||||
```
|
||||
|
||||
```
|
||||
POST /api/admin/v2/marketplaces/{id}/revisions -- create draft
|
||||
POST /api/admin/v2/marketplaces/{id}/revisions/{revId}/validate
|
||||
POST /api/admin/v2/marketplaces/{id}/revisions/{revId}/publish -- becomes immutable
|
||||
POST /api/admin/v2/marketplaces/{id}/revisions/{revId}/rollback -- creates a NEW revision pointing at the prior published content
|
||||
```
|
||||
|
||||
Replaces the current builder's `localStorage`-only draft persistence and the empty `apiEndpoints.builder: {}` placeholder in bootstrap. CMS/static-page content (currently in-memory bootstrap only) gets a real write path through this same revision model.
|
||||
|
||||
## 6. Tenant resolution hardening
|
||||
|
||||
```
|
||||
GET /api/v2/storefront/bootstrap -- resolved server-side from verified Host header
|
||||
```
|
||||
|
||||
- Host is normalized and matched against `MarketplaceDomain` server-side — the marketplace ID from the browser is never a trust boundary.
|
||||
- Unknown Host → `404`, with **no fallback to any other tenant**.
|
||||
|
||||
## 7. What the frontend will start doing once this ships
|
||||
|
||||
- Build the backoffice **Marketplaces** section (missing from admin nav today): registry, type, status, domains, currencies, feature set, responsible manager.
|
||||
- Build the **Domains & Releases** section: DNS/SSL status, staging/production, health checks, rollback.
|
||||
- Wire the project editor/builder to real revision persistence instead of `localStorage`.
|
||||
- Marketplace dashboard: GMV, paid orders, conversion, payment failure rate, moderation queue, low stock, unmatched events, integration health, domain/SSL/release status (plan §4.2).
|
||||
42
docs/backend/README.md
Normal file
42
docs/backend/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Backend Contracts Index — Product Plan v3.1
|
||||
|
||||
This directory is the complete set of wire contracts for building the backend behind [Product Plan v3.1](../PRODUCT-PLAN-v3.1-GAP-ANALYSIS.md). Each doc specifies entities, endpoints, and invariants only — never DB schema or service boundaries, which stay backend's own call.
|
||||
|
||||
**Read order matches build order.** Every doc after Phase 1 depends on the ones before it (noted at the top of each). All Sprint 0.1 decisions referenced throughout were answered 2026-08-17 — see [PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md](../PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md) Sprint 0.1 for the full record.
|
||||
|
||||
## Launch-gate phases (P0 — required before production)
|
||||
|
||||
| Doc | Covers | Status |
|
||||
|---|---|---|
|
||||
| [PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md](PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md) | Money model, FX quote, price snapshot, server-authoritative checkout amount, payment state machine | Ready |
|
||||
| [PHASE-2-ORDERS-NOTIFICATIONS-CONTRACT.md](PHASE-2-ORDERS-NOTIFICATIONS-CONTRACT.md) | Canonical Order/OrderLine/Fulfillment (unified multi-seller), event bus, Notification Center | Ready |
|
||||
| [PHASE-3-CATALOG-OFFER-FULFILLMENT-CONTRACT.md](PHASE-3-CATALOG-OFFER-FULFILLMENT-CONTRACT.md) | Product/Offer split, inventory/reservations, publish-time executability | Ready |
|
||||
| [PHASE-4-CONNECTOR-FRAMEWORK-CONTRACT.md](PHASE-4-CONNECTOR-FRAMEWORK-CONTRACT.md) | Generic external-order connector framework (no fixed marketplace list) | Ready |
|
||||
|
||||
## Post-launch-gate phases (P1/P2)
|
||||
|
||||
| Doc | Covers | Status |
|
||||
|---|---|---|
|
||||
| [PHASE-5-SELLER-PORTAL-CONTRACT.md](PHASE-5-SELLER-PORTAL-CONTRACT.md) | Seller org/user/membership, seller-scoped order/fulfillment views | Ready |
|
||||
| [PHASE-6-CART-CHECKOUT-CONTRACT.md](PHASE-6-CART-CHECKOUT-CONTRACT.md) | Server-owned cart, checkout session | Ready |
|
||||
| [PHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md](PHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md) | Refunds, reconciliation, settlements | Ready |
|
||||
| [PHASE-8-IDENTITY-MESSAGING-CONTRACT.md](PHASE-8-IDENTITY-MESSAGING-CONTRACT.md) | Customer identity, VK ID (built first), OTP, MAX/Telegram bots, Notification Orchestrator | Ready |
|
||||
| [PHASE-9-TENANT-REGISTRY-DOMAINS-CONTRACT.md](PHASE-9-TENANT-REGISTRY-DOMAINS-CONTRACT.md) | Marketplace registry, Hostinger DNS automation, publish/revision model | Ready |
|
||||
| [PHASE-10-CONTENT-MODULES-CONTRACT.md](PHASE-10-CONTENT-MODULES-CONTRACT.md) | Gorbushka-class mall/directory content entities | Ready, lowest priority |
|
||||
|
||||
## Cross-cutting tracks
|
||||
|
||||
| Doc | Covers | Status |
|
||||
|---|---|---|
|
||||
| [TRACK-A-ANALYTICS-CONTRACT.md](TRACK-A-ANALYTICS-CONTRACT.md) | Event pipeline, funnel, operational/quality metrics, synthetic-traffic separation | Ready — start alongside Phase 1, longest lead time |
|
||||
| [TRACK-S-SECURITY-RBAC-CONTRACT.md](TRACK-S-SECURITY-RBAC-CONTRACT.md) | 17 roles/3 scopes, enforcement, audit log, secrets, rate limiting, step-up auth | Ready — gates the launch |
|
||||
|
||||
## What is deliberately not in this directory
|
||||
|
||||
- **API namespace migration** — Sprint 0.1 decision: new endpoints only use `/api/v2/...` etc; legacy endpoints (`/cart`, `/orders`, `/items`) are not being migrated as part of this contract set. See `BACKEND-API-REFERENCE.md` for the current live surface.
|
||||
- **Per-connector adapters** (Ozon, Wildberries, etc.) — Sprint 0.1 decision: no fixed list. [Phase 4](PHASE-4-CONNECTOR-FRAMEWORK-CONTRACT.md) §8 is the onboarding runbook; each partner's adapter is written when that partner is actually onboarded.
|
||||
- **Additional payment providers** (wallets, BNPL) — open business decision, not yet made. [Phase 7](PHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md) §4.
|
||||
|
||||
## One open item across all of these
|
||||
|
||||
**Backend ownership is still unanswered** (Sprint 0.1). Every contract above is ready to hand to whoever builds it — that person/team just hasn't been named yet.
|
||||
89
docs/backend/TRACK-A-ANALYTICS-CONTRACT.md
Normal file
89
docs/backend/TRACK-A-ANALYTICS-CONTRACT.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# Track A Backend Contract — Analytics Event Pipeline
|
||||
|
||||
Companion to [PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md](../PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md) Track A. Covers plan §3.1, §6.3, §13.3.
|
||||
|
||||
**Status: ready to build. Start alongside Phase 1, not last** — longest lead time in the programme, and it's a P0 in the plan's own §3.1. No tracking infrastructure exists at all today; this is missing infrastructure, not a missing endpoint.
|
||||
|
||||
---
|
||||
|
||||
## 1. Event logging spine
|
||||
|
||||
```ts
|
||||
interface AnalyticsEvent {
|
||||
eventType: string; // see §2-4 for the fixed vocabulary
|
||||
marketplaceId: string;
|
||||
sessionId: string;
|
||||
customerId?: string;
|
||||
timestamp: string;
|
||||
properties: Record<string, unknown>;
|
||||
isSynthetic: boolean; // see §6 - mandatory, never inferred
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
POST /api/v2/storefront/analytics/events { eventType, properties } -- server-side batched ingest
|
||||
```
|
||||
|
||||
Frontend fires events client-side; backend is the source of truth for `sessionId` and `isSynthetic` — never trust a client-asserted synthetic flag without a matching signed staging/test-environment token.
|
||||
|
||||
## 2. Traffic events
|
||||
|
||||
```
|
||||
session_started, page_view, product_view (with source/utm/referrer), unique users/sessions rollups
|
||||
```
|
||||
|
||||
## 3. Catalog events
|
||||
|
||||
```
|
||||
search, category_view, product_view, seller_view
|
||||
```
|
||||
|
||||
## 4. Commerce events
|
||||
|
||||
```
|
||||
add_to_cart, cart_view, checkout_started, payment_started, payment_success, payment_failed, order_created
|
||||
```
|
||||
|
||||
These map directly onto the Phase 1/2/6 contracts' own state transitions — emit them from the same backend code paths that already produce `PaymentEvent`/`OrderEvent`, not a separately-maintained tracking layer that can drift.
|
||||
|
||||
## 5. Operational + quality metrics
|
||||
|
||||
```ts
|
||||
interface OperationalMetric {
|
||||
name: 'order_paid_to_notification_latency' | 'fulfillment_time' | 'connector_lag' | 'payment_webhook_lag';
|
||||
marketplaceId: string;
|
||||
value: number;
|
||||
unit: 'seconds' | 'minutes';
|
||||
measuredAt: string;
|
||||
}
|
||||
```
|
||||
|
||||
Quality events: frontend/backend errors, checkout validation failures, FX stale-rate blocks (Phase 1 §3.2).
|
||||
|
||||
## 6. Synthetic traffic separation (hard requirement, plan §3.1/§6.3/§10.2)
|
||||
|
||||
Synthetic/load-test traffic is permitted in staging and demo environments **only**, and must be technically inseparable-by-accident from production data — i.e. `isSynthetic: true` set server-side based on environment/token, never a client-settable flag that a real visit could accidentally or deliberately carry. Business reports must filter it out by construction, not by a manual exclusion query someone has to remember to add.
|
||||
|
||||
## 7. Endpoints
|
||||
|
||||
```
|
||||
GET /api/admin/v2/analytics/funnel?marketplaceId=&period=
|
||||
GET /api/admin/v2/analytics/operational?marketplaceId=&metric=
|
||||
GET /api/admin/v2/analytics/quality?marketplaceId=
|
||||
GET /api/v2/storefront/search/trending?marketplaceId= -- top N queries over a recent window, closes the existing SearchTrendingService.loadTrending() stub (returns of(null) today)
|
||||
```
|
||||
|
||||
## 8. Post-launch monitoring set (plan §13.3, reuses the same event stream)
|
||||
|
||||
```
|
||||
checkout_conversion, payment_success_failure_rate, webhook_processing_lag,
|
||||
order_notification_lag, external_connector_lag, fx_quote_age_errors,
|
||||
unmatched_reconciliation_count, fulfillment_stuck_count
|
||||
```
|
||||
|
||||
## 9. What the frontend will start doing once this ships
|
||||
|
||||
- Replace the fully mock-composed `AdminAnalyticsFacade` with real funnel data.
|
||||
- Fire the event vocabulary above from the relevant storefront interaction points.
|
||||
- Bridge or replace the currently-always-zero `AdminProduct.visits` column with real tracking (see `GAPS-AND-IMPROVEMENTS.md`'s admin-product-views item — already partially speced in this session's [admin product views design](../superpowers/plans/2026-08-15-admin-product-views-column.md)).
|
||||
- Wire `SearchTrendingService.loadTrending()` to the real endpoint in §7.
|
||||
83
docs/backend/TRACK-S-SECURITY-RBAC-CONTRACT.md
Normal file
83
docs/backend/TRACK-S-SECURITY-RBAC-CONTRACT.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Track S Backend Contract — RBAC, Audit, Secrets, Rate Limiting
|
||||
|
||||
Companion to [PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md](../PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md) Track S. Covers plan §4.4, §10.
|
||||
|
||||
**Status: ready to build. Gates the launch — this is the single most serious security gap identified in this session's audit.** Today the admin role model is decorative: `AdminRole` and permissions exist as types, but nothing gates any button, page, or action anywhere in the app. Any authenticated admin has full access.
|
||||
|
||||
---
|
||||
|
||||
## 1. Roles (17 total, 3 scopes, per plan §4.4)
|
||||
|
||||
```ts
|
||||
type PlatformRole = 'PLATFORM_OWNER' | 'TECH_ADMIN' | 'SECURITY_ADMIN' | 'DOMAIN_MANAGER' | 'VIEWER';
|
||||
|
||||
type MarketplaceRole =
|
||||
| 'MARKETPLACE_ADMIN' | 'CONTENT_MANAGER' | 'CATALOG_MANAGER' | 'ORDER_MANAGER'
|
||||
| 'FINANCE_MANAGER' | 'SUPPORT_MANAGER' | 'VIEWER';
|
||||
|
||||
type SellerRole =
|
||||
| 'SELLER_OWNER' | 'SELLER_CATALOG_MANAGER' | 'SELLER_ORDER_MANAGER'
|
||||
| 'SELLER_FINANCE_VIEWER' | 'SELLER_VIEWER';
|
||||
```
|
||||
|
||||
`SellerRole` is already specified in [Phase 5's contract](PHASE-5-SELLER-PORTAL-CONTRACT.md) §4 — this doc adds the platform and marketplace scopes around it.
|
||||
|
||||
## 2. Enforcement (backend-side, non-negotiable)
|
||||
|
||||
Every `/api/admin/v2/*` and `/api/platform/v1/*` endpoint must check `(role, tenantScope)` against the acting user's session — **before** touching data, not as a post-hoc filter. `tenant scope` here means: a `MARKETPLACE_ADMIN` for marketplace A must get a `403` (not an empty result) querying marketplace B's data, never a silently-scoped response that looks like "there's just nothing here."
|
||||
|
||||
```
|
||||
GET /api/identity/v1/session/permissions -> { role, scopes: string[], marketplaceIds: string[] }
|
||||
```
|
||||
|
||||
Frontend route/action guards derive from this endpoint's response — never hardcode role logic client-side beyond hiding UI affordances (which is convenience, not security).
|
||||
|
||||
## 3. Audit log
|
||||
|
||||
```ts
|
||||
interface AuditEvent {
|
||||
id: string;
|
||||
actor: string;
|
||||
action: string; // e.g. 'role.changed', 'offer.price_updated', 'refund.approved'
|
||||
entityType: string;
|
||||
entityId: string;
|
||||
before?: unknown;
|
||||
after?: unknown;
|
||||
reason?: string;
|
||||
occurredAt: string;
|
||||
ip?: string;
|
||||
}
|
||||
```
|
||||
|
||||
Mandatory coverage (plan §10.1): permission changes, seller status changes, catalog moderation actions, price changes, payment/refund actions, manual order overrides, integration credential changes, production launch actions.
|
||||
|
||||
```
|
||||
GET /api/admin/v2/audit?marketplaceId=&entityType=&actor=&from=&to=
|
||||
```
|
||||
|
||||
## 4. Secrets
|
||||
|
||||
All provider/connector credentials (payment providers, external marketplace connectors, VK/MAX/Telegram bot tokens, FX source keys) live in dedicated secret storage, referenced by opaque `credentialRef` strings in every other contract in this series — never returned in any API response body, never logged in plaintext.
|
||||
|
||||
## 5. Rate limiting
|
||||
|
||||
```
|
||||
429 response: { error: { code: 'RATE_LIMITED', retryAfterSeconds: number } }
|
||||
```
|
||||
|
||||
Applies to storefront/auth/provider endpoints. Frontend currently has **zero** 429 handling anywhere — see [BACKEND-API-REFERENCE.md §5](../../BACKEND-API-REFERENCE.md) for the full error-envelope contract this should follow.
|
||||
|
||||
## 6. Step-up authentication
|
||||
|
||||
Required before: bank/payment detail changes (Phase 5 §5), production launch (Phase 9 §3 step 8), role grants at `PLATFORM_OWNER`/`MARKETPLACE_ADMIN` level, and any manual financial override (refund approval outside normal flow, price override on a live order).
|
||||
|
||||
## 7. PII minimization
|
||||
|
||||
Customer/seller PII is exposed only to roles that need it for their scope (e.g. `FINANCE_VIEWER` sees payout totals, not raw bank account numbers unless `FINANCE_MANAGER`+). Export endpoints (`GET .../export`) are themselves audit-logged actions per §3.
|
||||
|
||||
## 8. What the frontend will start doing once this ships
|
||||
|
||||
- Route guards and action-level permission checks across the entire backoffice — currently none exist.
|
||||
- Backoffice **Audit & Security** section (missing from admin nav today): role changes, sensitive actions, login/security events, exports.
|
||||
- Reconcile `AdminRole` (already de-duplicated to one canonical type this session) against the real 17-role table from §1.
|
||||
- 429 interceptor + retry-after UI.
|
||||
Reference in New Issue
Block a user