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>
5.1 KiB
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/intenttakes the cart plus customer + shipping info, creates a pendingOrderrow, creates a Stripe PaymentIntent, and returns theclientSecretfor the browser to confirm./api/checkout/confirmis optional: it lets the frontend poll once after Stripe's client-side confirmation to get the finalOrdershape, without waiting for the webhook./api/webhooks/stripeis called by Stripe, not the browser. It verifies the signature and is the only source of truth forstatus: "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-Signatureagainst the endpoint secret. Reject unsigned or stale events. - Idempotency: the backend must treat the webhook as idempotent — the
same
payment_intent.succeededcan 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 OKon success.400 Bad Requestfor validation errors.409 ConflictforstockChanged/priceChanged— the client refreshes the cart and retries.402 Payment Requiredforpayment.declined.