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

126 lines
5.1 KiB
Markdown

# Checkout & Payments
Checkout is a thin orchestration layer on top of the cart. The frontend
hands off to Stripe for card capture, the backend creates and confirms
the order, and a webhook is the authoritative "paid" signal.
The seam on the frontend side lives in `src/api/checkout.js` (to be
added alongside `cart.js` and `products.js`). Until the backend is
online, that module can stub the calls locally.
## Flow at a glance
```
Browser (Vue) Backend (PHP) Stripe
───────────── ───────────── ──────
1. POST /api/checkout/intent ─────────────► create PaymentIntent
◄───────────── clientSecret
2. stripe.confirmCardPayment(clientSecret) ───────────► charge
◄──────── status
3. Stripe webhook ─────────► payment_intent.succeeded
→ mark order paid, decrement stock
4. GET /api/orders/:id ────────────►
◄──────── Order (status: "paid")
```
The browser never sees raw card data — it posts directly to Stripe
using the `clientSecret`. The backend relies on the webhook, not the
browser, to decide whether an order is paid.
## Endpoints
| Method | Path | Body | Returns |
| ------ | -------------------------- | --------------------- | ---------------------- |
| POST | `/api/checkout/intent` | `CheckoutRequest` | `CheckoutIntent` |
| POST | `/api/checkout/confirm` | `{ orderId }` | `Order` |
| POST | `/api/webhooks/stripe` | Stripe event (signed) | `200 OK` (server-only) |
- `/api/checkout/intent` takes the cart plus customer + shipping info,
creates a pending `Order` row, creates a Stripe PaymentIntent, and
returns the `clientSecret` for the browser to confirm.
- `/api/checkout/confirm` is optional: it lets the frontend poll once
after Stripe's client-side confirmation to get the final `Order`
shape, without waiting for the webhook.
- `/api/webhooks/stripe` is called by Stripe, not the browser. It
verifies the signature and is the only source of truth for
`status: "paid"`.
## Types
```ts
interface Address {
name: string
company?: string
street: string
postalCode: string
city: string
country: string // ISO 3166-1 alpha-2
phone?: string
}
interface CheckoutRequest {
email: string
shippingAddress: Address
billingAddress?: Address // defaults to shippingAddress
acceptsMarketing: boolean
}
interface CheckoutIntent {
orderId: string // the Order row, status: "pending"
clientSecret: string // passed to stripe.confirmCardPayment
publishableKey: string // Stripe publishable key (pk_live_… / pk_test_…)
amount: number // EUR, informational — Stripe is authoritative
currency: "eur"
}
interface Order {
id: string
status: "pending" | "paid" | "failed" | "refunded" | "cancelled"
items: CartLine[] // snapshot at checkout, not live cart
subtotal: number
shipping: number
tax: number
total: number
currency: "eur"
customer: { email: string; customerId?: string }
shippingAddress: Address
billingAddress: Address
createdAt: string // ISO-8601 UTC
paidAt?: string
}
```
## Stripe integration notes
- Use the **Payment Element** (not the deprecated Card Element). It
supports SEPA + Apple/Google Pay with no extra frontend work.
- Publishable key is returned per-request so staging and production
can use separate Stripe accounts without a frontend rebuild.
- The webhook endpoint must verify `Stripe-Signature` against the
endpoint secret. Reject unsigned or stale events.
- Idempotency: the backend must treat the webhook as idempotent — the
same `payment_intent.succeeded` can arrive more than once.
## Errors
```json
{ "error": { "code": "checkout.cartEmpty", "message": "Cart is empty." } }
```
| Code | When |
| -------------------------- | -------------------------------------------------- |
| `checkout.cartEmpty` | No items in the session cart at intent creation. |
| `checkout.addressInvalid` | Shipping or billing address fails validation. |
| `checkout.stockChanged` | A line is out of stock since the cart was built. |
| `checkout.priceChanged` | Catalog price drifted from the cart snapshot. |
| `payment.declined` | Stripe reported a final decline. |
| `payment.authRequired` | 3-D Secure / SCA step pending — retry confirm. |
HTTP status:
- `200 OK` on success.
- `400 Bad Request` for validation errors.
- `409 Conflict` for `stockChanged` / `priceChanged` — the client
refreshes the cart and retries.
- `402 Payment Required` for `payment.declined`.