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

153 lines
4.9 KiB
Markdown

# 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
```ts
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:
```http
POST /api/cart/items HTTP/1.1
Content-Type: application/json
```
Response:
```json
{
"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
```json
{ "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`:
```js
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.