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>
This commit is contained in:
148
docs/api/orders.md
Normal file
148
docs/api/orders.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user