@base44/app-plugin-commerce 0.2.5 → 0.2.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@base44/app-plugin-commerce",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Base44 Commerce plugin — entities, backend functions, shared commerce engine, admin UI and the commerce skill, shipped as copyable source",
5
5
  "keywords": [
6
6
  "base44",
@@ -10,77 +10,66 @@ Nothing else in this skill needs to be open right now.
10
10
 
11
11
  The kit ships, already written and tested: 20 `commerce.*` entities, the
12
12
  `commerce/*` backend functions (storefront API, admin API, payments, webhooks,
13
- emails), the shared commerce engine, a complete admin back office at
14
- `/store-admin`, the `commerce/StoreAdmin` copilot, and the storefront's entire
15
- logic layer (`@/commerce/storefront`, `@/commerce/utils`). None of that costs
16
- you generation — your work is the store's *UI and data*, not the plumbing.
17
-
18
- **Paths in this skill.** Every skill file named anywhere — in these docs, in a
19
- checklist, in a code comment is written **relative to this skill's folder**
20
- (`.agents/skills/commerce/` in an installed app). So `references/online-payments.md`
21
- means `.agents/skills/commerce/references/online-payments.md`, and
22
- `install/03-data.md` means `.agents/skills/commerce/install/03-data.md`. Resolve
23
- them from the skill folder, never from the directory of the file that mentioned
24
- them — a comment in `src/…` naming `install/03-data.md` is not
25
- `src/install/03-data.md`. (Markdown links between skill files are ordinary
26
- relative links and resolve as written.)
13
+ emails), the shared engine, a complete admin back office at `/store-admin`, the
14
+ `commerce/StoreAdmin` copilot, and the storefront's entire logic layer
15
+ (`@/commerce/storefront`). None of that costs you generation — your work is the
16
+ store's *UI and data*, not the plumbing.
17
+
18
+ **Paths in this skill** are relative to the skill's folder
19
+ (`.agents/skills/commerce/` in an installed app), wherever they are named
20
+ docs, checklists, code comments. Resolve them from the skill folder, never from
21
+ the file that mentioned them.
27
22
 
28
23
  ## Installing right now?
29
24
 
30
25
  Read **[`install/01-install.md`](./install/01-install.md)** and follow it. It
31
26
  routes you to `install/02-storefront.md` when you start the UI and
32
27
  `install/03-data.md` when you seed the catalog — in that order, at those
33
- moments. Read nothing else up front: those three files are the whole install,
34
- and each one says when a reference is genuinely needed.
28
+ moments. Read nothing else up front.
35
29
 
36
30
  ## Four things to hold from the start
37
31
 
38
32
  - **Entity names are dotted; SDK access is bracket syntax only** —
39
33
  `base44.entities["commerce.Product"]`. `commerce__Product` and `Product` do
40
- not exist. The full data-model map is [`docs/entities.md`](./docs/entities.md);
41
- never scan `base44/entities/` to find a name.
42
- - **Store configuration is declared in one seed call**, not assembled by editing
43
- records: catalog, shipping zones ("€20 in Europe, €100 worldwide" is a
44
- six-line `locations` payload), currency, payment methods. See
45
- [`install/03-data.md`](./install/03-data.md).
46
- - **Don't weaken the admin gating.** Three layers — the UI guard, entity RLS,
47
- and `requireAdmin()` in every admin function — see
48
- [`install/01-install.md`](./install/01-install.md). Keep all three when you
49
- touch routes or schemas.
50
- - **Online card payments are optional and off by default.** A seeded store takes
51
- manual (`offline`) payment and works end to end. Cards are a late, deliberate
52
- step, and the decision plus the timing live in
53
- [`install/03-data.md`](./install/03-data.md).
34
+ not exist. The map is [`docs/entities.md`](./docs/entities.md); never scan
35
+ `base44/entities/` for a name.
36
+ - **Store configuration is declared in one seed call** catalog, shipping
37
+ zones, currency, payment methods never assembled by editing records
38
+ ([`install/03-data.md`](./install/03-data.md)).
39
+ - **Don't weaken the admin gating** — three layers (UI guard, entity RLS,
40
+ `requireAdmin()`); keep all three when touching routes or schemas
41
+ ([`install/01-install.md`](./install/01-install.md)).
42
+ - **Online card payments are optional and off by default.** A seeded store
43
+ takes manual (`offline`) payment and works end to end; cards are a late,
44
+ deliberate step ([`install/03-data.md`](./install/03-data.md)).
54
45
 
55
46
  ## The storefront: logic is premade, UI never is
56
47
 
57
- **The UI is yours, all of it** — every page, element, class and word of copy,
58
- from the home page to the checkout's place-order button. That is where a brief
59
- like "make it feel like <site>" lives, and the kit deliberately ships **no
60
- markup and no CSS anywhere**: there are no premade components to drop in or
61
- restyle. You design the storefront the way you would any app.
48
+ **The UI is yours, all of it** — every page, element, class and word of copy.
49
+ The kit deliberately ships **no markup and no CSS anywhere**; a brief like
50
+ "make it feel like <site>" is your work, done as you would with no kit.
62
51
 
63
52
  **The logic ships as headless hooks** (`@/commerce/storefront`) — checkout
64
- repricing from the address, variant resolution, cart state, coupon redemption,
65
- review policies, order-return verification. Every store's version of these is
66
- functionally identical and hand-writing them is where storefront bugs cluster,
67
- so **never re-implement what a hook does**. Each hook returns a complete
68
- view-model a `status` to branch on, ready-to-map arrays, handlers, error
69
- objects — that your markup renders:
53
+ repricing, variant resolution, cart state, coupons, reviews, order-return
54
+ verification, cart-drawer mechanics. Each returns a complete view-model
55
+ (`status` to branch on, ready-to-map arrays, handlers) **plus ready-to-spread
56
+ prop sets** (`inputProps`, `radioProps`, `buttonProps`, `panelProps`) that
57
+ carry the wiring, so your markup is elements and classes. **Never re-implement
58
+ what a hook does** — that is where storefront bugs cluster.
70
59
 
71
60
  ```jsx
72
- import { useCart, CartLine, useTotalsLines } from "@/commerce/storefront";
61
+ import { useCart, CartLine } from "@/commerce/storefront";
73
62
  const { status, lines } = useCart(); // branch on status, map lines into YOUR rows
74
63
  ```
75
64
 
76
65
  The admin UI (`src/commerce/admin/`) is finished and **also yours to change** —
77
- restyle it, add pages, rework flows. To understand the backend it talks to, read
78
- [`docs/api-admin.md`](./docs/api-admin.md).
66
+ restyle it, add pages, rework flows ([`docs/api-admin.md`](./docs/api-admin.md)
67
+ is the backend it talks to).
79
68
 
80
69
  Four rules the API enforces — a storefront that skips them cannot complete a
