Files
kaiser-natron/docs/api/orders.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

149 lines
4.6 KiB
Markdown

# Orders API
Orders are created as `pending` by the checkout intent endpoint (see
`checkout.md`) and transition to `paid` / `failed` / `refunded` based on
Stripe webhooks. The frontend reads orders to render the confirmation
page, the account order history, and the order detail view.
The frontend consumes this surface through a future `src/api/orders.js`
module following the same pattern as `cart.js`.
## Base URL and session
- All endpoints are served under `/api`.
- Authorisation: the session cookie identifies the buyer. Guest orders
are scoped to the session cookie that created them; once a guest logs
in or registers, the backend MAY attach prior guest orders to the
customer record.
- Requests and responses are `application/json; charset=utf-8`.
## Endpoints
| Method | Path | Body | Returns |
| ------ | --------------------- | ---- | -------------- |
| GET | `/api/orders` | — | `OrderList` |
| GET | `/api/orders/:id` | — | `Order` |
`GET /api/orders` returns only orders visible to the current session —
the logged-in customer's orders, or guest orders created during this
session.
## Types
```ts
type Money = number // EUR, 2dp
type OrderId = string
type ISO8601 = string
type OrderStatus = 'pending' | 'paid' | 'failed' | 'refunded' | 'cancelled'
type PaymentStatus = 'pending' | 'paid' | 'failed' | 'refunded'
type FulfilmentStatus = 'unfulfilled' | 'processing' | 'shipped' | 'delivered'
interface OrderLine {
productId: string
title: string // snapshot at order time — safe to display as-is
size: string
quantity: number
unitPrice: Money
lineTotal: Money
}
interface Order {
id: OrderId
number: string // human-readable, e.g. "KN-2026-0001"
status: OrderStatus
paymentStatus: PaymentStatus
fulfilmentStatus: FulfilmentStatus
createdAt: ISO8601
paidAt?: ISO8601
items: OrderLine[]
subtotal: Money
shipping: Money
tax: Money
total: Money
currency: string // ISO 4217
shippingAddress: Address // shape defined in checkout.md
billingAddress: Address
email: string
trackingUrl?: string // set once the fulfilmentStatus is "shipped"
}
interface OrderList {
items: Order[] // newest first
count: number
}
```
Line snapshots (`title`, `size`, `unitPrice`) are frozen at order
creation. If the catalogue changes later, existing orders keep
rendering the values the customer actually bought.
## Example exchange
Request:
```http
GET /api/orders/ord_01HSX9Z0K3R7 HTTP/1.1
```
Response:
```json
{
"id": "ord_01HSX9Z0K3R7",
"number": "KN-2026-0142",
"status": "paid",
"paymentStatus": "paid",
"fulfilmentStatus": "processing",
"createdAt": "2026-04-23T09:14:02.000Z",
"paidAt": "2026-04-23T09:14:47.000Z",
"items": [
{
"productId": "kaiser-natron-pulver-250-g-grosspackung",
"title": "Kaiser-Natron® Pulver",
"size": "250 g Großpackung",
"quantity": 2,
"unitPrice": 4.49,
"lineTotal": 8.98
}
],
"subtotal": 8.98,
"shipping": 4.90,
"tax": 1.70,
"total": 15.58,
"currency": "EUR",
"shippingAddress": { "...": "see checkout.md Address" },
"billingAddress": { "...": "see checkout.md Address" },
"email": "ada@example.com"
}
```
## Errors
```json
{ "error": { "code": "order.notFound", "message": "Unknown orderId." } }
```
Known codes:
| Code | When |
| ------------------ | ------------------------------------------------------------------ |
| `order.notFound` | The order ID does not exist or is not visible to the session. |
| `order.forbidden` | The order exists but belongs to a different customer. |
HTTP status codes:
- `200 OK` on success.
- `401 Unauthorized` if authentication is required and absent.
- `403 Forbidden` for `order.forbidden`.
- `404 Not Found` for `order.notFound`. The backend MAY return `404` for
`order.forbidden` as well to avoid leaking order IDs.
## Polling after checkout
After Stripe redirects back to `/checkout/return?order=<orderId>`, the
frontend polls `GET /api/orders/:id` with modest backoff (e.g. 1 s / 2 s
/ 4 s, stopping at 15 s) until `paymentStatus !== 'pending'`. If the
webhook is slow, the page shows a "payment processing" state and keeps
polling; it does not mark the order failed on its own.