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>
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
# 🛒 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.*
|
||||
Reference in New Issue
Block a user