81
- purchase. In a React app the hooks implement all four; your markup just has to
82
- render what they hand back. The API-level statements are in
83
- [`docs/api-storefront.md`](./docs/api-storefront.md) for non-React clients:
70
+ purchase. In React the hooks implement all four; your markup renders what they
71
+ hand back (API-level statements for non-React clients:
72
+ [`docs/api-storefront.md`](./docs/api-storefront.md)):
84
73
 
85
74
  1. A product with variants needs **one selector per attribute**, resolved to a
86
75
  `variation_id` (`useProduct` + `variantAxes`).
@@ -88,8 +77,8 @@ render what they hand back. The API-level statements are in
88
77
  (`useCheckout` — automatic).
89
78
  3. **`/order-received` must exist** and render the return state, including a
90
79
  manual order's payment instructions (`useOrderReturn`).
91
- 4. **Never advertise what isn't configured** — no free-shipping banner without a
92
- real rate, no coupon codes without a field to redeem them in.
80
+ 4. **Never advertise what isn't configured** — no free-shipping banner without
81
+ a real rate, no coupon codes without a field to redeem them in.
93
82
 
94
83
  All backend functions return the envelope `{ success, data }`; with the SDK the
95
84
  payload is `res.data.data`:
@@ -101,32 +90,31 @@ const { products, has_next } = res.data.data;
101
90
 
102
91
  ## Where to look for what
103
92
 
104
- Open a file when its work starts — not while planning. The last column is there
105
- so you can answer "would that file help?" without paying for it.
106
-
107
- | Topic | Open when | Already covered without opening | Size |
108
- |---|---|---|---|
109
- | [`install/01-install.md`](./install/01-install.md) | installing it routes you to 02 and 03 | | 8K |
110
- | [`install/02-storefront.md`](./install/02-storefront.md) | building storefront pages — cart-drawer behaviour and driving the storefront from a browser script are in here too, not in a separate reference | | 38K |
111
- | [`install/03-data.md`](./install/03-data.md) | seeding the catalog, shipping, payments decision | | 18K |
112
- | [`docs/entities.md`](./docs/entities.md) | any direct entity read/write, or "which entity holds X" | function-mediated flows never need it; the addressing rule is above | 11K |
113
- | [`references/catalog-rendering.md`](./references/catalog-rendering.md) | which fields each catalog call returns, variant edge cases | the install's product list/page chunks already render correct cards, prices and selectors | 13K |
114
- | [`references/shipping-and-tax.md`](./references/shipping-and-tax.md) | zones beyond the standard recipe, taxes, editing locations later | "€X in a region, €Y worldwide" is inline in `install/03-data.md` | 8K |
115
- | [`references/online-payments.md`](./references/online-payments.md) | the store opted into cards and you are wiring the provider **now** | the decision and its timing are in `install/03-data.md`; wiring Stripe is a one-file copy, not code to write | 9K |
116
- | [`references/reviews.md`](./references/reviews.md) | moderation, or a policy beyond the `policy` prop | `useProductReviews` covers list + form + policies | 4K |
117
- | [`references/store-settings.md`](./references/store-settings.md) | changing store behavior through settings keys | the seeded defaults are right for a new store | 5K |
118
- | [`references/emails.md`](./references/emails.md) | order-email recipients, subjects, per-type overrides, the log | transactional emails already send | 5K |
119
- | [`references/admin-product-form.md`](./references/admin-product-form.md) | editing the shipped product editor | the editor works as shipped | 4K |
120
- | [`references/store-admin-agent.md`](./references/store-admin-agent.md) | changing the StoreAdmin copilot or its tools | it registers and works on install | 3K |
121
- | [`references/guest-access-security.md`](./references/guest-access-security.md) | **adding your own function or entity**, or questions about RLS/identity | the shipped functions already handle guest access | 5K |
122
- | [`references/operations.md`](./references/operations.md) | scheduled maintenance, scaling limits, outbound webhooks | nothing here is needed to ship a store | 5K |
123
- | [`docs/api-storefront.md`](./docs/api-storefront.md) | filters, customer accounts, refunds, a non-React client — anything past the happy path | the entire buy path | 40K |
124
- | [`docs/api-admin.md`](./docs/api-admin.md) | changing admin pages, automating against admin functions, the full seed contract | | 28K |
93
+ Open a file when its work starts — not while planning.
94
+
95
+ | Topic | Open when | Size |
96
+ |---|---|---|
97
+ | [`install/01-install.md`](./install/01-install.md) | installing — routes you to 02 and 03 | 6K |
98
+ | [`install/02-storefront.md`](./install/02-storefront.md) | building storefront pages | 27K |
99
+ | [`install/03-data.md`](./install/03-data.md) | seeding catalog/shipping, payments decision | 11K |
100
+ | [`docs/entities.md`](./docs/entities.md) | any direct entity read/write ("which entity holds X") | 11K |
101
+ | [`references/catalog-rendering.md`](./references/catalog-rendering.md) | which fields each catalog call returns, variant edge cases | 13K |
102
+ | [`references/shipping-and-tax.md`](./references/shipping-and-tax.md) | zones beyond 03's recipe, taxes, day-2 edits | 8K |
103
+ | [`references/online-payments.md`](./references/online-payments.md) | the store opted into cards and you are wiring the provider **now** | 8K |
104
+ | [`references/storefront-verification.md`](./references/storefront-verification.md) | driving the storefront from a browser script | 3K |
105
+ | [`references/reviews.md`](./references/reviews.md) | moderation, or a policy beyond the `policy` prop | 4K |
106
+ | [`references/store-settings.md`](./references/store-settings.md) | changing store behavior through settings keys | 5K |
107
+ | [`references/emails.md`](./references/emails.md) | order-email recipients, subjects, the log | 5K |
108
+ | [`references/admin-product-form.md`](./references/admin-product-form.md) | editing the shipped product editor | 6K |
109
+ | [`references/store-admin-agent.md`](./references/store-admin-agent.md) | changing the StoreAdmin copilot | 3K |
110
+ | [`references/guest-access-security.md`](./references/guest-access-security.md) | **adding your own function or entity**; RLS/identity questions | 5K |
111
+ | [`references/operations.md`](./references/operations.md) | scheduled maintenance, scaling limits, outbound webhooks | 7K |
112
+ | [`docs/api-storefront.md`](./docs/api-storefront.md) | filters, customer accounts, refunds, a non-React client | 32K |
113
+ | [`docs/api-admin.md`](./docs/api-admin.md) | changing admin pages, automating admin functions, the full seed contract | 24K |
125
114
 
126
115
  **The read budget.** Content you pull into context is re-read on every later
