From 707db6d43c3cb5f72af9ad8506adbe9be11dad41 Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Mon, 17 Aug 2026 23:04:11 +0400 Subject: [PATCH] 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 --- .../backend/PHASE-5-SELLER-PORTAL-CONTRACT.md | 88 ++++++++++++++++++ .../backend/PHASE-6-CART-CHECKOUT-CONTRACT.md | 90 ++++++++++++++++++ ...HASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md | 92 +++++++++++++++++++ 3 files changed, 270 insertions(+) create mode 100644 docs/backend/PHASE-5-SELLER-PORTAL-CONTRACT.md create mode 100644 docs/backend/PHASE-6-CART-CHECKOUT-CONTRACT.md create mode 100644 docs/backend/PHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md diff --git a/docs/backend/PHASE-5-SELLER-PORTAL-CONTRACT.md b/docs/backend/PHASE-5-SELLER-PORTAL-CONTRACT.md new file mode 100644 index 0000000..0adea2d --- /dev/null +++ b/docs/backend/PHASE-5-SELLER-PORTAL-CONTRACT.md @@ -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. diff --git a/docs/backend/PHASE-6-CART-CHECKOUT-CONTRACT.md b/docs/backend/PHASE-6-CART-CHECKOUT-CONTRACT.md new file mode 100644 index 0000000..a61c8c4 --- /dev/null +++ b/docs/backend/PHASE-6-CART-CHECKOUT-CONTRACT.md @@ -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. diff --git a/docs/backend/PHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md b/docs/backend/PHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md new file mode 100644 index 0000000..1a8b7dd --- /dev/null +++ b/docs/backend/PHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md @@ -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.