Files
kaiser-natron/docs/api/cart.md
Dorian ea7d9b04cc docs: correct backend stack to PHP/MySQL and document checkout/orders/customers
Swap lingering "Python/MySQL" wording for "PHP / MySQL" across the
README, `src/api/` seam, the Pinia cart store, and the cart contract
doc. Add endpoint specs for checkout (Stripe handoff + webhook),
orders, and customers so the full plug-in surface is documented in
the same style as cart.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 10:48:11 +01:00

4.9 KiB

Cart API

The frontend talks to the cart through a small, stable surface exported from src/api/cart.js. That file is the only seam that changes when the PHP / MySQL backend comes online — everything above it (the Pinia store, the CartDrawer component, pages that add to cart) keeps importing the same functions with the same signatures.

Today the function bodies delegate to an in-browser Pinia store with localStorage persistence so the UI is fully functional without a server. Replacing the bodies with fetch() calls against the endpoints below is a drop-in swap.

Base URL and session

  • All endpoints are served under /api.
  • Requests carry the session via an httpOnly cookie. The backend sets it on first POST, and the frontend sends credentials: 'include' on every call.
  • Requests and responses are application/json; charset=utf-8.

Endpoints

Method Path Body Returns
GET /api/cart Cart
POST /api/cart/items { productId, quantity } Cart
PATCH /api/cart/items/:productId { quantity } Cart
DELETE /api/cart/items/:productId Cart
DELETE /api/cart Cart

Every mutation returns the full, updated Cart. The client never has to merge partial responses.

Types

type Money     = number   // EUR, two decimals, always > 0
type ProductId = string   // /^[a-z0-9-]+$/, max 80 chars

interface ProductSummary {
  id:    ProductId
  title: string
  brand: string
  size:  string
  image: string   // absolute path, served from the frontend public/ dir
  href:  string   // PDP URL
}

interface CartLine {
  productId: ProductId
  quantity:  number        // integer >= 1
  unitPrice: Money
  lineTotal: Money         // unitPrice * quantity, rounded to 2dp
  product:   ProductSummary
}

interface Cart {
  items:     CartLine[]
  count:     number        // integer, sum of all quantities
  subtotal:  Money         // sum of all lineTotal values
  updatedAt: string        // ISO-8601 UTC
}

Product IDs match the id field of the catalogue exported from src/api/products.js. The backend is the source of truth for unitPrice and lineTotal; the client only displays what it receives.

Example exchange

Request:

POST /api/cart/items HTTP/1.1
Content-Type: application/json

{ "productId": "kaiser-natron-pulver-250-g-grosspackung", "quantity": 2 }

Response:

{
  "items": [
    {
      "productId": "kaiser-natron-pulver-250-g-grosspackung",
      "quantity": 2,
      "unitPrice": 4.49,
      "lineTotal": 8.98,
      "product": {
        "id":    "kaiser-natron-pulver-250-g-grosspackung",
        "title": "Kaiser-Natron® Pulver",
        "brand": "Kaiser-Natron",
        "size":  "250 g Großpackung",
        "image": "/products/kaiser-natron-pulver-250-g-gro%C3%9Fpackung.jpg",
        "href":  "/shop/kaiser-natron-pulver-250-g-grosspackung"
      }
    }
  ],
  "count":     2,
  "subtotal":  8.98,
  "updatedAt": "2026-04-22T14:05:21.004Z"
}

Errors

{ "error": { "code": "product.notFound", "message": "Unknown productId." } }

Known codes:

Code When
product.notFound productId is not in the catalogue.
cart.quantityInvalid quantity is missing, non-integer, or < 1 on POST/PATCH.
cart.itemLimit The cart exceeds its per-line or per-cart quantity cap.

HTTP status codes:

  • 200 OK on success (including DELETE, which returns the updated cart).
  • 400 Bad Request for validation errors (cart.quantityInvalid).
  • 404 Not Found for product.notFound and for PATCH/DELETE on a productId that is not in the cart.
  • 409 Conflict for cart.itemLimit.
  • 5xx for server failures — the frontend surfaces these as a generic retry message.

Swapping the local implementation for HTTP

Replace each body in src/api/cart.js with a fetch. Example for addToCart:

export async function addToCart(productId, quantity = 1) {
  const res = await fetch('/api/cart/items', {
    method: 'POST',
    credentials: 'include',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ productId, quantity }),
  })
  if (!res.ok) throw await res.json().catch(() => ({ error: { code: 'network' } }))
  return res.json()
}

The Pinia store in src/stores/cart.js is the frontend-side cache; once the API is remote, hydrate it from fetchCart() on app start and after each mutation. The CartDrawer component is state-agnostic and keeps working either way.