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