Files
marketplaces/docs/backend/PHASE-6-CART-CHECKOUT-CONTRACT.md

91 lines
3.8 KiB
Markdown
Raw Normal View History

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