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

5.1 KiB

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

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

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