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>
149 lines
4.6 KiB
Markdown
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.
|