@base44/app-plugin-commerce 0.1.20 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -22
- package/base44/agents/commerce/StoreAdmin.jsonc +1 -1
- package/base44/entities/commerce.OrderRefund.jsonc +1 -1
- package/base44/entities/commerce.PaymentGateway.jsonc +1 -1
- package/base44/entities/commerce.Webhook.jsonc +1 -1
- package/base44/functions/commerce/admin-products/entry.ts +1 -1
- package/base44/functions/commerce/admin-reports/entry.ts +1 -1
- package/base44/functions/commerce/payments/entry.ts +2 -2
- package/base44/functions/commerce/seed-store/defaults.ts +1 -1
- package/base44/functions/commerce/seed-store/entry.ts +34 -0
- package/base44/functions/commerce/seed-store/seed-catalog.ts +39 -5
- package/base44/functions/commerce/storefront-catalog/entry.ts +1 -1
- package/base44/functions/commerce/storefront-checkout/entry.ts +1 -1
- package/base44/shared/commerce/card-payment.stripe.ts +198 -0
- package/base44/shared/commerce/card-payment.ts +1 -1
- package/base44/shared/commerce/payments.ts +2 -2
- package/base44/shared/commerce/scan.ts +1 -1
- package/base44/shared/commerce/sequence.ts +2 -2
- package/package.json +1 -1
- package/scripts/install.js +24 -14
- package/skills/commerce/SKILL.md +117 -51
- package/skills/commerce/docs/api-admin.md +89 -28
- package/skills/commerce/docs/api-storefront.md +113 -126
- package/skills/commerce/docs/entities.md +137 -0
- package/skills/commerce/install/01-install.md +101 -0
- package/skills/commerce/install/02-storefront.md +444 -0
- package/skills/commerce/install/03-data.md +162 -0
- package/skills/commerce/references/admin-product-form.md +10 -0
- package/skills/commerce/references/catalog-rendering.md +110 -0
- package/skills/commerce/references/emails.md +49 -12
- package/skills/commerce/references/guest-access-security.md +18 -5
- package/skills/commerce/references/online-payments.md +49 -149
- package/skills/commerce/references/operations.md +52 -0
- package/skills/commerce/references/reviews.md +31 -16
- package/skills/commerce/references/shipping-and-tax.md +110 -0
- package/skills/commerce/references/store-admin-agent.md +21 -0
- package/skills/commerce/references/store-settings.md +49 -0
- package/src/commerce/admin/README.md +6 -3
- package/src/commerce/admin/layout/AuthGuard.jsx +1 -1
- package/src/commerce/admin/pages/products/Reviews.jsx +1 -1
- package/src/commerce/admin/pages/settings/InventorySettings.jsx +1 -1
- package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +1 -1
- package/src/commerce/storefront/StorefrontProvider.jsx +106 -20
- package/src/commerce/storefront/index.js +74 -24
- package/src/commerce/storefront/internal/useAsyncData.js +86 -0
- package/src/commerce/storefront/useAddressForm.js +96 -0
- package/src/commerce/storefront/useCartLine.js +221 -0
- package/src/commerce/storefront/useCheckout.jsx +18 -6
- package/src/commerce/storefront/useOrderReturn.js +36 -10
- package/src/commerce/storefront/useProduct.js +295 -0
- package/src/commerce/storefront/useProductGallery.js +74 -0
- package/src/commerce/storefront/useProductList.js +153 -0
- package/src/commerce/storefront/useProductPrice.js +58 -0
- package/src/commerce/storefront/useProductReviews.js +242 -0
- package/src/commerce/storefront/useStorefrontSeo.js +204 -0
- package/src/commerce/storefront/useTotalsLines.js +109 -0
- package/src/commerce/utils/address-spec.js +89 -0
- package/src/commerce/utils/images.js +45 -0
- package/src/commerce/utils/index.js +22 -7
- package/src/commerce/utils/price.js +95 -0
- package/src/commerce/utils/shipping-promos.js +2 -2
- package/src/commerce/utils/specs.js +26 -0
- package/src/commerce/utils/storefront.js +47 -3
- package/src/commerce/utils/totals.js +110 -0
- package/src/commerce/utils/variants.js +58 -3
- package/skills/commerce/installation-guidelines.md +0 -93
- package/skills/commerce/post-installation.md +0 -496
- package/skills/commerce/references/limits-and-performance.md +0 -16
- package/skills/commerce/references/media-and-downloads.md +0 -4
- package/skills/commerce/references/product-render.md +0 -89
- package/skills/commerce/references/scheduled-work.md +0 -19
- package/skills/commerce/references/storefront-product-page.md +0 -83
- package/skills/commerce/references/webhooks.md +0 -10
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
---
|
|
2
|
+
stage: install/02
|
|
3
|
+
read_when: "You are about to build storefront pages (product list/page, cart, checkout, order-received)."
|
|
4
|
+
skip_when: "The storefront pages already render against live data and pass the checklist at the bottom of this file."
|
|
5
|
+
forget_when: "The checklist at the bottom passes — every page renders against the seeded catalog and an offline order completes."
|
|
6
|
+
carry_forward:
|
|
7
|
+
- "Payment gateways, currency and countries come from useStoreInfo() only — never off a cart (cart.payment_gateways is always undefined)."
|
|
8
|
+
- "A store with any coupons must have a coupon field (useCoupon) in the cart or the checkout, or its codes can never be redeemed."
|
|
9
|
+
- "/order-received is mandatory and renders useOrderReturn's states, including paymentInstructions — how a normal (offline) customer learns how to pay."
|
|
10
|
+
- "Branch cart/list/product UI on `status`, never on `isEmpty`/nullable data — `isEmpty` is false while loading by design."
|
|
11
|
+
- "Variant options that aren't buyable render disabled, never hidden; one control per axis, never a list of variations."
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# 02 — Storefront
|
|
15
|
+
|
|
16
|
+
One split decides everything here: **the logic is premade, the UI never is.**
|
|
17
|
+
|
|
18
|
+
- **Logic — hooks, shipped.** Checkout repricing from the address, variant
|
|
19
|
+
resolution, cart state, coupon redemption, review policies, order-return
|
|
20
|
+
verification. Every store's version of these is functionally identical, and
|
|
21
|
+
hand-writing them is where storefront bugs cluster: **never re-implement what
|
|
22
|
+
a hook does.**
|
|
23
|
+
- **UI — yours, always.** Every element, class, layout and word of copy on
|
|
24
|
+
every page. Nothing in `@/commerce/storefront` renders markup or carries CSS,
|
|
25
|
+
and this file deliberately doesn't hand you page bodies either — the design
|
|
26
|
+
is the part of the storefront only you can do, and it should be designed,
|
|
27
|
+
not assembled.
|
|
28
|
+
|
|
29
|
+
Each hook returns a complete view-model — a `status` to branch on,
|
|
30
|
+
ready-to-map arrays, handlers, error objects — and its **doc comment (JSDoc) is
|
|
31
|
+
the API reference**: open the hook's file when you need exact shapes; don't
|
|
32
|
+
guess fields. This file gives you the routing, each surface's hook, and the
|
|
33
|
+
render rules that keep a store correct (marked ⚑ — these must survive whatever
|
|
34
|
+
design you build).
|
|
35
|
+
|
|
36
|
+
Where you get a **reference implementation** and where you get only the hook is
|
|
37
|
+
deliberate: **cart, checkout and order-received** have reference code below —
|
|
38
|
+
their wiring is dense enough that reading it is cheaper than deriving it, and
|
|
39
|
+
they are conventions (a form, a receipt) where familiarity beats invention.
|
|
40
|
+
The **identity surfaces** — home, collection, the card, the product page's
|
|
41
|
+
layout — get hooks only, on purpose: reference markup there would make every
|
|
42
|
+
store look the same, and their design is the work only you can do. Either way
|
|
43
|
+
the hooks are high-level enough that a page is a handful of calls plus your
|
|
44
|
+
markup — writing more code than the budgets at the bottom allow means you are
|
|
45
|
+
re-deriving logic a hook already owns. Everything imports from
|
|
46
|
+
`@/commerce/storefront`.
|
|
47
|
+
|
|
48
|
+
## Setup — once
|
|
49
|
+
|
|
50
|
+
```jsx
|
|
51
|
+
import { StorefrontProvider } from "@/commerce/storefront";
|
|
52
|
+
import { base44 } from "@/api/base44Client";
|
|
53
|
+
|
|
54
|
+
<BrowserRouter>
|
|
55
|
+
<StorefrontProvider base44={base44}> {/* wraps <Routes> — never a child of it */}
|
|
56
|
+
<Routes> {/* ONE <Routes> — merge new pages into the app's */}
|
|
57
|
+
<Route path="/" element={<Home />} />
|
|
58
|
+
<Route path="/product/:slug" element={<ProductPage />} />
|
|
59
|
+
<Route path="/bag" element={<Bag />} />
|
|
60
|
+
<Route path="/checkout" element={<Checkout />} />
|
|
61
|
+
<Route path="/order-received" element={<OrderReceived />} />
|
|
62
|
+
<Route path="/store-admin/*" element={<AdminApp />} />
|
|
63
|
+
</Routes>
|
|
64
|
+
</StorefrontProvider>
|
|
65
|
+
</BrowserRouter>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The provider owns the shared client, the store-info cache and **one** shared
|
|
69
|
+
cart, so a header badge, a drawer and the checkout render the same state. Never
|
|
70
|
+
mount a second provider, and never touch the `cart_token` — the provider owns
|
|
71
|
+
its whole lifecycle.
|
|
72
|
+
|
|
73
|
+
> ⚠ **`<Routes>` accepts only `<Route>` children.** Nesting the provider inside
|
|
74
|
+
> it — the natural reading of "wrap the storefront routes" — throws at render:
|
|
75
|
+
> `Error: [StorefrontProvider] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>`.
|
|
76
|
+
> To scope the provider to storefront routes only, use a pathless **layout
|
|
77
|
+
> route**, the one place a wrapper is legal:
|
|
78
|
+
> ```jsx
|
|
79
|
+
> <Route element={<StorefrontProvider base44={base44}><Outlet /></StorefrontProvider>}>
|
|
80
|
+
> <Route path="/" element={<Home />} />
|
|
81
|
+
> <Route path="/checkout" element={<Checkout />} />
|
|
82
|
+
> </Route>
|
|
83
|
+
> <Route path="/store-admin/*" element={<AdminApp />} /> {/* outside the provider */}
|
|
84
|
+
> ```
|
|
85
|
+
|
|
86
|
+
## Product list / collection
|
|
87
|
+
|
|
88
|
+
`useProductList(params)` → `{ status, products, hasNext, next, refreshing,
|
|
89
|
+
setParams, reload }`. ⚑ `status` is `"loading" | "ready" | "empty" | "error"`
|
|
90
|
+
— branch on it, so a failed request renders as a failure instead of an empty
|
|
91
|
+
grid. `setParams({ category_id, search, on_sale, min_price, in_stock_only })`
|
|
92
|
+
resets to page 1 and keeps the current rows on screen (`refreshing`) while the
|
|
93
|
+
page loads. `useCategories()` / `useRibbons()` → `{ items }` (arrays, children
|
|
94
|
+
nested).
|
|
95
|
+
|
|
96
|
+
Your card renders `name`, `images[0]?.src` (⚑ **images are `{src,name,alt}`
|
|
97
|
+
objects and the array may be empty — render a placeholder, never a broken
|
|
98
|
+
`<img>`**), `useProductPrice(row).label` (already "From €19.99" when the
|
|
99
|
+
product sells variants — there is no product `type` flag), `on_sale`,
|
|
100
|
+
`short_description`, `stock_status`, `average_rating`/`rating_count`,
|
|
101
|
+
`ribbons`. Full field matrix:
|
|
102
|
+
[`../references/catalog-rendering.md`](../references/catalog-rendering.md).
|
|
103
|
+
|
|
104
|
+
**Rails** (featured row, "new in") are the same hook with a filter
|
|
105
|
+
(`{ featured: true, per_page: 4 }`). ⚑ Any filter may legitimately match
|
|
106
|
+
nothing — render *nothing* then, never a heading over an empty row. Upsells
|
|
107
|
+
beside a product need no query: `p.upsells` / `p.crossSells` from `useProduct`.
|
|
108
|
+
|
|
109
|
+
## Product page
|
|
110
|
+
|
|
111
|
+
`useProduct(slug)` (or `{ id }`) owns fetch + variant selection + quantity +
|
|
112
|
+
price, race-safe, selection mirrored to the URL. ⚑ `status: "not_found"` is a
|
|
113
|
+
404 page, not a spinner. Destructure `{ product, view, price, categories }`
|
|
114
|
+
and build your layout from:
|
|
115
|
+
|
|
116
|
+
- **Price** — `price.label`, plus `price.compareAtLabel` (struck through) when
|
|
117
|
+
on sale. Never read `product.price` directly — the parent's price is a
|
|
118
|
+
rolled-up from-price.
|
|
119
|
+
- **Gallery** — `useProductGallery(product, view)` → `{ hasImages, images,
|
|
120
|
+
active, activeIndex, setActiveIndex, next, prev }`. The active image already
|
|
121
|
+
follows the variant selection; `hasImages: false` means render your
|
|
122
|
+
placeholder.
|
|
123
|
+
- **Variant selector** — `variantAxes(view, p.pick)` → one entry per axis:
|
|
124
|
+
`{ key, name, selectedOption, options: [{ value, selected, disabled,
|
|
125
|
+
outOfStock, pick }] }`. Map it to any control — buttons, swatches, a dropdown.
|
|
126
|
+
⚑ **One control per axis, never a list of variations** (`Red / S`, `Red / M`,
|
|
127
|
+
… is n × m noise), and ⚑ **an unbuyable option renders `disabled`, never
|
|
128
|
+
hidden** (`outOfStock` stays visible, just marked) — a customer who can't see
|
|
129
|
+
that a size exists assumes the store doesn't carry it. `view.missingAxes`
|
|
130
|
+
names what's still unpicked. The shape of the map (the one interaction agents
|
|
131
|
+
reliably get wrong — the control itself is yours):
|
|
132
|
+
|
|
133
|
+
```jsx
|
|
134
|
+
{variantAxes(view, p.pick).map((axis) => (
|
|
135
|
+
<fieldset key={axis.key}>{/* label from axis.name / axis.selectedOption */}
|
|
136
|
+
{axis.options.map((o) => (
|
|
137
|
+
<button key={o.value} disabled={o.disabled} aria-pressed={o.selected} onClick={o.pick}>
|
|
138
|
+
{o.value}{/* o.outOfStock → mark visibly, keep clickable-looking off */}
|
|
139
|
+
</button>
|
|
140
|
+
))}
|
|
141
|
+
</fieldset>
|
|
142
|
+
))}
|
|
143
|
+
```
|
|
144
|
+
- **Buy box** — `useAddToCartButton(p, { onAdded })` → `{ add, adding, error,
|
|
145
|
+
disabled, soldOut, needsSelection, quantity, increase, decrease, canIncrease,
|
|
146
|
+
canDecrease, showQuantity }`. It gates on purchasability, recovers from every
|
|
147
|
+
add failure and clamps quantity to stock and `sold_individually`. ⚑ Render
|
|
148
|
+
`error.message` inline; ⚑ `showQuantity: false` means no stepper (only 1 can
|
|
149
|
+
be bought); the button label should reflect `adding`/`soldOut`/
|
|
150
|
+
`needsSelection` — the words are yours.
|
|
151
|
+
- **Description** — `product.description` is HTML; render as rich text
|
|
152
|
+
(`dangerouslySetInnerHTML`), `short_description` above it.
|
|
153
|
+
- **Specs** — `productSpecs(product)` → `[{ key, label, value }]` from
|
|
154
|
+
`meta_data` (Material, Care). `[]` means no section at all.
|
|
155
|
+
- **Breadcrumbs** — build from `categories`
|
|
156
|
+
(`/collection?category_id=${c.id}`); skip on a flat catalog. Ribbons are
|
|
157
|
+
labels, not breadcrumbs.
|
|
158
|
+
|
|
159
|
+
All optional — include what this store's products actually have.
|
|
160
|
+
|
|
161
|
+
### Reviews — optional
|
|
162
|
+
|
|
163
|
+
**Build reviews only if the store wants them** — because the brief asks, or the
|
|
164
|
+
products are the kind customers rate. No review UI means no reviews, and that
|
|
165
|
+
is a complete outcome. (If you skip them, don't put star ratings on cards
|
|
166
|
+
either — an average of nothing is `0`.)
|
|
167
|
+
|
|
168
|
+
`useProductReviews(product, { policy, user })` is the whole surface: `items`,
|
|
169
|
+
paging (`hasNext`/`loadMore`), `averageRating`/`ratingCount`, and the submit
|
|
170
|
+
form — `form`/`setField`/`fieldErrors` (matching the server's error codes),
|
|
171
|
+
`valid`, `submit`, `requiresEmail` (false for a signed-in visitor),
|
|
172
|
+
`reviewBlockedReason` (`"login_required"` / `"not_a_buyer"` under the stricter
|
|
173
|
+
policies). ⚑ The confirmation copy is `message`, **taken from the server's
|
|
174
|
+
response** — a store with auto-approval on says "published", not "awaiting
|
|
175
|
+
approval", so render `message`, never your own text. `policy` is
|
|
176
|
+
`"open" | "login" | "verified_buyers"`. Details beyond this:
|
|
177
|
+
[`../references/reviews.md`](../references/reviews.md).
|
|
178
|
+
|
|
179
|
+
## Cart / bag
|
|
180
|
+
|
|
181
|
+
A cart *page* is optional: a store selling one made-to-order piece reads better
|
|
182
|
+
as buy-now straight to checkout. The surface is four hooks: `useCart()`
|
|
183
|
+
(`status`, `lines`, `notices`), `CartLine` (headless render-prop binding
|
|
184
|
+
`useCartLine` per row — quantity stepping that clamps, coalesces and recovers),
|
|
185
|
+
`useTotalsLines()`, `useCoupon()`.
|
|
186
|
+
|
|
187
|
+
⚑ Rules: branch on `status`, never on emptiness while loading. Render
|
|
188
|
+
`notices` — they say what auto-dropped from the cart and why. Render every
|
|
189
|
+
non-`hidden` totals line rather than hardcoding subtotal/total — a hand-written
|
|
190
|
+
summary omits discount and tax, then stops adding up the day a coupon or a tax
|
|
191
|
+
rate exists. **A store with any coupons must have a coupon field** (here or in
|
|
192
|
+
the checkout): coupons are admin-only data, redeemable only through a field the
|
|
193
|
+
customer types into — if no field exists anywhere, don't seed coupons and don't
|
|
194
|
+
write "use WELCOME10" in the copy.
|
|
195
|
+
|
|
196
|
+
**Reference implementation** — read once for the wiring, then write your own
|
|
197
|
+
page: the structure below is correct, the presentation is deliberately absent.
|
|
198
|
+
Restyle, rearrange, split into your own components; the ⚑ rules are the part
|
|
199
|
+
that must survive.
|
|
200
|
+
|
|
201
|
+
```jsx
|
|
202
|
+
function Bag() {
|
|
203
|
+
const { status, lines, notices } = useCart();
|
|
204
|
+
const totals = useTotalsLines();
|
|
205
|
+
const formatMoney = useFormatMoney();
|
|
206
|
+
if (status === "loading") return /* your loading state */;
|
|
207
|
+
if (status === "empty") return /* your empty-bag state, linking back to the catalog */;
|
|
208
|
+
return (
|
|
209
|
+
<>
|
|
210
|
+
{notices.map((n, i) => <p key={i} role="status">{n.message}</p>)}
|
|
211
|
+
{lines.map((line) => (
|
|
212
|
+
<CartLine key={line.item_key} line={line}>
|
|
213
|
+
{(l) => ( /* line: name, attributesLabel, image, total — l: the controls */
|
|
214
|
+
<li>
|
|
215
|
+
{line.name} {line.attributesLabel}
|
|
216
|
+
<button onClick={l.decrease} disabled={!l.canDecrease || l.pending}>−</button>
|
|
217
|
+
{l.quantity}
|
|
218
|
+
<button onClick={l.increase} disabled={!l.canIncrease || l.pending}>+</button>
|
|
219
|
+
<button onClick={l.remove}>Remove</button>
|
|
220
|
+
{formatMoney(line.total)}
|
|
221
|
+
{l.error && <p role="alert">{l.error.message}</p>}
|
|
222
|
+
</li>
|
|
223
|
+
)}
|
|
224
|
+
</CartLine>
|
|
225
|
+
))}
|
|
226
|
+
<CouponField /> {/* useCoupon: code/setCode, apply, applying, error, applied[] + remove */}
|
|
227
|
+
{totals.filter((l) => !l.hidden).map((l) => (
|
|
228
|
+
<div key={l.key}>{l.label} {l.formatted}</div> /* l.emphasis → the total row */
|
|
229
|
+
))}
|
|
230
|
+
<Link to="/checkout">Checkout</Link>
|
|
231
|
+
</>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
No shipping estimator here — checkout reprices shipping and tax from the
|
|
237
|
+
address.
|
|
238
|
+
|
|
239
|
+
## Checkout
|
|
240
|
+
|
|
241
|
+
The state machine is `useCheckout`, shared across the page's regions by
|
|
242
|
+
`CheckoutProvider` + `useCheckoutContext()`. It reprices shipping/tax from the
|
|
243
|
+
address automatically (debounced, never on a half-typed address), derives the
|
|
244
|
+
shipping and payment choices, gates the button (`canPlaceOrder` +
|
|
245
|
+
`useCheckoutBlockers()` in words), and `placeOrder()` handles **both**
|
|
246
|
+
navigations — online gateway → provider redirect, everything else →
|
|
247
|
+
`/order-received`. The address form comes from `useAddressForm(which)` as a
|
|
248
|
+
field spec (`state` collected, country options never null); the two
|
|
249
|
+
store-data choices come through the headless `ShippingMethodPicker` /
|
|
250
|
+
`PaymentMethodPicker`, whose render props enumerate every branch.
|
|
251
|
+
|
|
252
|
+
⚑ Rules: handle every picker branch (they exist because every one occurs in a
|
|
253
|
+
normal store); a single shipping or payment option still *shows* what it is —
|
|
254
|
+
never a picker of one, never "nothing selected"; zero gateways → say checkout
|
|
255
|
+
is unavailable instead of a dead button; keep each field's `autoComplete` (the
|
|
256
|
+
spec provides it) and render `f.error` — "we don't ship there" arrives on the
|
|
257
|
+
country field; show `orderError.message` and the blockers so the gate explains
|
|
258
|
+
itself. ⚑ Payment methods, currency and countries come from `useStoreInfo()`
|
|
259
|
+
only — `cart.payment_gateways` is always `undefined`, and a default store
|
|
260
|
+
offers `offline` only ([`./03-data.md`](./03-data.md)).
|
|
261
|
+
|
|
262
|
+
**Reference implementation** — the densest wiring in the storefront; read it,
|
|
263
|
+
then build yours around it. Structure correct, presentation absent.
|
|
264
|
+
|
|
265
|
+
```jsx
|
|
266
|
+
function Checkout() { // hooks read the context BELOW the provider
|
|
267
|
+
return <CheckoutProvider><CheckoutForm /></CheckoutProvider>;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function CheckoutForm() {
|
|
271
|
+
const { status } = useCart();
|
|
272
|
+
const checkout = useCheckoutContext();
|
|
273
|
+
const blockers = useCheckoutBlockers();
|
|
274
|
+
const formatMoney = useFormatMoney();
|
|
275
|
+
if (status === "loading") return /* loading */;
|
|
276
|
+
if (status === "empty") return /* "your bag is empty" — a checkout with nothing says so */;
|
|
277
|
+
return (
|
|
278
|
+
<>
|
|
279
|
+
<AddressFields which="billing" />
|
|
280
|
+
<label>
|
|
281
|
+
<input type="checkbox" checked={checkout.shipToDifferent}
|
|
282
|
+
onChange={(e) => checkout.setShipToDifferent(e.target.checked)} />
|
|
283
|
+
Deliver to a different address
|
|
284
|
+
</label>
|
|
285
|
+
{checkout.shipToDifferent && <AddressFields which="shipping" />}
|
|
286
|
+
|
|
287
|
+
<ShippingMethodPicker>
|
|
288
|
+
{({ status, methods, chosen, choose, mustChoose, syncing }) => (
|
|
289
|
+
<fieldset>{/* syncing → subtle busy state; renders null for a virtual cart */}
|
|
290
|
+
{status === "missing_address" && <p>Delivery options appear once your address is entered.</p>}
|
|
291
|
+
{status === "none_available" && <p role="alert">We don't deliver to that address yet.</p>}
|
|
292
|
+
{mustChoose && methods.map((m) => (
|
|
293
|
+
<label key={m.id}>
|
|
294
|
+
<input type="radio" checked={chosen?.id === m.id} onChange={() => choose(m.id)} />
|
|
295
|
+
{m.title} {formatMoney(m.cost)}
|
|
296
|
+
</label>
|
|
297
|
+
))}
|
|
298
|
+
{!mustChoose && chosen && <p>{chosen.title} {formatMoney(chosen.cost)}</p>}
|
|
299
|
+
</fieldset>
|
|
300
|
+
)}
|
|
301
|
+
</ShippingMethodPicker>
|
|
302
|
+
|
|
303
|
+
<PaymentMethodPicker>
|
|
304
|
+
{({ gateways, value, select, selected, single }) => (
|
|
305
|
+
<fieldset>
|
|
306
|
+
{gateways.length === 0 && <p role="alert">No payment method is available right now.</p>}
|
|
307
|
+
{!single && gateways.map((g) => (
|
|
308
|
+
<label key={g.slug}>
|
|
309
|
+
<input type="radio" checked={value === g.slug} onChange={() => select(g.slug)} />
|
|
310
|
+
{g.title} {g.description}
|
|
311
|
+
</label>
|
|
312
|
+
))}
|
|
313
|
+
{single && selected && <p>{selected.title}</p>}
|
|
314
|
+
</fieldset>
|
|
315
|
+
)}
|
|
316
|
+
</PaymentMethodPicker>
|
|
317
|
+
|
|
318
|
+
{/* summary: coupon field (if not in the cart) + useTotalsLines(), as in the cart page */}
|
|
319
|
+
|
|
320
|
+
<button disabled={!checkout.canPlaceOrder || checkout.placing} onClick={() => checkout.placeOrder()}>
|
|
321
|
+
{checkout.placing ? "Placing your order…" : "Place order"}
|
|
322
|
+
</button>
|
|
323
|
+
{checkout.orderError && <p role="alert">{checkout.orderError.message}</p>}
|
|
324
|
+
{!checkout.canPlaceOrder && blockers.map((b) => <p key={b.code}>{b.message}</p>)}
|
|
325
|
+
</>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function AddressFields({ which }) {
|
|
330
|
+
const { fields, set, countriesLoading } = useAddressForm(which);
|
|
331
|
+
return fields.map((f) => (
|
|
332
|
+
<label key={f.key}>
|
|
333
|
+
{f.label}{f.required && " *"}
|
|
334
|
+
{f.type === "select" ? (
|
|
335
|
+
<select value={f.value} onChange={(e) => set(f.key, e.target.value)} autoComplete={f.autoComplete}>
|
|
336
|
+
<option value="">{f.key === "country" && countriesLoading ? "Loading…" : `Select ${f.label}`}</option>
|
|
337
|
+
{f.options.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
|
|
338
|
+
</select>
|
|
339
|
+
) : (
|
|
340
|
+
<input type={f.type} value={f.value} required={f.required}
|
|
341
|
+
onChange={(e) => set(f.key, e.target.value)} autoComplete={f.autoComplete} />
|
|
342
|
+
)}
|
|
343
|
+
{f.error && <span role="alert">{f.error}</span>}
|
|
344
|
+
</label>
|
|
345
|
+
));
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Order received
|
|
350
|
+
|
|
351
|
+
**Mandatory route** — every payment link returns here, and confirming is what
|
|
352
|
+
marks a card order paid. `useOrderReturn()` is the whole page's logic: it reads
|
|
353
|
+
`order_id`/`order_key` from the URL, verifies with the provider (idempotent),
|
|
354
|
+
and marks the page noindex itself.
|
|
355
|
+
|
|
356
|
+
⚑ Rules: render all five states — never a blank page while `"loading"`, and a
|
|
357
|
+
retry via `reload()` on `"error"`. ⚑ **Never drop `paymentInstructions`**: a
|
|
358
|
+
manual/offline order settles outside the store, so these ARE how the store's
|
|
359
|
+
default customer learns how to pay — render them whenever present, on any
|
|
360
|
+
state. ⚑ An order's totals are **flat** — `order.total`; there is no
|
|
361
|
+
`order.totals` (use `useTotalsLines(order)`).
|
|
362
|
+
|
|
363
|
+
**Reference implementation** — a receipt is a convention, not an identity
|
|
364
|
+
surface: take this structure, restyle it to the store, keep every branch.
|
|
365
|
+
|
|
366
|
+
```jsx
|
|
367
|
+
function OrderReceived() {
|
|
368
|
+
const { status, order, lines, paymentLink, paymentInstructions, error, reload } = useOrderReturn();
|
|
369
|
+
const totals = useTotalsLines(order);
|
|
370
|
+
const formatMoney = useFormatMoney();
|
|
371
|
+
if (status === "loading") return /* confirming copy */;
|
|
372
|
+
if (status === "error") return <><p role="alert">{error.message}</p>
|
|
373
|
+
<button onClick={() => reload()}>Try again</button></>;
|
|
374
|
+
return (
|
|
375
|
+
<>
|
|
376
|
+
{status === "paid" && /* thank-you heading */}
|
|
377
|
+
{status === "unpaid" && <>{/* awaiting-payment heading */}
|
|
378
|
+
{paymentLink?.url && <a href={paymentLink.url}>Pay now</a>}</>}
|
|
379
|
+
{status === "cancelled" && <>{/* payment-cancelled heading */}
|
|
380
|
+
{paymentLink?.url && <a href={paymentLink.url}>Try payment again</a>}</>}
|
|
381
|
+
{order?.order_number && <p>Order {order.order_number}</p>}
|
|
382
|
+
{paymentInstructions && (
|
|
383
|
+
<section>{/* "How to pay" — the offline customer's next step */}
|
|
384
|
+
{paymentInstructions.description && <p>{paymentInstructions.description}</p>}
|
|
385
|
+
{paymentInstructions.account_details && Object.entries(paymentInstructions.account_details)
|
|
386
|
+
.map(([k, v]) => <p key={k}>{k.replace(/_/g, " ")}: {String(v)}</p>)}
|
|
387
|
+
</section>
|
|
388
|
+
)}
|
|
389
|
+
{lines.map((l, i) => <p key={i}>{l.name} {l.attributesLabel} × {l.quantity} — {formatMoney(l.total)}</p>)}
|
|
390
|
+
{totals.filter((t) => !t.hidden).map((t) => <p key={t.key}>{t.label} {t.formatted}</p>)}
|
|
391
|
+
</>
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
## SEO — one line per page type
|
|
397
|
+
|
|
398
|
+
```jsx
|
|
399
|
+
useStorefrontSeo(productSeo(p.product, p.view, { storeName, currency })); // product page
|
|
400
|
+
useStorefrontSeo(collectionSeo({ title, products: list.products })); // collection / home
|
|
401
|
+
// order-received is already noindex via useOrderReturn
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
It is a hook — call it above the page's early returns (the builders tolerate a
|
|
405
|
+
null product).
|
|
406
|
+
|
|
407
|
+
## Per-page output budgets
|
|
408
|
+
|
|
409
|
+
| Page | budget (chars) | rationale |
|
|
410
|
+
|---|---|---|
|
|
411
|
+
| Checkout | ≤ 5K | your markup over the reference above — the logic is all hook calls |
|
|
412
|
+
| Cart / bag | ≤ 3K | `useCart` + `CartLine` rows + totals + coupon + empty state |
|
|
413
|
+
| Order-received | ≤ 2.5K | five states + payment instructions + summary |
|
|
414
|
+
| Product page | ≤ 5K | your layout and type around `useProduct`, `variantAxes`, `useAddToCartButton`, the gallery |
|
|
415
|
+
| Collection | ≤ 3K | `useProductList` + custom card + pagination controls |
|
|
416
|
+
| Home | ≤ 5K | pure identity — hero/editorial earn their chars |
|
|
417
|
+
| Any single component file | ≤ 4K, hard ceiling 8K | Base1 evidence: decode is 34% of wall; a 12K file is a 45s write batch |
|
|
418
|
+
|
|
419
|
+
These budgets assume the hooks carry the logic and your markup carries only the
|
|
420
|
+
design. Over budget ⇒ you are re-implementing something a hook does — an
|
|
421
|
+
address spec, a quantity clamp, totals math, variant resolution, add-to-cart
|
|
422
|
+
error recovery. Go back to the hook and delete your version.
|
|
423
|
+
|
|
424
|
+
## Done — forget this file
|
|
425
|
+
|
|
426
|
+
- [ ] Catalog UI exists in whatever form fits the store (list, product pages, or both), plus a checkout, plus `/order-received`.
|
|
427
|
+
- [ ] **One** `<StorefrontProvider>` above every storefront route, wrapping `<Routes>` (or a layout route's `<Outlet/>`); one client, no hand-rolled `cart_token`.
|
|
428
|
+
- [ ] Pages branch on `status`; no page maps a possibly-null list or shows an empty state while loading.
|
|
429
|
+
- [ ] Gateways/currency/countries read from `useStoreInfo()` only.
|
|
430
|
+
- [ ] If the store has coupons, a coupon field (`useCoupon`) exists in the cart or the checkout.
|
|
431
|
+
- [ ] `/order-received` renders `useOrderReturn`'s states **including `paymentInstructions`**.
|
|
432
|
+
- [ ] Variant options render one control per axis; unbuyable options are disabled, not hidden.
|
|
433
|
+
- [ ] No hook logic was re-implemented by hand (address fields, quantity clamps, totals rows, shipping/payment branching).
|
|
434
|
+
- [ ] The storefront looks designed — the reference implementations above were adapted into this store's design, not shipped bare.
|
|
435
|
+
- [ ] Every page is within its budget above.
|
|
436
|
+
- [ ] A real purchase completes in the preview — pick a variant, add it, check out, place an offline order, land on `/order-received`.
|
|
437
|
+
|
|
438
|
+
Record these lines in your working notes; do not re-read this file.
|
|
439
|
+
|
|
440
|
+
- Payment gateways, currency and countries come from `useStoreInfo()` only — never off a cart (`cart.payment_gateways` is always undefined).
|
|
441
|
+
- A store with any coupons must have a coupon field (`useCoupon`) in the cart or the checkout, or its codes can never be redeemed.
|
|
442
|
+
- `/order-received` is mandatory and renders `useOrderReturn`'s states, including `paymentInstructions` — how a normal (offline) customer learns how to pay.
|
|
443
|
+
- Branch cart/list/product UI on `status`, never on `isEmpty`/nullable data — `isEmpty` is false while loading by design.
|
|
444
|
+
- Variant options that aren't buyable render disabled, never hidden; one control per axis, never a list of variations.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
---
|
|
2
|
+
stage: install/03
|
|
3
|
+
read_when: "You are about to build the seed payload — the store's catalog, shipping, coupons, currency — or deciding about payments."
|
|
4
|
+
skip_when: "commerce/seed-store has already returned success for this store and payments are decided."
|
|
5
|
+
forget_when: "The seed response is received and recorded (slugs + warnings), and the checklist at the bottom passes."
|
|
6
|
+
carry_forward:
|
|
7
|
+
- "Product slugs come from the seed response's catalog.products[] — link pages by slug, never by a client-side map."
|
|
8
|
+
- "Payments: report at handover where they landed (default = offline on, card off) — the owner must never learn it from a customer."
|
|
9
|
+
- "Turning card payments on or off later is one more seed call: { payment_methods: [\"offline\", \"card\"] }."
|
|
10
|
+
- "Seed-time `locations` is THE shipping path; patching commerce.ShippingTaxLocation is the day-2 route."
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# 03 — Store data
|
|
14
|
+
|
|
15
|
+
A fresh install has **no settings and no catalog**. One admin-only, idempotent call to `commerce/seed-store` creates both: the six settings groups (`general`, `products`, `inventory`, `tax`, `shipping`, `emails`), the two gateway rows (`offline` enabled, `card` disabled), and — from the payload — the catalog, coupons and shipping locations. Nothing in [`./02-storefront.md`](./02-storefront.md) waits on it: fire it when the image URLs are back and pick the response up when you need the slugs.
|
|
16
|
+
|
|
17
|
+
| Mode | Body | Products created |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| **Real catalog** | `{ store_name, products: [...] }` (+ optional `coupons`, `locations`) | yours — categories, ribbons, attributes, variants, all in this one call |
|
|
20
|
+
| **Demo data** | `{ store_name, with_sample_data: true }` | 10 generic demo products (skipped if any product exists) |
|
|
21
|
+
| **No products** | `{ store_name }` | none — defaults only |
|
|
22
|
+
|
|
23
|
+
`with_sample_data` cannot be combined with `products` (**400** `invalid_payload`). Never calling the function leaves the operator the admin's first-run "Set up your store" screen.
|
|
24
|
+
|
|
25
|
+
**`store_name` is required on a first seed** (**400** `store_name_required`) — the app's name as the platform shows it (`base44/config.jsonc` → `name` can be stale; ask if unsure). It becomes the email sender name and the public shop name. **`currency`** is an ISO code (`"EUR"`); prices are *formatted* per the viewer's locale, so there is nothing else to set. Explicit values always win, first seed and re-runs alike.
|
|
26
|
+
|
|
27
|
+
Full field contract — every payload key, every response field, all error codes: [`../docs/api-admin.md`](../docs/api-admin.md#commerceseed-store). Here is the working call.
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
try {
|
|
31
|
+
const res = await base44.functions.invoke("commerce/seed-store", {
|
|
32
|
+
store_name: "Aurora Threads",
|
|
33
|
+
currency: "EUR",
|
|
34
|
+
products: [
|
|
35
|
+
{ name: "Classic T-Shirt",
|
|
36
|
+
sku: "TEE-CLASSIC", // optional; makes re-runs idempotent
|
|
37
|
+
regular_price: 19.99,
|
|
38
|
+
stock_quantity: 50, // implies manage_stock
|
|
39
|
+
categories: ["Clothing"], // get-or-created by display name
|
|
40
|
+
ribbons: ["Best Seller"],
|
|
41
|
+
images: ["https://…/tee.jpg"], // URLs or { src, alt } — see Images below
|
|
42
|
+
short_description: "A soft, breathable everyday tee.",
|
|
43
|
+
description: "<p>Cut from combed cotton…</p>", // HTML, rendered as rich text
|
|
44
|
+
},
|
|
45
|
+
{ name: "Runner Sneaker",
|
|
46
|
+
sku: "SNK-RUN",
|
|
47
|
+
regular_price: 89, // inherited by variations that don't override
|
|
48
|
+
attributes: [ // the axes → one selector each in the storefront
|
|
49
|
+
{ name: "Size", options: ["41", "42"] },
|
|
50
|
+
{ name: "Color", options: ["Black", "White"] },
|
|
51
|
+
],
|
|
52
|
+
default_options: { Size: "42", Color: "Black" },
|
|
53
|
+
variations: [ // omit entirely → all 4 combos auto-generated
|
|
54
|
+
{ options: { Size: "41", Color: "Black" }, stock_quantity: 4 },
|
|
55
|
+
{ options: { Size: "42", Color: "White" }, regular_price: 94, stock_quantity: 3,
|
|
56
|
+
image: "https://…/sneaker-white.jpg" }, // per-variation image for a visual axis
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
coupons: [{ code: "WELCOME10", discount_type: "percent", amount: 10 }],
|
|
61
|
+
// ONLY with a coupon field in the cart or checkout (see ./02-storefront.md)
|
|
62
|
+
// locations: [ … ], // shipping — the next section; passing any makes them the store's ONLY ones
|
|
63
|
+
});
|
|
64
|
+
return res.data; // ← the { success, data } envelope: plain JSON
|
|
65
|
+
} catch (e) {
|
|
66
|
+
return { success: false, status: e.response?.status, ...(e.response?.data ?? { error: e.message }) };
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Running this through a code-execution tool? Return `res.data`, never the raw response.** `invoke` resolves to the raw HTTP response, which carries circular request/response objects — `return res` (or stringifying a thrown error whole) fails with `Converting circular structure to JSON` *even when the seed succeeded*, and a thrown error needs `e.response?.data` for the same reason.
|
|
71
|
+
|
|
72
|
+
Reference taxonomy by **display name** (categories, ribbons, attributes, options) — existing records are matched case-insensitively and reused. The seeder derives slugs, checks SKU uniqueness, prices variations, and **rolls the parent's `price`/`regular_price`/`on_sale` up from the cheapest publishable variant** — never set a variant parent's price yourself. Unknown keys are rejected, so typos surface instead of vanishing.
|
|
73
|
+
|
|
74
|
+
**Idempotency:** a product whose `sku` (or derived slug) exists is skipped and reported — safe to retry after a timeout, or to seed into a store that already has products. **Limits:** ≤100 products, ≤500 variations per call, ≤50 per product, ≤50 locations. Bad payloads fail **400** `invalid_payload` with `errors: [{ path, error }]`, a modified schema **422** `schema_incompatible` — both before anything is written.
|
|
75
|
+
|
|
76
|
+
The response reports everything; these matter downstream:
|
|
77
|
+
|
|
78
|
+
```jsonc
|
|
79
|
+
{ "catalog": { "products_created": 2, "variations_created": 3,
|
|
80
|
+
"products": [{ "name": "Runner Sneaker", "id": "…", "slug": "runner-sneaker", "variation_count": 3 }] },
|
|
81
|
+
"store_name": { "value": "Aurora Threads", "action": "created" },
|
|
82
|
+
"payment_methods": null, // null = the default (offline on, card off)
|
|
83
|
+
"warnings": [] } // always present; read it — see the shipping section
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Link pages by the **`slug`** from `catalog.products[]`; never mirror the seed in client-side constants (the database is the source of truth and a mirror diverges the first time the merchant edits a product).
|
|
87
|
+
|
|
88
|
+
## Shipping — declare it in the seed call
|
|
89
|
+
|
|
90
|
+
**Seed-time `locations` is THE way to configure shipping.** Each location is a scope plus its rates and taxes, locations match in `order` ascending, and `order` defaults to the payload position — so the array reads as the priority. "€20 in Europe, €100 everywhere else" is two locations:
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
locations: [
|
|
94
|
+
{ name: "Europe", continents: ["EU"], shipping_rates: [{ name: "Standard", cost: 20 }] },
|
|
95
|
+
{ name: "Worldwide", rest_of_world: true, shipping_rates: [{ name: "International", cost: 100 }] },
|
|
96
|
+
]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- **`continents: ["EU"]`** spares you a 51-code country list. The seven codes are `AF` `AN` `AS` `EU` `NA` `OC` `SA`, and `EU` is the *continent* Europe, not the European Union. An unknown code fails `400 invalid_payload` with the known list.
|
|
100
|
+
- **`rest_of_world: true`** is the catch-all — the location that matches every address no other location claims. It cannot also carry `countries`/`continents`/`regions`.
|
|
101
|
+
- Other scopes: `countries: ["IL", "DE"]`, or explicit `regions: [{ type: "state", code: "US:CA" }]`. Matching is **country + state only** — no postcode or city rules exist.
|
|
102
|
+
- One matched location supplies **both** the shipping rates and the tax groups: `shipping_rates: [{ name, cost, free_over? }]`, `tax_groups: [{ name, rates: [{ name, rate }] }]`, `shipping_tax: { type: "percent"|"fixed", value }`.
|
|
103
|
+
|
|
104
|
+
**The catch-all trap.** Passing any `locations` suppresses the seeded "Rest of the world" fallback, so scoped locations with nothing behind them mean every other address gets `shipping_not_available` at checkout. That is plausibly intended, so it is not an error — it comes back in the response as `warnings: ["no_catchall_location: …"]`. Read `warnings` on every seed and either add a `rest_of_world` location or state the restriction to the user.
|
|
105
|
+
|
|
106
|
+
Patching `commerce.ShippingTaxLocation` records afterwards is the **harder, day-2 route** — you must mint stable rate `id`s yourself, and there is no admin function for it. Do shipping in the seed payload. For continent codes in full, state-level regions, tax-group binding, free-over thresholds, VAT-on-shipping and day-2 edits: [`../references/shipping-and-tax.md`](../references/shipping-and-tax.md).
|
|
107
|
+
|
|
108
|
+
**Tiered rates are worth one assertion**, because a wrong zone looks exactly like a right one until a customer in the wrong country pays. The cart prices from an address, so ask it directly — no UI needed:
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
// one item in the cart, then price it for each country the brief names
|
|
112
|
+
for (const country of ["DE", "AU"]) {
|
|
113
|
+
const { data } = await base44.functions.invoke("commerce/storefront-cart", {
|
|
114
|
+
action: "set-shipping-address", cart_token, address: { country, city: "x" },
|
|
115
|
+
});
|
|
116
|
+
console.log(country, data.data.available_shipping_methods.map((m) => m.cost));
|
|
117
|
+
}
|
|
118
|
+
// DE → [20], AU → [100] for the payload above. A [] means no location matched
|
|
119
|
+
// that address — the catch-all is missing.
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Images
|
|
123
|
+
|
|
124
|
+
Every product needs at least one, and **the URL you seed is the URL the store serves** — there are no placeholders to swap later. Resolve each to its **final, permanent URL before seeding**: the app's image generation, `base44.integrations.Core.UploadFile({ file })` → public URL, or stable public stock URLs (`seed-store/sample-data.ts` shows a working pattern). Match the image to the product.
|
|
125
|
+
|
|
126
|
+
Both failure modes are silent at seed time and visible only in the store, so check now: a **temporary or signed** URL (expiry params in the query string are the tell) must be re-hosted via `UploadFile`, and spot-check that one or two URLs resolve (200, image content type). Seeding without an image and setting it later through the admin API is allowed, but it is an open debt — track every imageless product and close it before handover.
|
|
127
|
+
|
|
128
|
+
## Payments — the decision
|
|
129
|
+
|
|
130
|
+
**The rule, in full.** Online card payments are **off by default**: `commerce/seed-store` enables the manual `offline` method and leaves the `card` gateway disabled. Offline-only is a complete, payable store — the order goes on-hold with the gateway's description rendered as payment instructions on `/order-received`, which needs no code and no credentials. **Enable `card` only if a payment provider is wired, or will be in the same stretch of work:** enabled means offered, and an enabled card option with nothing behind it answers **`503 no_card_payment_provider`** the moment a customer picks it. Enabling and wiring are two halves of one step; neither half is useful alone.
|
|
131
|
+
|
|
132
|
+
**`payment_methods` is the on/off switch.** Pass gateway slugs and the listed gateways are enabled while **every other row is disabled** — `["offline"]`, `["card"]` (card-only) or both, with no `commerce.PaymentGateway` reads or writes of your own. Omit it and the store stays offline-only. It is idempotent and needs no catalog, so `{ payment_methods: ["offline", "card"] }` alone is the later on/off switch. Unknown slugs fail `400 invalid_payload` with the known list.
|
|
133
|
+
|
|
134
|
+
| What the request says | Decision |
|
|
135
|
+
|---|---|
|
|
136
|
+
| A provider is named ("use Stripe") | copy the provider file, enable `card` — whenever convenient; it blocks nothing |
|
|
137
|
+
| Selling online implied, no provider named | cards are a reasonable read of *what* to offer, never of *when*: raise the provider **after** the store works |
|
|
138
|
+
| Paid another way (transfer, COD, invoice, pickup, quotes) | nothing to do — the default already is exactly this |
|
|
139
|
+
| Payments not mentioned at all | leave the default and **mention it at handover** |
|
|
140
|
+
|
|
141
|
+
**Timing: late, never the opening move.** Payments have no dependency edge into anything — the admin, the seed and the entire buy path down to `place-order` work with no provider — so putting the provider connection before the store exists only stalls the build. Any instruction that treats a payment provider as a required install step is superseded by this section.
|
|
142
|
+
|
|
143
|
+
**Say where payments landed** in your handover summary, including "offline only, cards off". A store's owner should never discover their payment configuration from a customer who couldn't pay.
|
|
144
|
+
|
|
145
|
+
*Only for a store that opted into cards:* **if it is Stripe, the code is already written** — `base44/shared/commerce/card-payment.stripe.ts` is a complete implementation, used **as-is**. Copy it over `base44/shared/commerce/card-payment.ts` (`fs.copyFileSync`, whole file — never a partial edit, which leaves duplicate exports and breaks every commerce function's deploy) and enable the gateway with `payment_methods: ["offline", "card"]`. Nothing in it needs filling in and no key belongs in the code; it reads the credential the app's Stripe connection publishes. Any other provider means implementing four functions in that one file — [`../references/online-payments.md`](../references/online-payments.md).
|
|
146
|
+
|
|
147
|
+
## Done — forget this file
|
|
148
|
+
|
|
149
|
+
- [ ] `commerce/seed-store` returned success and reported the catalog — real products, final permanent image URLs.
|
|
150
|
+
- [ ] `warnings` in the response is empty, or every warning is deliberate and stated to the user.
|
|
151
|
+
- [ ] Shipping is expressed in `locations` (with a catch-all if the store ships worldwide), not patched into entities afterwards.
|
|
152
|
+
- [ ] `coupons` seeded only if a coupon field exists ([`./02-storefront.md`](./02-storefront.md)).
|
|
153
|
+
- [ ] Cards are either off, or on with the provider file copied whole and the `card` gateway enabled.
|
|
154
|
+
- [ ] Product slugs from `catalog.products[]` recorded, and the storefront links by them.
|
|
155
|
+
- [ ] If the brief named tiered rates, each named region prices to its rate (the `set-shipping-address` check above).
|
|
156
|
+
|
|
157
|
+
Record these lines in your working notes; do not re-read this file.
|
|
158
|
+
|
|
159
|
+
- Product slugs come from the seed response's `catalog.products[]` — link pages by slug, never by a client-side map.
|
|
160
|
+
- Payments: report at handover where they landed (default = offline on, card off) — the owner must never learn it from a customer.
|
|
161
|
+
- Turning card payments on or off later is one more seed call: `{ payment_methods: ["offline", "card"] }`.
|
|
162
|
+
- Seed-time `locations` is THE shipping path; patching `commerce.ShippingTaxLocation` is the day-2 route.
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
stage: reference
|
|
3
|
+
read_when: "You are changing the admin's product editing screens — sections, price/inventory rows, attribute or variant behaviour."
|
|
4
|
+
skip_when: "You are not editing the admin product form; the shipped form already covers every product shape."
|
|
5
|
+
forget_when: "The form change saves correctly for both a simple product and one with attributes."
|
|
6
|
+
carry_forward:
|
|
7
|
+
- "A product sells variants because it carries attributes — the attributes ARE the variant control; there is no product type and no generate-variants button."
|
|
8
|
+
- "A variant parent's price is derived from the cheapest publishable variant on save — never add an input for it."
|
|
9
|
+
---
|
|
10
|
+
|
|
1
11
|
# The admin product form
|
|
2
12
|
|
|
3
13
|
Where to change what, in `src/commerce/admin/pages/products/`. The shape follows one
|