Files
pak-jabit/docs/napkin-storefront.md
juxtapoandClaude Opus 5 a58a61b98c Pak Jabit: full stack — storefront, Spree customisations, catalogue tooling
A South African men's apparel store on Spree 5.6 + Next.js 15. 160 products
live, three markets, live Malaysian courier rates, customer-owned carts.

What is here:
  storefront/  the Next.js app
  backend/     ONLY the custom Spree code. Spree itself is a prebuilt image
               that mounts no app code, so every file here needs an explicit
               volume entry in docker-compose.yml or it is silently ignored.
  catalogue/   the scripts that loaded and maintain the products
  docs/        manifest, mock registry, napkins

Real .env files are committed deliberately. This lives on the house's private
Gitea, and a clone that runs beats a clone that needs a secrets handover.
LANDMINE.md maps every key, what it opens, and what must be rotated before this
repo goes anywhere less private.

The admin API key used to be a literal in six catalogue scripts. They now read
SPREE_ADMIN_KEY from the environment and refuse to run without it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 22:09:55 +08:00

112 lines
5.0 KiB
Markdown

# 🛒 Storefront — Napkin Notes
Ideas and landmines for the shopper-facing side. Raised by abang 2026-09-15,
after the night we wired the region anchor and put the cart on the customer.
---
## 1. Currency chosen after the total, not before 💡
**The idea:** let someone shop, reach the total, and *then* pick what currency to
pay in. More natural than being asked up front, before they know what anything
costs.
**Why it's hard as things stand:** Spree fixes a cart's currency **and** market at
creation and neither can be changed afterwards. That is exactly why the anchor
had to come first — see `switchCurrency` in `CartProvider`, which cannot convert
a cart and instead builds a new one and re-adds every line.
So "choose at the end" is really "rebuild the basket at the end." That already
works (the switcher does it), it just gets more expensive the fuller the basket,
and the totals move under the customer at the worst moment.
**Worth trying:** show every currency's total on the cart/checkout page as a
read-only comparison, and only rebuild when they commit. They get the "see it
first" benefit without a silent re-price. The price rows already exist — all 160
products carry ZAR, USD and MYR.
---
## 2. 🔴 LANDMINE — one person, addresses in several countries
**The case:** a customer lives in South Africa, has an office in Kuala Lumpur,
and family in the US. Perfectly ordinary. Our model does not survive it.
**The mechanism.** The region anchor ties three things into one choice, because a
Spree market *is* all three:
```
currency -> market -> shippable countries -> shipping methods
```
So a customer anchored to ZAR can only deliver to ZA. To send something to their
KL office they must switch to MYR, which **rebuilds the cart and re-prices the
whole basket** — even though the only thing they wanted to change was where the
parcel goes.
Worse, it is silent about the real reason. They'd read it as "this shop won't
deliver to my office," not "delivery country is welded to display currency."
**What is actually separable:** currency and delivery country are independent in
principle — you can price in ZAR and ship to Malaysia. What welds them here is
that shipping methods are per-zone AND their calculators are per-currency (see
`MOCKS.md`, the bug that killed Malaysian checkout). A ZAR order finds no rate in
the Malaysia zone because every method there quotes in MYR.
**Possible shapes, none built:**
- **Delivery country separate from currency.** Give each zone a method in every
supported currency. A ZAR order shipping to MY charges a ZAR rate. Most honest
to how people actually shop; most rows to maintain.
- **Ask on the address, not in the header.** The customer picks a delivery
address first and the store says plainly: *"Delivering to Malaysia switches
your prices to MYR."* Same restriction, but named instead of hidden.
- **Single market, international rates.** Drop the multi-market idea, price
everything in ZAR, treat non-SA as international shipping. Simplest by far, and
it matches the actual NSH brief — a South African store.
⚠️ Decide this before more countries get added, not after. Every product carries
three price rows already; that is three times the pricing surface to keep honest
for a market nobody has confirmed we sell to.
---
## 3. Dark / light mode selection
The storefront is dark-only right now (`#0E0D0B` ground, `#EDE6DA` text,
`#C8873A` ochre). Abang shops in Brave with a dark setup, so dark stays the
default — this is about offering the choice, not flipping it.
- Respect `prefers-color-scheme` on first visit, then let an explicit toggle win
- The palette is hardcoded as hex literals all through the components. A light
theme means lifting those into CSS custom properties first — that refactor is
most of the work, the theming itself is small after it
- Store the choice next to the region anchor: localStorage for guests,
`public_metadata` for signed-in customers (the plumbing for that exists now)
---
## 4. Admin — stock and restock, actually tested
Never exercised. Known state:
- Two stock locations: `Default` (no country) and `Shop location` (US), both
active, both `propagate_all_variants = false`
- 161 products, every one a single variant — the S/M/L/XL buttons on the product
page are decorative, there is no `Spree::Variant` behind them
- No restock flow has been run, and no out-of-stock path has been seen end to end
**Worth testing, in order:**
1. Set a variant to 0 on hand → does the storefront hide it, or let it be added
to a cart and fail at checkout?
2. Backorderable on/off — which one do we want for Lisa's stock?
3. Restock through admin → does the storefront pick it up, and how fast?
4. Two locations with nothing propagated: which one is a new product stocked in?
This is the likeliest silent trap.
Variants matter here too: real S/M/L/XL means per-size stock, which is the whole
point of restocking apparel. Right now "medium out of stock" cannot be expressed.
---
*Started 2026-09-15 from abang's list. Add to it rather than remembering.*