127
- call, so a file opened while planning costs many times what the same file costs
128
- opened at the moment it is used. Open one reference when its task starts, take
129
- what you need, and when a stage's checklist passes, record that stage's
130
- carry-forward lines and treat the file as gone. If a reference and this map
131
- disagree, the reference wins but if a *rule* appears in code (a hook's return
132
- value, its doc comment), the code wins over both.
116
+ call, so a file opened while planning costs many times what it costs opened at
117
+ the moment it is used. Open one reference when its task starts, take what you
118
+ need, and when a stage's checklist passes, record its carry-forward lines and
119
+ treat the file as gone. If a *rule* appears in code (a hook's JSDoc), the code
120
+ wins over any doc.
@@ -10,11 +10,7 @@ Two styles. **Reads are direct** entity SDK calls. **Mutations with side effects
10
10
 
11
11
  ## Invocation & envelope
12
12
 
13
- ```js
14
- const res = await base44.functions.invoke("commerce/admin-products", { action: "search", q: "shirt", limit: 20 });
15
- const { rows, has_next } = res.data.data; // res.data = { success, data }; .data = payload
16
- ```
17
- Success: `{ success: true, data }`. Failure: `{ success: false, error, code }` with the HTTP status set. Auth: every `commerce/admin-*` function calls `requireAdmin()` → **401** if not signed in, **403** if signed in without the `admin` role, before any data access (via the service role).
13
+ Standard invoke envelope — payload at `res.data.data` (SKILL.md owns it). Auth: every `commerce/admin-*` function calls `requireAdmin()` → **401** if not signed in, **403** if signed in without the `admin` role, before any data access (via the service role).
18
14
 
19
15
  ## Pagination & search
20
16
 
@@ -44,10 +40,10 @@ Actions: `save` · `delete` · `batch` · `duplicate` · `set-stock` · `search`
44
40
  | `attribute` | commerce.ProductAttribute | `id?, name, code?, order?` |
45
41
  | `attribute-term` | commerce.ProductAttributeTerm | `id?, attribute_id` (**required**, must exist)`, name, order?` |
46
42
 
47
- Only a category has a slug, derived from its name and made unique; its `parent_id` pointing at itself is coerced to `""` (`categoryWithDescendants` would loop). An attribute's **`code`** is derived from the name and made unique — it is the key a storefront filter URL should carry. Creating a **ribbon** is get-or-create: a name that already exists case-insensitively returns the existing record instead of splitting the ribbon in two. Renaming an **attribute value** rewrites `attributes[].options` and `default_attributes` on every product using it, and the matching `option` on their variations — products store a value by name, so the rename would otherwise orphan them.
43
+ Cascade semantics category slug/`parent_id` coercion, attribute `code` derivation, ribbon get-or-create, the attribute-value rename rewrite across products and variations: [`../references/admin-product-form.md`](../references/admin-product-form.md#taxonomy-cascade-semantics-save-term--delete-term).
48
44
 
49
45
  This is how a non-UI caller (notably the StoreAdmin agent) creates the records that `category_ids`/`ribbon_ids`/`attributes[].attribute_id` reference — assigning an id is useless if the record can't be created. **A product's `commerce.ProductAttribute` and its values must exist first**, so create those, then `save` the product with `attributes[]`/`variations[]`. (At install time, `commerce/seed-store` can do all of this in one call — it takes whole products by display name and get-or-creates the taxonomy internally; see below.)
50
- - **`delete-term`** — `{ taxonomy, id, detach? }` → `{ deleted, detached, terms_deleted }`. Deleting an **attribute** always deletes its terms (a term outliving its attribute is unreachable); `detach: true` additionally strips the attribute from every product's `attributes[]`. For a category or ribbon, products keep the id by default (the storefront skips ids that no longer resolve); `detach: true` strips it from every product first. **For an attribute *value* `detach` is a no-op** deleting a value leaves its name in every product's `attributes[].options` and leaves the variations that use it in place, so remove the value from the products first (or expect variants the storefront can no longer resolve).
46
+ - **`delete-term`** — `{ taxonomy, id, detach? }` → `{ deleted, detached, terms_deleted }`. What each taxonomy cascades and what `detach` does including where it is a no-op: same reference as above.
51
47
  - **`list-terms`** — `{ taxonomy, q?, attribute_id?, limit?, skip? }` → `{ rows, has_next }`. Categories sort by `menu_order`, attributes and attribute values by `order`, ribbons by `name`; `attribute_id` filters values to one attribute. Use it to reuse an existing record instead of creating a duplicate.
52
48
 
53
49
  ## commerce/admin-orders
@@ -60,7 +56,7 @@ Actions: `create-draft` · `create` · `update` · `preview` · `update-status`
60
56
  - Any time: `billing`, `shipping`, `customer_id`, `customer_note`, `payment_method`, `meta_data`, `status`.
61
57
  - **Line edits require pending/on-hold** (else `409 order_locked`): `items` (specs `{ product_id, variation_id?, quantity, price_override? }`), `fees`, `coupon_codes`, `chosen_shipping_method`. Providing any of these — or changing addresses — triggers a full reprice.
62
58
  - A `status` in the patch is delegated to the transition engine last (fires the side effects below); otherwise an `order.updated` webhook fires.
63
- - **`preview`** — `{ order_id, patch }`. **Reads only — writes nothing, fires no webhooks.** Prices the same patch shape `update` takes and returns `{ fields }`: the Order fields `update` *would* have written (`line_items` with per-line tax, `shipping_lines`, `fee_lines`, `coupon_lines`, `tax_lines` and every total). Allowed on locked orders, since nothing is persisted. Only the pricing keys matter (`items`, `fees`, `shipping_lines`, `billing`, `shipping`); the rest are ignored. This is how the admin order editor shows live totals for **unsaved** edits — add a product and the tax, discount and grand total update before saving — without reimplementing tax and coupon rules on the client. Sharing `repriceOptsFromPatch()` with `update` is what guarantees the preview equals the saved result.
59
+ - **`preview`** — `{ order_id, patch }`. **Reads only — writes nothing, fires no webhooks.** Prices the same patch shape `update` takes and returns `{ fields }`: the Order fields `update` *would* have written (`line_items` with per-line tax, `shipping_lines`, `fee_lines`, `coupon_lines`, `tax_lines` and every total). Allowed on locked orders, since nothing is persisted. Only the pricing keys matter (`items`, `fees`, `shipping_lines`, `billing`, `shipping`); the rest are ignored. This is how the admin order editor shows live totals for **unsaved** edits without reimplementing tax and coupon rules on the client; the preview always equals what saving would write.
64
60
  - **`update-status`** — `{ order_id, status, note? }` → runs the transition engine (side-effect matrix below).
65
61
  - **`bulk-status`** — `{ ids, status }` → `{ results: [{ id, success, error? }] }`.
66
62
  - **`status-counts`** — no payload → `{ all, pending, processing, "on-hold", completed, cancelled, refunded, failed }` (includes the `all` key). Powers the list tabs.
@@ -126,21 +122,8 @@ Actions: `test` · `redeliver` (webhook definitions themselves are direct `comme
126
122
 
127
123
  ## commerce/admin-reports
128
124
 
129
- All actions scan orders on demand (counted = `date_paid` set, or status `processing`/`completed`). Scaling: [`../references/operations.md`](../references/operations.md).
130
-
131
- | Action | Payload | Returns |
132
- |---|---|---|
133
- | `summary` | — | `{ sales_today, sales_month, orders_by_status, low_stock_count, out_of_stock_count, top_seller }` where `sales_*` = `{ gross_sales, net_sales, orders, items, tax, shipping, discount, refunds }` |
134
- | `sales` | `{ date_min?, date_max?, interval? }` (`day`\|`week`\|`month`) | `{ totals: <agg>, series: [{ period, ...agg }] }` (net = gross − refunds − tax − shipping) |
135
- | `top-sellers` | `{ date_min?, date_max?, limit? }` | `{ rows: [{ product_id, name, sku, quantity, net_revenue }] }` |
136
- | `stock` | — | `{ low_stock: [...], out_of_stock: [...] }` |
137
- | `orders-totals` | — | `{ [status]: count }` |
138
- | `products-totals` | — | `{ total, by: { [status]: count } }` |
139
- | `customers-totals` | — | `{ total, guests, registered, paying }` |
140
- | `coupons-totals` | — | `{ total, by: { [discount_type]: count } }` |
141
- | `reviews-totals` | — | `{ total, by: { [status]: count } }` |
142
- | `categories-totals` / `ribbons-totals` | — | `{ total, terms: [{ id, name, count }] }` |
143
- | `attributes-totals` | — | `{ total, attributes: [{ id, name, terms }] }` |
125
+ Actions: `summary` · `sales` · `top-sellers` · `stock` · a `*-totals` count action per resource (`orders` `products` `customers` `coupons` `reviews` `categories` `ribbons` `attributes`). All scan orders on demand (counted = `date_paid` set, or status `processing`/`completed`).
126
+ The full payload/return table and scaling guidance: [`../references/operations.md`](../references/operations.md#commerceadmin-reports-actions).
144
127
 
145
128
  ## commerce/admin-tools
146
129
 
@@ -157,11 +140,8 @@ Actions: `status` · `admin-email-recipients` · `recount-terms` · `recount-cou
157
140
 
158
141
  ## commerce/payments
159
142
 
160
- Actions: `create-link` · `complete-return` · `verify` — the admin side of online payments (the same function serves customers, who authorize with `order_key` instead; see [`api-storefront.md`](./api-storefront.md)).
161
-
162
- - **`create-link`** — `{ order_id }` → `{ url, reference }`: a provider-hosted payment page for an unpaid order — the **payment link** to send a customer. Accepts an order on **any** payment method (including none, as admin-created orders start) and switches it onto the card gateway, logging the change: refunds key off `payment_method`, so this keeps the order honest about how it was paid. `409 already_paid`, `400 card_payments_disabled`, `503 no_card_payment_provider`.
163
- - **`verify`** — `{ order_id }` → `{ paid, already_confirmed, status, order }`: re-asks the provider and moves the order to `processing` when the money is there. Idempotent.
164
- - **`complete-return`** — what the storefront's mandatory `/order-received` page calls; full contract in [`api-storefront.md`](./api-storefront.md#commercepayments--online-payment-for-an-order).
143
+ Actions: `create-link` · `complete-return` · `verify` — the admin side of online payments. [`api-storefront.md`](./api-storefront.md#commercepayments--online-payment-for-an-order) owns the action table (payloads, returns, error codes, the mandatory `/order-received` flow).
144
+ An admin authorizes with the role and passes `{ order_id }` alone where a customer passes `order_key`; `create-link` mints the payment link for an unpaid order on **any** current method (it is switched onto the card gateway, logged).
165
145
 
166
146
  `commerce/payment-webhook` is the provider's server-to-server callback (raw body, no `action` envelope). The function is **premade** — it validates every event through `card-payment.ts`'s `parseWebhook` and never trusts an event body on its own: an unverified event only names an order, and payment is verified through `checkCardPaymentPaid` against the provider's API, so no signing secret is needed. Until `card-payment.ts` is implemented it answers `400 webhook_not_implemented`; events it can't tie to an order are acknowledged with `200 { ignored: true }` so providers don't retry. See [`../references/online-payments.md`](../references/online-payments.md).
167
147
 
@@ -197,7 +177,7 @@ The one-call catalog bootstrap. Entries reference categories/ribbons/attributes
197
177
  | `attributes` | `[{ name, options }]` or `{ <name>: [options] }` | declares the variant axes |
198
178
  | `variations` | `[{ options: { <name>: <option> }, ...overrides }]` | the stocked combinations. **Omit it to auto-generate all** combinations, each inheriting the product-level price/sale fields |
199
179
  | `default_options` | `{ <name>: <option> }` | the pre-selected combination |
200
- | everything else | `commerce.Product` fields | `name` (required), `slug`, `sku`, `status`, `regular_price`, `sale_price`, `images`, `featured`, `virtual`, `downloadable`, `downloads`, `tax_status`, `tax_group`, `stock_quantity`, `weight`, `dimensions`, `meta_data` |
180
+ | everything else | `commerce.Product` fields | `name` (required), `slug`, `sku`, `status`, `regular_price`, `sale_price`, `date_on_sale_from`/`date_on_sale_to`, `short_description`, `description`, `images`, `featured`, `virtual`, `downloadable`, `downloads`, `download_limit`, `download_expiry`, `tax_status`, `tax_group`, `stock_quantity`, `manage_stock`, `low_stock_amount`, `backorders`, `sold_individually`, `weight`, `dimensions`, `meta_data` (`upsell_ids`/`cross_sell_ids` take Product *ids*, which exist only after this call — cross-link via `admin-products` later) |
201
181
 
202
182
  A variation with its own `stock_quantity` gets `manage_stock: "yes"`; without one it draws on the parent's pooled stock (`"parent"`), and its SKU is synthesized from the parent's when absent. Parent price and `stock_status` are derived by the same helpers as `save` ([derived fields](./entities.md#derived-fields--never-write-these)), but **no `product.created` webhooks fire** — bootstrap precedes subscribers, so use `admin-products` for webhook-visible creates. **Re-runs converge**: a product whose `sku` (or, with none, derived slug) already exists is skipped and reported, so retries never duplicate; an explicit variation SKU already in use is **`409 duplicate_sku`**. `coupons` is a thin passthrough (code lowercased, skip-if-exists).
203
183
 
@@ -205,28 +185,9 @@ A variation with its own `stock_quantity` gets `manage_stock: "yes"`; without on
205
185
 
206
186
  ### Payload — `locations`
207
187
 
208
- Seeding `locations` is **the** way to set shipping up. Each entry becomes a `commerce.ShippingTaxLocation`, skip-if-exists **by name**.
209
-
210
- | Key | Shape | Notes |
211
- |---|---|---|
212
- | `name` | string, required | the skip-if-exists match key |
213
- | `countries` | `["IL", "DE"]` | ISO country codes → `{ type: "country" }` regions |
214
- | `continents` | `["EU"]` | one of `AF` `AN` `AS` `EU` `NA` `OC` `SA`, sparing you a 51-code country list. `EU` is the **continent** Europe, not the European Union. An unknown code fails listing all seven |
215
- | `regions` | `[{ type, code }]` | raw escape hatch (`country`\|`state`\|`continent`; state codes are `US:CA`) |
216
- | `rest_of_world` | `true` | the **catch-all** every unmatched address falls to. It has no regions, so combining it with `countries`/`continents`/`regions` is a contradiction and fails validation |
217
- | `order` | integer | match priority, **ascending**. Defaults to the **payload position**, so the array reads as the priority; an explicit value wins |
218
- | `shipping_rates` | `[{ id?, name, cost, free_over? }]` | one checkout choice each. `id` is minted from the location + rate names when omitted, and must stay stable (carts and orders reference the chosen rate by it). `free_over` = discounted items subtotal at which it becomes free. Negative `cost` fails |
219
- | `tax_groups` | `[{ name, rates: [{ name, rate }] }]` | a `Products` group is prepended when missing (products pick one by name via `tax_group`). A `rate` outside 0–100 fails |
220
- | `shipping_tax` | `{ type: "percent"\|"fixed", value }` | tax on the shipping line |
221
-
222
- ```js
223
- locations: [
224
- { name: "Europe", continents: ["EU"], shipping_rates: [{ name: "Standard", cost: 20 }] },
225
- { name: "Worldwide", rest_of_world: true, shipping_rates: [{ name: "International", cost: 100 }] },
226
- ]
227
- ```
188
+ Seeding `locations` is **the** way to set shipping up. Each entry becomes a `commerce.ShippingTaxLocation`, skip-if-exists **by name**: a scope (`countries: ["IL", "DE"]`, `continents: ["EU"]` — the **continent**, not the EU — raw `regions`, or `rest_of_world: true`, the catch-all, which contradicts the other scope keys), `order` (match priority ascending, defaults to payload position), `shipping_rates: [{ id?, name, cost, free_over? }]` (a minted `id` must stay stable — carts/orders reference it), `tax_groups` (a `Products` group prepended when missing) and `shipping_tax`. Unknown codes, negative `cost` or a tax `rate` outside 0–100 fail validation. Field-by-field semantics, matching and recipes: [`../references/shipping-and-tax.md`](../references/shipping-and-tax.md).
228
189
 
229
- **A payload carrying `locations` suppresses the seeded `Rest of the world` fallback** (free standard delivery, `order: 999`) yours become the store's only shipping data, since a second no-region location beside them would be a fallback nobody asked for: shadowed today, live the moment theirs is reordered or deleted. The cost: **locations with no catch-all ship only to the listed regions**, every other address getting `shipping_not_available` at checkout. Plausibly intended, so not an error — it returns as `warnings: ["no_catchall_location: …"]`. Full model: [`../references/shipping-and-tax.md`](../references/shipping-and-tax.md).
190
+ A payload carrying `locations` suppresses the seeded catch-all fallback — without your own, unlisted addresses get `shipping_not_available`, flagged as `warnings: ["no_catchall_location: …"]` (same reference; [`../install/03-data.md`](../install/03-data.md) also owns this).
230
191
 
231
192
  ### Response
232
193
 
@@ -2,65 +2,28 @@
2
2
 
3
3
  The raw HTTP/SDK surface behind a customer-facing shopfront. **A React storefront should not call most of it directly** — `src/commerce/storefront/` ships the logic layer, and this file is the reference for what lies beyond it, for non-React and headless clients, and for the exact payloads and error codes.
4
4
 
5
- **Logic is premade, UI never is.** In a React app, every surface here has a headless hook in `@/commerce/storefront` — `useProductList`, `useProduct` (+ `variantAxes`, `useAddToCartButton`), `useCart`/`useCartLine`/`useCoupon`, `useCheckout` (+ `useAddressForm`, the pickers), `useOrderReturn`, `useProductReviews`, `useStorefrontSeo`. The hooks own the API calls and the branching below and hand you a view-model; **all markup and styling are yours** — nothing in the kit renders UI. Never hand-roll a hook's logic. Framework-free helpers (API client, variant resolution, price and totals rules, free-shipping rules) live in `src/commerce/utils/`.
5
+ **Logic is premade, UI never is.** In a React app, every surface here has a headless hook in `@/commerce/storefront` — `useProductList`, `useProduct` (+ `variantAxes`, `useAddToCartButton`, `useProductSpecs`, `useUpsell`), `useCart`/`useCartLine`/`useCoupon`/`useCartUI` (+ `CartUIProvider`), `useCheckout`/`usePlaceOrder` (+ `useAddressForm`, the pickers and prop-getters), `useOrderReturn`, `useProductReviews`, `useStorefrontSeo`. The hooks own the API calls and the branching below and hand you a view-model; **all markup and styling are yours** — nothing in the kit renders UI. Never hand-roll a hook's logic. Framework-free helpers (API client, variant resolution, price and totals rules, free-shipping rules) live in `src/commerce/utils/`.
6
6
 
7
- ## Contents
8
-
9
- - [Required behaviors](#required-behaviors--a-storefront-that-skips-these-cannot-sell) (the four hard rules) · [capabilities](#what-the-api-already-gives-you-read-before-designing-the-ui) · [conventions](#conventions) (envelope, auth, money)
10
- - [`storefront-catalog`](#commercestorefront-catalog): [`get-store-info`](#get-store-info) · [`list-products`](#list-products) · [`get-product`](#get-product) · [`list-categories`](#list-categories) · [`list-ribbons`](#list-ribbons) · [`list-attributes`](#list-attributes) · [`submit-review`](#submit-review)
11
- - [`storefront-cart`](#commercestorefront-cart): [priced view](#the-priced-cart-view-returned-by-every-action) · [actions](#actions) · [`cart_token`](#cart_token--the-storefronts-one-job) · [**shipping rules**](#shipping-is-not-optional--read-this-before-building-checkout) · [never advertise unconfigured offers](#never-advertise-what-the-store-isnt-configured-to-do)
12
- - [`storefront-checkout`](#commercestorefront-checkout): [`place-order`](#place-order) · [`confirm-payment`](#confirm-payment) · [`cancel-order`](#cancel-order)
13
- - [`payments`](#commercepayments--online-payment-for-an-order): payment links, `complete-return` for `/order-received`
14
- - [`storefront-account`](#commercestorefront-account): orders, notes, downloads, addresses, my reviews
15
- - [Walkthrough A — guest checkout](#walkthrough-a--guest-checkout) · [B — member](#walkthrough-b--member-experience) (non-React clients)
16
- - Entities and direct CRUD → [`entities.md`](./entities.md) · admin surface → [`api-admin.md`](./api-admin.md)
7
+ Entities and direct CRUD → [`entities.md`](./entities.md) · admin surface → [`api-admin.md`](./api-admin.md)
17
8
 
18
9
  ## Required behaviors — a storefront that skips these cannot sell
19
10
 
20
- The canonical statement. Each is enforced by the API, not a nicety:
21
-
22
- | # | Requirement | Enforced by |
23
- |---|---|---|
24
- | 1 | **Products with variants: one selector per attribute**, resolved to a `variation_id` before adding to the cart — never a flat list of combinations | `add-item` → `400 variation_required` ([product page](#get-product)) |
25
- | 2 | **Present shipping options and send a choice** — drive it off the cart's `shipping_status`; a single option is auto-applied, several mean you must ask | `place-order` → `400 shipping_method_required` ([shipping](#shipping-is-not-optional--read-this-before-building-checkout)) |
26
- | 3 | **Redirect to `payment.checkout_url`** when the customer pays by card | the order stays `pending` otherwise |
27
- | 4 | **Implement the return page** (`/order-received`, or your own route set in Settings → General → *Payment return path*) and call `commerce/payments` `complete-return` there | without it a paid order is never confirmed, and a wrong path is a 404 |
28
- | 5 | **Only advertise offers the store is configured for** (free-shipping thresholds come from a location's shipping rates) | nothing — this one is on you |
11
+ Five hard rules the first four API-enforced, the fifth on you:
29
12
 
30
- Four public functions **`storefront-catalog`**, **`storefront-cart`**, **`storefront-checkout`**, **`storefront-account`** all invoked the same way, all returning the same envelope.
13
+ 1. **Variants: one selector per attribute**, resolved to a `variation_id` before `add-item` (`400 variation_required`, [product page](#get-product)) never a flat list of combinations.
14
+ 2. **Present shipping options and send a choice**, driven by the cart's `shipping_status` (`place-order` → `400 shipping_method_required`, [shipping](#shipping-is-not-optional--read-this-before-building-checkout)).
15
+ 3. **Redirect to `payment.checkout_url`** for card payments — the order stays `pending` otherwise.
16
+ 4. **Implement the return page** (`/order-received`, or the route set in Settings → General → *Payment return path*) and call `commerce/payments` `complete-return` there — otherwise a paid order is never confirmed, and a wrong path is a 404.
17
+ 5. **Only advertise offers the store is configured for** (free-shipping thresholds come from a location's rates) — nothing enforces this one.
31
18
 
32
- ## What the API already gives you (read before designing the UI)
33
-
34
- **Build to this surface, not to the basics** — everything below already exists server-side, so leaving it out ships a storefront weaker than the store behind it.
35
-
36
- | Capability | Already supported | Where |
37
- |---|---|---|
38
- | **Product discovery** | full-text `search`; filters: category (incl. descendants), ribbon, attribute+term, price range, `featured`, `on_sale`, `in_stock_only`; sort by name / price / **newest** (default) / **popularity** / **rating**; paging | [`list-products`](#list-products) |
39
- | **Rich product pages** | galleries, per-variant price/stock/image, categories, ribbons, **upsells**, **cross-sells** | [`get-product`](#get-product) |
40
- | **Taxonomy navigation** | category tree, **ribbons with counts**, attributes + terms for filter UIs | [`list-categories`](#list-categories) · [`list-ribbons`](#list-ribbons) · [`list-attributes`](#list-attributes) |
41
- | **Customer reviews** | paginated per product with **average rating + count**, `verified` flag, **submission by anyone with an email** (no login), "my reviews" | [`submit-review`](#submit-review) |
42
- | **Cart** | guest carts via `cart_token`, add/update/remove, quantity merging, `sold_individually` caps, live re-pricing, stock revalidation | [`storefront-cart`](#commercestorefront-cart) |
43
- | **Coupons** | apply/remove by code, fully validated server-side (eligibility, limits, per-user usage). Codes are admin-only data, so a store with coupons needs a **code field** in the buy path or they can never be redeemed | [`apply-coupon`](#actions) |
44
- | **Shipping** | address → matched location → **selectable rates with live costs**, free-over thresholds | [`set-shipping-address`](#actions) |
45
- | **Tax** | inclusive/exclusive pricing, per-location tax groups, itemized display — resolved server-side | [`get-store-info`](#get-store-info) |
46
- | **Stock states** | in stock / out of stock / **on backorder**, low-stock signalling, out-of-stock hiding | [`list-products`](#list-products) |
47
- | **Checkout** | guest or authenticated, billing/shipping, per-gateway routing, **payment instructions** for manual methods, cancel, post-payment confirm | [`storefront-checkout`](#commercestorefront-checkout) |
48
- | **Accounts** | order history, saved addresses, **guest order tracking by `order_key`** with customer-visible notes | [`storefront-account`](#commercestorefront-account) |
49
- | **Digital products** | entitlement-checked downloads with remaining-count, expiry, signed URLs for private files | [`get-download`](#commercestorefront-account) |
50
- | **Store config** | currency, units, catalog/cart price display — honour it instead of hardcoding | [`get-store-info`](#get-store-info) |
51
-
52
- Not in the backend: the visitor UI (all of it is yours — the hooks above carry the logic), and the card-payment provider integration, which only a store that opts into cards needs — cards are off by default, the decision and its timing live in [`../install/03-data.md`](../install/03-data.md), the provider code in [`../references/online-payments.md`](../references/online-payments.md). Payment methods and the currency are admin-owned data — always render them from `get-store-info`, never a hardcoded list, and format prices with `Intl.NumberFormat(undefined, { style: "currency", currency })`.
19
+ Four public functions — **`storefront-catalog`**, **`storefront-cart`**, **`storefront-checkout`**, **`storefront-account`** — all invoked the same way, all returning the same envelope. Discovery filters and sorts, reviews, coupons, live shipping rates, tax, stock states, digital downloads and guest order tracking all already exist server-side build to the sections below, not to a subset.
53
20
 
54
21
  ## Conventions
55
22
 
56
- - **Invoke:** `base44.functions.invoke("commerce/<function>", { action, ...payload })`. Response body is on `res.data`; the envelope is `{ success, data }` on success or `{ success, error, code }` on failure (HTTP status set accordingly).
57
- ```js
58
- const res = await base44.functions.invoke("commerce/storefront-catalog", { action: "get-store-info" });
59
- const info = res.data.data; // envelope → payload
60
- ```
23
+ - **Invoke:** `base44.functions.invoke("commerce/<function>", { action, ...payload })`; the `{ success, data }` / `{ success, error, code }` envelope and `res.data.data` unwrapping are covered in SKILL.md.
61
24
  - **Auth modes:** most actions are anonymous. Some require an authenticated Base44 session (marked **auth**). Two bearer credentials (HTTPS only): `order_key` for guest order access, `cart_token` for the cart — whose lifecycle rules are [in the cart section](#cart_token--the-storefronts-one-job).
62
25
  - **Identity is never a payload field.** Anything attributed to a person — a review's author, a saved profile — comes from the session. An email in the body does not make you that customer, and no request can assert that an order was paid. A feature built on client-supplied identity will be ignored.
63
- - **Money:** numbers, 2-dp. **Dates:** ISO strings.
26
+ - **Money:** numbers, 2-dp — format with `Intl.NumberFormat(undefined, { style: "currency", currency })` using the `get-store-info` currency, never a hardcoded symbol. **Dates:** ISO strings.
64
27
  - **Errors:** every failure has a stable `code` (listed per action) plus a human `error` message.
65
28
 
66
29
  ---
@@ -131,15 +94,7 @@ await cat({ category_id, sort: "popularity", per_page: 4 }); // top in catego
131
94
  ```
132
95
  **Errors:** `404 not_found` (missing / not published / hidden).
133
96
 
134
- > **`variations[]` is not a list of choices to show.** Build **one control per `product.attributes[]` entry** (Size, Color, …) — every attribute is an axis — and resolve the combination to a variation client-side; `add-item` needs that `variation_id`. Variant prices come from `variations[]`, never from `product.price` (the parent's price is a rolled-up from-price). A React page gets all of this from `useProduct` + `useAddToCartButton` (map `variantAxes(view, pick)` into your controls, gate on `view.purchasable`); a non-React client uses the framework-free resolver:
135
- > ```js
136
- > import { resolveSelection, defaultSelection, selectOption } from "@/commerce/utils";
137
- > const view = resolveSelection(product, variations, selection); // selection from defaultSelection(), updated via selectOption()
138
- > // view.axes → one control per axis view.display → image/price/sku/stock of the resolved variation
139
- > // view.availability[axis.key][option] → "available" | "out_of_stock" | "unavailable" (disable, don't hide)
140
- > // view.purchasable + view.addToCart → { product_id, variation_id } for add-item
141
- > ```
142
- > Field availability per view, ribbons, from-price rules and the variant deep-dive (availability states, incomplete-selection pricing, URL round-trip): [`../references/catalog-rendering.md`](../references/catalog-rendering.md).
97
+ > **`variations[]` is not a list of choices to show** variant rule 1 applies, and variant prices come from `variations[]`, never `product.price` (a rolled-up from-price). React: `useProduct` + `useAddToCartButton`. The non-React resolver sample (`resolveSelection` from `@/commerce/utils`) and the variant deep-dive: [`../references/catalog-rendering.md`](../references/catalog-rendering.md).
143
98
 
144
99
  ### `list-categories`
145
100
  No payload. Returns a nested tree: `{ "categories": [ { ...category, "children": [...] } ] }` sorted by `menu_order` then name.
@@ -157,7 +112,7 @@ This is the **only** way a storefront can enumerate ribbons (the entity is admin
157
112
  No payload. Returns `{ "attributes": [ { ...attribute, "terms": [ ...values ] } ] }` — each attribute (`id, name, code, order`) with its values (`id, attribute_id, name, order, count`), both sorted by `order`; for filter UIs. Filter with `list-products` `attribute_id` (id or attribute **name**) + `attribute_term` (the value name); `code` is the stable key for a URL.
158
113
 
159
114
  ### `submit-review`
160
- **Payload:** `{ product_id, reviewer?, review, rating }`.
115
+ **Payload:** `{ product_id, email?, reviewer?, review, rating? }` — `email` is required for guests (`400 email_required`); a signed-in caller's session email always wins.
161
116
 
162
117
  > A React storefront gets the whole reviews surface — this call, the paginated list from `get-product`, refresh, and the store's moderation policy — from **`useProductReviews`**; don't hand-roll it. Read on for the raw contract.
163
118
 
@@ -239,15 +194,9 @@ rather than assuming:
239
194
  | `missing_address` | options undeterminable until an address is set (several locations, none matchable yet) | collect the address, call `set-shipping-address`; cost is not calculated yet |
240
195
  | `none_available` | nothing ships to this address | say so and block checkout (`set-shipping-address` already failed `shipping_not_available`) |
241
196
 
242
- Rules the cart enforces on every price, so you get them for free:
243
-
244
- - **A single available method is auto-selected.** One option is not a choice, and requiring a round trip for it is how a storefront ends up never sending a method at all.
245
- - **A single location answers before the address.** A store with exactly **one** location gets its rates (and, via auto-select, a cost) with no address set — the options are the same everywhere. With several, the cart reports `missing_address` until `set-shipping-address` resolves one.
246
- - **An unsupported address fails on entry**, with `400 shipping_not_available` — surface it on the address form instead of letting the customer reach payment.
247
- - **A stale choice is dropped.** If the address or cart changes so the chosen method is no longer offered, it is cleared (and re-auto-selected when one remains) rather than silently pricing zero shipping.
248
- - **Costs change with the cart** — free-shipping thresholds and coupon-gated methods appear and disappear, so **call `set-shipping-address` as soon as an address exists and re-read the cart after every mutation**; never carry an options list you fetched earlier.
197
+ Rates re-resolve on every cart change (a stale choice is dropped, re-auto-selected when one remains) call `set-shipping-address` as soon as an address exists and re-read the cart after every mutation; never cache an options list. A store with exactly one location answers before any address is set.
249
198
 
250
- What `place-order` does with all this: a single offered method is applied for you; several offered with none chosen is `400 shipping_method_required` (with `available_shipping_methods` in the error body); a `chosen_shipping_method` not offered for the address is `400 invalid_shipping_method`, never silently swapped; nothing offered is `400 no_shipping_available`. No order is created in any of those cases, and the order that is created prices the resolved method, so its `shipping_total` always matches what the customer was shown.
199
+ `place-order` errors: none chosen of several `400 shipping_method_required`; `chosen_shipping_method` no longer offered `400 invalid_shipping_method` (never silently swapped); nothing offered `400 no_shipping_available`; an unsupported address already failed `set-shipping-address` with `400 shipping_not_available`. Each error body carries `available_shipping_methods`, no order is created, and a created order's `shipping_total` matches what was shown.
251
200
 
252
201
  **In React this is `useCheckout`**: address state debounces into `set-shipping-address`, `ShippingMethodPicker` renders the options, and `canPlaceOrder` blocks with a named blocker until a method resolves — the whole table above is the hook's behavior, not code you write. Location/zone/tax configuration: [`../references/shipping-and-tax.md`](../references/shipping-and-tax.md).
253
202
 
@@ -255,19 +204,7 @@ What `place-order` does with all this: a single offered method is applied for yo
255
204
 
256
205
  Copy like *"Free shipping on orders over €150"* is a **claim about store configuration**, and checkout honours only what is configured. Don't write such a line unless a matching rule exists — configure it (with the operator's agreement) or leave the copy out. The same goes for a discount claim (it needs a real `commerce.Coupon`, and only a code the operator gave you) and for delivery-time or returns promises the template cannot enforce.
257
206
 
258
- **Locations are admin-only data** like every entity, so a visitor cannot list them — do not try. Two honest sources:
259
-
260
- 1. **The cart** — the live answer for a real address: after `set-shipping-address`, `available_shipping_methods` reflects every rule checkout will honour, free shipping included. Prefer this.
261
- 2. **A projection you add**, for a threshold banner *before* an address exists: widen `get-store-info` (or add an action) to return the free-shipping rules, then normalize them:
262
-
263
- ```js
264
- import { freeShippingRules, freeShippingThreshold, freeShippingProgress } from "@/commerce/utils";
265
- // locations come from your own storefront action — NOT a client entity read
266
- const threshold = freeShippingThreshold(freeShippingRules(locations)); // null → no rule: no banner
267
- const progress = freeShippingProgress(threshold, cart.totals.subtotal - cart.totals.discount_total);
268
- ```
269
-
270
- A free-shipping rule is a rate that is free (`cost: 0`) or carries a `free_over` threshold. Thresholds are **per location** — a rule in one location must not become a site-wide banner.
207
+ **Locations are admin-only data**, so a visitor cannot list them. Two honest sources: the cart's `available_shipping_methods` after `set-shipping-address` (prefer this it reflects every rule checkout will honour), or a server projection you add (widen `get-store-info` or add an action — never a client entity read) normalized with `freeShippingRules` / `freeShippingThreshold` / `freeShippingProgress` from `@/commerce/utils` (a `null` threshold means no rule: no banner). Thresholds are **per location** — a rule in one location must not become a site-wide banner.
271
208
 
272
209
  ---
273
210
 
@@ -352,68 +289,38 @@ Two access modes: **auth** (Base44 session) or **`order_key` bearer** (guest tra
352
289
 
353
290
  ---
354
291
 
355
- ## Walkthrough A — guest checkout
292
+ ## Walkthrough — guest checkout
356
293
 
357
- Raw-call sequence for a non-React client (a React app gets this from `useCart` + `useCheckout`).
294
+ Raw-call spine for a non-React client (a React app gets this from `useCart` + `useCheckout`).
358
295
 
359
296
  ```js
360
297
  const inv = (fn, payload) => base44.functions.invoke(fn, payload).then(r => r.data.data);
361
298
 
362
- // 1. Browse, then 2. open a cart with one item
363
299
  const { products } = await inv("commerce/storefront-catalog", { action: "list-products", per_page: 12 });
364
- let cart = await inv("commerce/storefront-cart", { action: "create",
365
- items: [{ product_id: products[0].id, quantity: 1 }] });
366
- const token = cart.cart_token; // re-read it from every response
300
+ let cart = await inv("commerce/storefront-cart", { action: "create", items: [{ product_id: products[0].id, quantity: 1 }] });
301
+ const token = cart.cart_token; // re-read it from every response
367
302
 
368
- // 3. (variants) fetch the options, then add the chosen variation
369
303
  const detail = await inv("commerce/storefront-catalog", { action: "get-product", id: products[0].id });
370
- if ((detail.product.attributes ?? []).length) {
304
+ if ((detail.product.attributes ?? []).length) // variants: add the resolved variation
371
305
  cart = await inv("commerce/storefront-cart", { action: "add-item",
372
306
  cart_token: token, product_id: detail.product.id, variation_id: detail.variations[0].id });
373
- }
374
307
 
375
- // 4. Coupon (optional)
376
308
  cart = await inv("commerce/storefront-cart", { action: "apply-coupon", cart_token: token, code: "welcome10" });
377
309
 
378
- // 5. Shipping: address as soon as it's known, then a method if asked for one
379
310
  cart = await inv("commerce/storefront-cart", { action: "set-shipping-address",
380
311
  cart_token: token, address: { country: "US", state: "CA", postcode: "90210", city: "Los Angeles" } });
381
312
  if (cart.shipping_status === "choice_required") {
382
313
  const picked = await askCustomer(cart.available_shipping_methods); // your UI
383
- cart = await inv("commerce/storefront-cart", { action: "choose-shipping-method",
384
- cart_token: token, method_id: picked.id });
314
+ cart = await inv("commerce/storefront-cart", { action: "choose-shipping-method", cart_token: token, method_id: picked.id });
385
315
  }
386
316
 
387
- // 6. Place it (offline → on-hold; no money has arrived yet)
388
- const order = await inv("commerce/storefront-checkout", { action: "place-order",
317
+ const order = await inv("commerce/storefront-checkout", { action: "place-order", // offline → on-hold
389
318
  cart_token: token, payment_method: "offline",
390
319
  billing: { first_name: "Ada", last_name: "Lovelace", address_1: "1 St",
391
320
  city: "Los Angeles", state: "CA", postcode: "90210", country: "US", email: "ada@example.com" } });
392
321
 
393
- // 7. Track it later without an account, using order_key from step 6
394
- const tracked = await inv("commerce/storefront-account", { action: "get-order",
322
+ const tracked = await inv("commerce/storefront-account", { action: "get-order", // guest tracking
395
323
  order_id: order.order_id, order_key: order.order_key });
396
324
  ```
397
325
 
398
- ## Walkthrough Bmember experience
399
-
400
- Same, for an authenticated caller (the SDK sends the session automatically).
401
-
402
- ```js
403
- // Carts made while logged out merge into this one automatically on `get`/`create`.
404
- let cart = await inv("commerce/storefront-cart", { action: "create", items: [{ product_id, quantity: 2 }] });
405
-
406
- // Saved addresses speed up checkout
407
- await inv("commerce/storefront-account", { action: "update-my-addresses",
408
- billing: { first_name: "Ada", last_name: "Lovelace", address_1: "1 St", city: "LA",
409
- state: "CA", postcode: "90210", country: "US", email: "ada@example.com" } });
410
-
411
- const order = await inv("commerce/storefront-checkout", { action: "place-order",
412
- cart_token: cart.cart_token, payment_method: "offline", billing: { /* ... */ } });
413
- // offline → on-hold; show order.payment_instructions.account_details
414
-
415
- // Order history, downloads, reviews — all auth, no order_key needed
416
- const { orders } = await inv("commerce/storefront-account", { action: "my-orders", per_page: 10 });
417
- const { downloads } = await inv("commerce/storefront-account", { action: "my-downloads" });
418
- const file = await inv("commerce/storefront-account", { action: "get-download", permission_id: downloads[0]?.permission_id });
419
- ```
326
+ **Authenticated caller:** same sequence the SDK sends the session automatically, carts made while logged out merge in on `create`/`get`, and `update-my-addresses` lets checkout prefill saved addresses. The account actions (`my-orders`, `my-downloads`, `my-reviews`, `get-download`) then need no `order_key`.