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