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

4.6 KiB

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

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:

GET /api/orders/ord_01HSX9Z0K3R7 HTTP/1.1

Response:

{
  "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

{ "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.