docs: backend contracts for Phases 5-7 (seller portal, server cart, reconciliation)
- Phase 5: Seller Portal from scratch (zero backend bytes exist today) - SellerOrganization/SellerUser/SellerMarketplaceMembership, all endpoints scoped server-side to the unified-orders Fulfillment model from Phase 2. - Phase 6: server-owned Cart/CartLine/CheckoutSession, extending Phase 1's server-authoritative-amount contract into the cart itself. Replaces localStorage/Telegram-CloudStorage cart persistence. - Phase 7: Refund and ReconciliationRecord entities, settlement contract. Flags additional payment providers (wallets/BNPL) as still an open business decision - not blocking, schema is provider-agnostic already. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
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.
|
||||||
Reference in New Issue
Block a user