# 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.