Add real order persistence, customer accounts, and cart sync
Checkout now persists orders server-side (Payload orders collection, re-priced from live product data, discount codes redeemed exactly once) instead of writing a client-only sessionStorage snapshot. Buying requires an account (registration inline in checkout, no separate step) — accounts get order history with delivery status, profile/address editing, password change, and a cart that syncs across devices while logged in. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,8 +12,12 @@ specific to this project.
|
||||
image — see `AGENTS.md` before touching anything version-specific, this
|
||||
Next.js release differs from older training-data conventions)
|
||||
- **React 19.2.4**, **Tailwind CSS 4**, **Motion** for animation
|
||||
- No local database, no auth — all editable content comes from the shared
|
||||
Payload CMS at `payload.mk360.de` (see below)
|
||||
- No local database — all editable content (and now orders/customer
|
||||
accounts) lives in the shared Payload CMS at `payload.mk360.de` (see
|
||||
below). Customer auth is Payload's own (a second, separate `auth: true`
|
||||
collection there, `customers` — not this app's own user store), bridged
|
||||
via an httpOnly session cookie this app mints itself; see "Orders &
|
||||
customer accounts" below.
|
||||
|
||||
## Getting started
|
||||
|
||||
@@ -34,9 +38,11 @@ the Payload backend). `NEXT_PUBLIC_PAYLOAD_URL` (optional, defaults to the
|
||||
same `https://payload.mk360.de` — only needed if the client-side Live
|
||||
Preview components should ever point somewhere else). `DISCOUNT_SERVICE_SECRET`
|
||||
(no safe default — required for discount codes to validate/redeem at all;
|
||||
must match the value set on the Payload backend). Set in Coolify's app
|
||||
settings for production, not in a committed `.env` — this app has no other
|
||||
secrets.
|
||||
must match the value set on the Payload backend). `ORDER_SERVICE_SECRET`
|
||||
(no safe default — required for `/api/checkout` to persist an order in
|
||||
Payload at all; must match the value set on the Payload backend). Set in
|
||||
Coolify's app settings for production, not in a committed `.env` — this app
|
||||
has no other secrets.
|
||||
|
||||
## Pages
|
||||
|
||||
@@ -48,6 +54,9 @@ secrets.
|
||||
| `/cart` | Cart (localStorage-backed, see below) |
|
||||
| `/checkout` | Shipping + payment method selection, order summary |
|
||||
| `/bestellbestaetigung` | Order confirmation — reads the one-time snapshot `/checkout` wrote |
|
||||
| `/konto/login` | Customer login |
|
||||
| `/konto/bestellungen`, `/konto/bestellungen/[orderNumber]` | Order history + order detail (own orders only) |
|
||||
| `/konto/profil` | Profile/address editing + password change |
|
||||
| `/challenge` | "Mini-Challenge" tool |
|
||||
| `/todo-cards` | "Todo-Karten" tool |
|
||||
| `/newsletter` | Newsletter signup |
|
||||
@@ -182,24 +191,77 @@ check against Payload's public API, unlike most content on this site.
|
||||
atomic against a true concurrent race on a capped code's very last
|
||||
redemption — not worth custom atomic SQL at this shop's traffic level.
|
||||
|
||||
## Cart & checkout — demo status
|
||||
## Cart & checkout
|
||||
|
||||
- **Cart** (`app/lib/cart.ts`) is entirely client-side, stored in
|
||||
`localStorage` under `ep_cart`, keyed by each product's `slug`.
|
||||
`localStorage` under `ep_cart`, keyed by each product's `slug`. Still the
|
||||
source of truth while browsing — the server-side mirror (see below) only
|
||||
exists to carry a logged-in customer's cart across devices/browsers.
|
||||
- **`app/cart/components/RelatedProducts.tsx`** only ever suggests products
|
||||
not already in the cart — it stopped falling back to re-suggesting an
|
||||
already-in-cart product just to pad the grid out to 3 cards, so with a
|
||||
small catalog it can render fewer cards (down to 1, centered in the
|
||||
12-column grid) rather than recommending something already added.
|
||||
- **Checkout has no real backend for the *order* itself.** `/checkout`'s
|
||||
"Jetzt kaufen" click writes a one-time snapshot (chosen shipping/payment
|
||||
method, cart contents, applied discount) to `sessionStorage`
|
||||
(`app/lib/order.ts`), which `/bestellbestaetigung` reads once and
|
||||
displays — that snapshot *is* the order record. There is no payment
|
||||
processing, no persisted order in Payload or anywhere else, and no
|
||||
confirmation email yet — discount-code validation/redemption is the one
|
||||
part of this flow with real server-side enforcement today (see above).
|
||||
Treat the rest as a frontend/demo checkout flow, not a functioning store.
|
||||
- **`/checkout`'s "Jetzt kaufen" always goes through
|
||||
`POST /api/checkout`.** That route re-prices the entire cart server-side
|
||||
from Payload's live product data (never trusts client-submitted prices),
|
||||
re-validates+redeems a discount code exactly once, registers a new
|
||||
account inline if nobody's logged in yet ("Konto Pflicht" — see below),
|
||||
and only then creates the order in Payload's `orders` collection via
|
||||
`app/lib/orderServer.ts`. `app/lib/order.ts`'s `OrderSnapshot` is still
|
||||
written to `sessionStorage` for `/bestellbestaetigung` to read once, but
|
||||
its `orderNumber`/`orderDateIso` now come back from that Payload create
|
||||
call, not generated client-side.
|
||||
- Still not built: real payment processing (the checkout button is
|
||||
labelled "zahlungspflichtig" but nothing captures a payment) and a
|
||||
transactional confirmation email — see `project_backend_checkout_plan`
|
||||
in the assistant's own memory for what's deliberately deferred to a
|
||||
later stage.
|
||||
|
||||
## Orders & customer accounts
|
||||
|
||||
An account is required to buy — there is no guest checkout. Registration
|
||||
happens inline in `/checkout`'s "1. Rechnungsadresse" card (a password
|
||||
field appears there when nobody's logged in); returning customers can
|
||||
instead expand a small "Schon Kundin? Einloggen" toggle in the same place
|
||||
without leaving the page.
|
||||
|
||||
- **`app/lib/customerAuth.ts`** (server-only) is the single place that
|
||||
talks to Payload's `customers` collection — a second, fully separate
|
||||
`auth: true` collection from any admin login, existing purely for this
|
||||
storefront's own accounts. Payload issues a JWT on register/login; this
|
||||
app never relies on Payload's own auth cookie (different origin —
|
||||
`einfach-produktiv.mk360.de` vs `payload.mk360.de`) and instead mints its
|
||||
**own** httpOnly `ep_customer_token` cookie holding that JWT, forwarded
|
||||
as an `Authorization: JWT <token>` header on every subsequent Payload
|
||||
call. No token refresh in this stage — Payload's ~2h default JWT
|
||||
lifetime means a session just expires and the customer logs in again.
|
||||
- **`app/api/account/*`** — thin route handlers around `customerAuth.ts`:
|
||||
`register`, `login`, `logout`, `me`, `orders` (list), `profile`
|
||||
(GET/PATCH incl. the one saved default address), `password` (verifies
|
||||
the current password via a real login attempt before changing it,
|
||||
doesn't just trust the caller), `cart` (GET/POST, see below).
|
||||
- **`/konto/bestellungen`** lists a customer's own orders;
|
||||
**`/konto/bestellungen/[orderNumber]`** shows one order's full detail
|
||||
(items, address, totals, `status`). `status` (`received` → `processing`
|
||||
→ `shipped` → `delivered`) is maintained by hand in the Payload admin —
|
||||
no shipping-carrier API integration.
|
||||
- **`/konto/profil`** edits name + the one saved default address (deliberately
|
||||
a single address, not a full address book — see the assistant's memory
|
||||
note on optionally expanding this later) and changes the password.
|
||||
- **Cart sync**: `app/components/CartSync.tsx` (mounted once in
|
||||
`app/layout.tsx`) watches the local cart via `useCart()` and
|
||||
debounce-POSTs it to `/api/account/cart` on every change; the route
|
||||
401s (silently, by design) when nobody's logged in. On login
|
||||
(`LoginForm.tsx`, and `CheckoutContent.tsx`'s inline toggle),
|
||||
`mergeServerCartIntoLocal()` (`app/lib/cart.ts`) folds whatever was
|
||||
saved server-side into the local cart by quantity — CartSync's own
|
||||
effect then pushes the merged result back up on its own, so there's no
|
||||
separate explicit "save after merge" call.
|
||||
- Only a single default address per account, single-currency, no order
|
||||
cancellation/return flow, no email verification, no password-reset
|
||||
(self-service — a customer who forgets their password currently has no
|
||||
recovery path). All known, deliberately out of scope for now.
|
||||
|
||||
## Deployment
|
||||
|
||||
|
||||
Reference in New Issue
Block a user