@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.
Files changed (73) hide show
  1. package/README.md +25 -22
  2. package/base44/agents/commerce/StoreAdmin.jsonc +1 -1
  3. package/base44/entities/commerce.OrderRefund.jsonc +1 -1
  4. package/base44/entities/commerce.PaymentGateway.jsonc +1 -1
  5. package/base44/entities/commerce.Webhook.jsonc +1 -1
  6. package/base44/functions/commerce/admin-products/entry.ts +1 -1
  7. package/base44/functions/commerce/admin-reports/entry.ts +1 -1
  8. package/base44/functions/commerce/payments/entry.ts +2 -2
  9. package/base44/functions/commerce/seed-store/defaults.ts +1 -1
  10. package/base44/functions/commerce/seed-store/entry.ts +34 -0
  11. package/base44/functions/commerce/seed-store/seed-catalog.ts +39 -5
  12. package/base44/functions/commerce/storefront-catalog/entry.ts +1 -1
  13. package/base44/functions/commerce/storefront-checkout/entry.ts +1 -1
  14. package/base44/shared/commerce/card-payment.stripe.ts +198 -0
  15. package/base44/shared/commerce/card-payment.ts +1 -1
  16. package/base44/shared/commerce/payments.ts +2 -2
  17. package/base44/shared/commerce/scan.ts +1 -1
  18. package/base44/shared/commerce/sequence.ts +2 -2
  19. package/package.json +1 -1
  20. package/scripts/install.js +24 -14
  21. package/skills/commerce/SKILL.md +117 -51
  22. package/skills/commerce/docs/api-admin.md +89 -28
  23. package/skills/commerce/docs/api-storefront.md +113 -126
  24. package/skills/commerce/docs/entities.md +137 -0
  25. package/skills/commerce/install/01-install.md +101 -0
  26. package/skills/commerce/install/02-storefront.md +444 -0
  27. package/skills/commerce/install/03-data.md +162 -0
  28. package/skills/commerce/references/admin-product-form.md +10 -0
  29. package/skills/commerce/references/catalog-rendering.md +110 -0
  30. package/skills/commerce/references/emails.md +49 -12
  31. package/skills/commerce/references/guest-access-security.md +18 -5
  32. package/skills/commerce/references/online-payments.md +49 -149
  33. package/skills/commerce/references/operations.md +52 -0
  34. package/skills/commerce/references/reviews.md +31 -16
  35. package/skills/commerce/references/shipping-and-tax.md +110 -0
  36. package/skills/commerce/references/store-admin-agent.md +21 -0
  37. package/skills/commerce/references/store-settings.md +49 -0
  38. package/src/commerce/admin/README.md +6 -3
  39. package/src/commerce/admin/layout/AuthGuard.jsx +1 -1
  40. package/src/commerce/admin/pages/products/Reviews.jsx +1 -1
  41. package/src/commerce/admin/pages/settings/InventorySettings.jsx +1 -1
  42. package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +1 -1
  43. package/src/commerce/storefront/StorefrontProvider.jsx +106 -20
  44. package/src/commerce/storefront/index.js +74 -24
  45. package/src/commerce/storefront/internal/useAsyncData.js +86 -0
  46. package/src/commerce/storefront/useAddressForm.js +96 -0
  47. package/src/commerce/storefront/useCartLine.js +221 -0
  48. package/src/commerce/storefront/useCheckout.jsx +18 -6
  49. package/src/commerce/storefront/useOrderReturn.js +36 -10
  50. package/src/commerce/storefront/useProduct.js +295 -0
  51. package/src/commerce/storefront/useProductGallery.js +74 -0
  52. package/src/commerce/storefront/useProductList.js +153 -0
  53. package/src/commerce/storefront/useProductPrice.js +58 -0
  54. package/src/commerce/storefront/useProductReviews.js +242 -0
  55. package/src/commerce/storefront/useStorefrontSeo.js +204 -0
  56. package/src/commerce/storefront/useTotalsLines.js +109 -0
  57. package/src/commerce/utils/address-spec.js +89 -0
  58. package/src/commerce/utils/images.js +45 -0
  59. package/src/commerce/utils/index.js +22 -7
  60. package/src/commerce/utils/price.js +95 -0
  61. package/src/commerce/utils/shipping-promos.js +2 -2
  62. package/src/commerce/utils/specs.js +26 -0
  63. package/src/commerce/utils/storefront.js +47 -3
  64. package/src/commerce/utils/totals.js +110 -0
  65. package/src/commerce/utils/variants.js +58 -3
  66. package/skills/commerce/installation-guidelines.md +0 -93
  67. package/skills/commerce/post-installation.md +0 -496
  68. package/skills/commerce/references/limits-and-performance.md +0 -16
  69. package/skills/commerce/references/media-and-downloads.md +0 -4
  70. package/skills/commerce/references/product-render.md +0 -89
  71. package/skills/commerce/references/scheduled-work.md +0 -19
  72. package/skills/commerce/references/storefront-product-page.md +0 -83
  73. package/skills/commerce/references/webhooks.md +0 -10
@@ -7,8 +7,8 @@
7
7
  * event). No provider ships with the template and the `card` gateway is
8
8
  * seeded disabled, so this half is dormant until a store opts into online
9
9
  * cards. Any provider works; the rules — and a complete Stripe
10
- * implementation, for that one common choice — are in
11
- * `.agents/skills/commerce/references/online-payments.md`.
10
+ * implementation, for that one common choice — are in the commerce skill's
11
+ * references/online-payments.md.
12
12
  *
13
13
  * Everything here — return URLs, storing the payment reference on the order,
14
14
  * idempotent confirmation that moves the order to processing, refund routing —
@@ -2,7 +2,7 @@
2
2
  * Full-collection scan helper. Base44's filter() is exact-match with a 5k page
3
3
  * cap and no total count, so server-side search/aggregation loops pages of 500.
4
4
  * Keep `cap` sane — reports over very large stores should move to a
5
- * materialized stats entity (see .agents/skills/commerce/references/limits-and-performance.md).
5
+ * materialized stats entity (see the commerce skill's references/operations.md).
6
6
  */
7
7
 
8
8
  export interface ScanOpts {
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * NOTE ON CONCURRENCY: Base44 has no transactions, so nextOrderNumber() is a
5
5
  * read-max-then-increment and two simultaneous checkouts could race. The window
6
- * is tiny and order ids (not numbers) are the primary key; see
7
- * .agents/skills/commerce/references/limits-and-performance.md for mitigations.
6
+ * is tiny and order ids (not numbers) are the primary key; see the commerce
7
+ * skill's references/operations.md for mitigations.
8
8
  */
9
9
 
10
10
  const ORDER_NUMBER_START = 1001;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@base44/app-plugin-commerce",
3
- "version": "0.1.20",
3
+ "version": "0.2.2",
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",
@@ -25,12 +25,11 @@
25
25
  * overwritten (so re-running after a template update is safe), everything
26
26
  * else in the app is left untouched.
27
27
  *
28
- * This is only the static part of the install. The remaining steps live in
29
- * ../skills/commerce/installation-guidelines.md (deps, deploy, seeding) and
30
- * ../skills/commerce/post-installation.md (mounting the /store-admin route, admin
31
- * role, seeding, the storefront quick start); day-2 guidance starts at
32
- * ../skills/commerce/SKILL.md. The skill folder carries all of this
33
- * documentation into the app.
28
+ * This is only the static part of the install. Everything after it lives in
29
+ * ../skills/commerce/install/01-install.md (deps, the /store-admin mount, the
30
+ * admin-role gating), which routes on to 02-storefront.md and 03-data.md at the
31
+ * right moments; ../skills/commerce/SKILL.md is the map for all later work. The
32
+ * skill folder carries all of that documentation into the app.
34
33
  *
35
34
  * Written with dynamic import() and process.argv[1] (instead of require/
36
35
  * __dirname) so it runs unchanged whether the host app's package.json is
@@ -132,6 +131,17 @@
132
131
  ["src", "commerce", "admin", "pages", "settings", "TaxRatesTable.jsx"],
133
132
  ["src", "commerce", "admin", "pages", "settings", "ShippingSettings.jsx"],
134
133
  ["src", "commerce", "admin", "pages", "settings", "ShippingZoneEditor.jsx"],
134
+ // The skill's docs were restructured into stage files (install/01..03) plus a
135
+ // smaller reference set. Stale guidance is worse than none — agents read
136
+ // whatever is in .agents/skills/ — so every retired doc goes, by name.
137
+ [".agents", "skills", "commerce", "post-installation.md"], // → install/01..03
138
+ [".agents", "skills", "commerce", "installation-guidelines.md"], // → install/01-install.md + references/store-admin-agent.md
139
+ [".agents", "skills", "commerce", "references", "product-render.md"], // → references/catalog-rendering.md
140
+ [".agents", "skills", "commerce", "references", "storefront-product-page.md"], // → references/catalog-rendering.md
141
+ [".agents", "skills", "commerce", "references", "scheduled-work.md"], // → references/operations.md
142
+ [".agents", "skills", "commerce", "references", "limits-and-performance.md"], // → references/operations.md
143
+ [".agents", "skills", "commerce", "references", "webhooks.md"], // → references/operations.md
144
+ [".agents", "skills", "commerce", "references", "media-and-downloads.md"], // → docs/api-storefront.md + install/03-data.md
135
145
  ];
136
146
  let retiredRemoved = 0;
137
147
  for (const parts of RETIRED) {
@@ -186,16 +196,16 @@
186
196
  (retiredRemoved ? ` (${retiredRemoved} retired file${retiredRemoved === 1 ? "" : "s"} removed)` : ""),
187
197
  );
188
198
  console.log(
189
- "\nNext steps (see .agents/skills/commerce/installation-guidelines.md + post-installation.md):\n" +
199
+ "\nNext: read .agents/skills/commerce/install/01-install.md and follow it.\n" +
200
+ "It is the whole install, and it says when to open 02-storefront.md and\n" +
201
+ "03-data.md — read nothing else up front.\n" +
202
+ "\n" +
190
203
  " 1. No deps to add: sonner, recharts and react-markdown ship with the default\n" +
191
- " Base44 template — check package.json and npm i only what is truly missing;\n" +
192
- " do not re-install packages already listed as dependencies\n" +
204
+ " Base44 template — check package.json and npm i only what is truly missing\n" +
193
205
  ' 2. Mount the admin router: <Route path="/store-admin/*" element={<AdminApp />} />\n' +
194
- " and implement the payment return page /order-received one hook,\n" +
195
- " useOrderReturn from @/commerce/storefront (post-installation.md)\n" +
196
- " 3. Grant your user the admin role, then seed the store's data — one commerce/seed-store\n" +
197
- " call (store_name required) takes the whole catalog via products, or the demo data\n" +
198
- " via with_sample_data, or defaults only (post-installation.md §3)\n" +
206
+ " plus the mandatory /order-received route (useOrderReturn + your markup)\n" +
207
+ " 3. Seed the store — one commerce/seed-store call (store_name required) takes the\n" +
208
+ " catalog, currency, shipping locations and payment methods\n" +
199
209
  " 4. CLI installs only: npx base44 agents push (the hosted runtime syncs agents on write)"
200
210
  );
201
211
  })();
@@ -1,66 +1,132 @@
1
1
  ---
2
2
  name: commerce
3
- description: Base44 Commerce template 20 commerce.* entities, 16 commerce/* backend functions (storefront + admin APIs + online payments), the shared commerce engine under base44/shared/commerce/, the Store Management UI mounted at /store-admin, and the commerce/StoreAdmin agent (admin copilot). Read before working on store features the admin UI, storefront building, payment provider wiring, scheduled maintenance, emails, webhooks, downloads, or scaling limits.
3
+ description: Base44 Commerce — commerce.* entities, commerce/* backend functions (storefront + admin APIs, payments, webhooks), the shared engine under base44/shared/commerce/, the Store Management UI at /store-admin, and the headless storefront hooks under src/commerce/. Read before working on store features: installing, building a storefront, seeding a catalog, shipping zones, payments, or the admin.
4
4
  ---
5
5
 
6
6
  # Commerce
7
7
 
8
- Operational guidance for extending, operating and building on the Base44 Commerce Template. This file is the map: it stays short, and each topic links to a focused reference under [`.agents/skills/commerce/references/`](./references/) — open one only when your task touches that area. The API references live in [`.agents/skills/commerce/docs/`](./docs/).
9
-
10
- > **If you are a Base44 agent working inside the runtime, read this first:**
11
- > - **Don't read the whole codebase up front.** Start with this file, then open only the files your current task touches plus the matching reference below, pulling in more as you go. Reading everything first just burns context.
12
- > - **Don't weaken the admin gating.** The UI guard, entity RLS and function guards form three enforcement layers — see [`.agents/skills/commerce/post-installation.md`](./post-installation.md) — keep all of them intact when changing routes or schemas.
13
- > - **Entity names are dotted — SDK access is bracket syntax only:** `base44.entities["commerce.PaymentGateway"]` (schema file: `base44/entities/commerce.PaymentGateway.jsonc`). `commerce__PaymentGateway` / `PaymentGateway` don't exist. Gateway on/off is seed data anyway: `commerce/seed-store` `payment_methods`.
14
- > - **Online card payments are optional and off by default.** The seed enables the manual `offline` method and leaves the `card` gateway disabled. **Enable cards only if a payment provider is wired — or will be, in the same stretch of work** (one file): enabled with nothing behind it answers `503 no_card_payment_provider` at checkout. Choosing to add them is fine, but it belongs at the *end* of an install, once there's a working store to point at — nothing else depends on it, so never open a build by asking a user for a provider key. [`post-installation.md` §4.1](./post-installation.md#41-off-by-default--enable-only-with-a-provider-wired) has the rule and the timing.
15
-
16
- ## IMPORTANTfirst-time installation
17
-
18
- If the template was just installed (or you are installing it right now), read [`.agents/skills/commerce/post-installation.md`](./post-installation.md) **before anything else and, unless the user has a special requirement, read nothing else**: it alone covers embedding the admin pages, the three-layer admin-role enforcement (do not weaken), seeding the store's data **one `commerce/seed-store` call takes the whole catalog** (products with attributes; variants, categories, ribbons and Shipping & Tax Locations created internally — §3) — **card payments as an optional, late step** (§4.1: off by default — the seed enables offline payment and leaves cards off; **no payment provider ships with the kit**, so cards mean deliberately wiring one, raised at the end of the install and never at its start; §4.2 is the four-step how-to, and the provider code itself lives in [`references/online-payments.md`](./references/online-payments.md) — read it only if the store opts in), and the **storefront quick start** (§2): logic-only chunks for product list → product page → optional cart → checkout. Its §0 schedules the whole install: **storefront components are written while image generation and the seed call (parallel writes — usually a few seconds) run — never after them, and never behind a payment-provider round-trip**. The references below and the API docs are for requests that go beyond that happy path, not for the install. The full install-from-scratch steps are in [`.agents/skills/commerce/installation-guidelines.md`](./installation-guidelines.md).
19
-
20
- ## Working on the UI
21
-
22
- - **Admin UI** (`src/commerce/admin/`) — a complete store back office ships with the template, and it is **yours to change**: restyle it, add or remove pages, rework flows, extend it however the app needs. To understand the backend it talks to, read [`.agents/skills/commerce/docs/api-admin.md`](./docs/api-admin.md) — every admin function/action plus the direct-entity-CRUD contract. The only invariant is the admin-role gating (see above).
23
- - **Storefront** — **no visitor UI ships** (no visual components at all — the shopfront's look is the app's to design); the storefront *API* is complete (token-based cart), and the storefront *logic* ships on two levels: framework-free helpers in [`src/commerce/utils/`](../../src/commerce/utils/) (`storefront.js` client, `variants.js`, `shipping-promos.js` — import from `@/commerce/utils`) and the React layer in [`src/commerce/storefront/`](../../src/commerce/storefront/) (import from `@/commerce/storefront`): `StorefrontProvider` (one client, one store-info cache, ONE shared cart), `useCart`, `useCheckout` (guided checkout — automatic shipping/tax recalculation when the address is complete, shipping/payment choice, a gated `placeOrder`), headless `ShippingMethodPicker`/`PaymentMethodPicker`, and `useOrderReturn` for the mandatory `/order-received` page. **In a React app, build cart/checkout on these hooks — don't hand-roll their logic.** Catalog views stay thin by design (client calls + variant helpers — the design freedom lives there). Start from the logic-only quick start in [`post-installation.md` §2](./post-installation.md#2-storefront-quick-start--logic-only) — it covers the whole buy path; go to [`.agents/skills/commerce/docs/api-storefront.md`](./docs/api-storefront.md) for anything beyond it. Navigation comes from three actions — `list-categories` (tree), `list-ribbons` (flat, with counts) and `list-attributes` (filter UIs). What to render in the grid vs. the product page — and which fields only one of the two calls returns — is [`references/product-render.md`](./references/product-render.md); **ribbons are the most-skipped part of it and belong in both views**.
24
-
25
- ### If you build a storefront, these four are not optional
26
-
27
- Agents keep shipping storefronts that miss these, and each one breaks buying outright. **In a React app, rules 2 and 3 are already implemented — `useCheckout` and `useOrderReturn` from `@/commerce/storefront` (post-installation.md §2.4); use them instead of re-deriving the logic.** The rules below stay stated in API terms so non-React (or non-hook) storefronts can follow them too; details in [`docs/api-storefront.md`](./docs/api-storefront.md) and [`references/storefront-product-page.md`](./references/storefront-product-page.md).
28
-
29
- 1. **Products with variants need one selector per attribute — and the selection must resolve to a variation.** `get-product` gives `product.attributes` (every entry is an axis — there is no product type) and `variations` (the combinations). Render a control per axis, never a list of combinations, then send the resolved `variation_id`:
30
- ```js
31
- import { resolveSelection, defaultSelection, selectOption } from "@/commerce/utils";
32
- const view = resolveSelection(product, variations, selection); // axes, availability, price, addToCart
33
- await addItem(view.addToCart); // useCart(); or add-item with cart_token + ...view.addToCart
34
- ```
35
- `add-item` **rejects a product with attributes unless it gets a `variation_id`** (`400 variation_required`), so a page that ignores this cannot sell anything.
8
+ **This file is the map. It tells you what to read, when, and what you can skip.**
9
+ Nothing else in this skill needs to be open right now.
10
+
11
+ The kit ships, already written and tested: 20 `commerce.*` entities, the
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 generationyour 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.)
27
+
28
+ ## Installing right now?
29
+
30
+ Read **[`install/01-install.md`](./install/01-install.md)** and follow it. It
31
+ routes you to `install/02-storefront.md` when you start the UI and
32
+ `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.
35
+
36
+ ## Four things to hold from the start
37
+
38
+ - **Entity names are dotted; SDK access is bracket syntax only** —
39
+ `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).
54
+
55
+ ## The storefront: logic is premade, UI never is
56
+
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.
62
+
63
+ **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:
70
+
71
+ ```jsx
72
+ import { useCart, CartLine, useTotalsLines } from "@/commerce/storefront";
73
+ const { status, lines } = useCart(); // branch on status, map lines into YOUR rows
74
+ ```
36
75
 
37
- 2. **Checkout must recalculate shipping from the address and send a choice.** `useCheckout` does all of it: it calls `set-shipping-address` automatically once the address is complete (recalculating options, cost and tax; `400 shipping_not_available` → its `addressError`), resolves `shipping_status`, and blocks `placeOrder` until the choice is made. Hand-rolled flows must do the same by hand: call `set-shipping-address` as soon as the customer provides an address, then read `shipping_status` on the returned cart — `auto_selected` (one option, already applied) · `chosen` · `choice_required` → **show `available_shipping_methods` and call `choose-shipping-method`** · `missing_address` → collect the address first (a single-location store shows its options even before one). `place-order` refuses with `400 shipping_method_required` until then — that is not a bug to work around. (`chosen_shipping_method` on the cart is the rate **id**, a string display it by looking up its entry's `title`/`cost`, never by rendering the id.)
76
+ 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).
38
79
 
39
- 3. **Handle card payments if the store offers them, and build `/order-received` either way.** Render whatever gateways `get-store-info` reports — a default-seeded store offers `offline` only (cards are off by default, and enabled only with a provider wired: [`post-installation.md` §4.1](./post-installation.md#41-off-by-default--enable-only-with-a-provider-wired)), so never hardcode a card option. Choosing the card gateway returns `payment.checkout_url` — `useCheckout().placeOrder` redirects there for you (with no provider implemented — one file, [`post-installation.md` §4.2](./post-installation.md#42-wiring-a-provider--one-file) — it answers `503 no_card_payment_provider`; offer the other methods). Every payment link comes back to `/order-received`, which **you must implement**: one `useOrderReturn()` call rendered by `status` (hand-rolled: `commerce/payments` `complete-return` with the query params). Without that page a customer pays into a 404 and the order is never marked paid. Also persist the `cart_token` from **every** cart response (the provider/client do this) — `add-item` silently starts a fresh cart when the stored token is stale.
80
+ 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:
40
84
 
41
- 4. **Never advertise what isn't configured — and never configure what can't be reached.** "Free shipping over €150" must come from a real shipping rate that is free or carries a `free_over` threshold on a Shipping & Tax Location. Locations are admin-only data, so a storefront cannot read them: the live answer is the cart's `available_shipping_methods` after the address is set, and `shipping-promos.js` normalizes the rules wherever the records *are* in hand. No rule means no banner. **Coupons are the same rule in reverse:** codes are admin-only data that a customer can only use by typing them, so a store with coupons needs a code field `useCart().applyCoupon`, in the cart or, when there's no cart page, in the checkout — rendering `discount_total` and invalid codes inline. No field means no coupons: don't seed them, don't name a code in the copy ([`post-installation.md` §2.3/§2.4](./post-installation.md#23-cart)).
85
+ 1. A product with variants needs **one selector per attribute**, resolved to a
86
+ `variation_id` (`useProduct` + `variantAxes`).
87
+ 2. Checkout must **recalculate shipping from the address and send a choice**
88
+ (`useCheckout` — automatic).
89
+ 3. **`/order-received` must exist** and render the return state, including a
90
+ 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.
42
93
 
43
- All functions return the envelope `{ success, data }` (or `{ success, error, code }`); with the SDK the body is on `res.data`:
94
+ All backend functions return the envelope `{ success, data }`; with the SDK the
95
+ payload is `res.data.data`:
44
96
 
45
97
  ```js
46
98
  const res = await base44.functions.invoke("commerce/storefront-catalog", { action: "list-products", per_page: 12 });
47
- const { products, has_next } = res.data.data; // res.data = envelope, .data = payload
99
+ const { products, has_next } = res.data.data;
48
100
  ```
49
101
 
50
- ## Topic references
51
-
52
- Open the matching file under `.agents/skills/commerce/references/` only when a task touches its area:
53
-
54
- | Topic | Read when the task involves | Reference |
55
- |---|---|---|
56
- | Product rendering (list + page) | what to show in a product grid vs. the product page, field availability across `list-products`/`get-product`, **ribbons in both views**, variant pricing on cards, adding a page-only field to the listing call | [`references/product-render.md`](./references/product-render.md) |
57
- | Variant selection | attribute-level selectors, resolving a selection to a variation, availability states, incomplete-selection pricing, add-to-cart contract | [`references/storefront-product-page.md`](./references/storefront-product-page.md) |
58
- | Reviews | stars on cards and the product page, the review list + submit form (public by email, backend ships complete), the auto-approve toggle, UI-enforced policies (login-gated, verified-only, required rating) | [`references/reviews.md`](./references/reviews.md) |
59
- | Admin product form | changing the product editor — its stacked sections are **Price & Inventory** (tax group, then the attributes, then a row per variant, or a single *Base price* row when there are none), **Modifiers** (`meta_data`), **Downloads**, **Linked products**; one **Visible** toggle drives `status`. Variants reconcile from the attribute values automatically: no generate step, no per-variant delete. Weight and dimensions are per variant. | [`references/admin-product-form.md`](./references/admin-product-form.md) |
60
- | Online payments | a store that has **opted into card payments** (they are off by default and no provider ships) the order side (checkout, confirmation, payment links, refund records) *and* the payment webhook are premade; wiring a provider means implementing the four functions in one file (`shared/commerce/card-payment.ts`), and this reference holds the rules plus a complete Stripe implementation | [`references/online-payments.md`](./references/online-payments.md) |
61
- | Scheduled work | recurring maintenance stock-hold release, abandoned-cart cleanup, webhook-log pruning, counter-drift repair | [`references/scheduled-work.md`](./references/scheduled-work.md) |
62
- | Emails | transactional order emails, per-type overrides, deliverability, the email log | [`references/emails.md`](./references/emails.md) |
63
- | Webhooks | outbound webhooks, HMAC signing, delivery log, auto-disable behavior | [`references/webhooks.md`](./references/webhooks.md) |
64
- | Images & downloads | catalog image uploads, downloadable products, private files & signed URLs | [`references/media-and-downloads.md`](./references/media-and-downloads.md) |
65
- | Limits & performance | pagination caps, search, no-transaction consequences, record size, reports scaling | [`references/limits-and-performance.md`](./references/limits-and-performance.md) |
66
- | Guest access & security | public storefront functions, `cart_token`/`order_key` handling, RLS boundaries | [`references/guest-access-security.md`](./references/guest-access-security.md) |
102
+ ## Where to look for what
103
+
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 | — | 14K |
111
+ | [`install/03-data.md`](./install/03-data.md) | seeding the catalog, shipping, payments decision | | 14K |
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 | 12K |
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 |
125
+
126
+ **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.
@@ -2,27 +2,11 @@
2
2
 
3
3
  For building store automation or an alternative admin UI against the Base44 Commerce Template. The bundled admin UI (`src/commerce/admin/`) uses exactly this surface.
4
4
 
5
- ## Data-access contract
6
-
7
- Two access styles. **Reads are direct** entity SDK calls; **mutations with side effects go through `commerce/admin-*` functions**; simple config entities are **direct CRUD** (protected by admin-only RLS).
8
-
9
- | Resource | Reads | Writes | Why |
10
- |---|---|---|---|
11
- | commerce.Product, commerce.ProductVariation | direct (`filter`/`get`/`list`) | **`commerce/admin-products`** | derived pricing/stock, taxonomy counts, webhooks |
12
- | commerce.Order, commerce.OrderNote | direct | **`commerce/admin-orders`** | lifecycle side effects (stock, emails, webhooks, dates) |
13
- | commerce.OrderRefund | direct | **`commerce/admin-refunds`** | restock, totals, refund status transition |
14
- | commerce.Coupon | direct | **`commerce/admin-coupons`** | code normalization/uniqueness, webhooks |
15
- | commerce.Customer | direct | **`commerce/admin-customers`** | email uniqueness, invite/link, stats |
16
- | commerce.ProductReview | direct | **`commerce/admin-reviews`** | rating recalculation |
17
- | commerce.ProductCategory, commerce.ProductRibbon | direct | **direct CRUD**, or `commerce/admin-products` `save-term`/`delete-term`/`list-terms` (the API/agent path) | category slug uniqueness; ribbon get-or-create by name |
18
- | commerce.ProductAttribute, commerce.ProductAttributeTerm | direct | **direct CRUD**, or `commerce/admin-products` `save-term`/`delete-term`/`list-terms` (the API/agent path) | attribute `code` uniqueness; value rename rewrites products; attribute delete cascades its values |
19
- | commerce.ShippingTaxLocation | direct | **direct CRUD** | config; consumed by the pricing engine at read time |
20
- | commerce.PaymentGateway | direct | **`commerce/seed-store`** `payment_methods` to switch methods on/off; **direct CRUD** to change a record (add or rename a manual option, its `description`, bank accounts, `order`) | one seed call converges every gateway row to the given set, so "enable cards", "card-only" and "offline-only" are one idempotent call; the record's own fields are ordinary config — the admin's Settings → Payments screen edits them directly |
21
- | commerce.StoreSettings | direct | **direct CRUD** (one record per `group_id`) | grouped config |
22
- | commerce.Webhook | direct | **direct CRUD** (+ `commerce/admin-webhooks` for test/redeliver) | definition is data; dispatch is engine |
23
- | commerce.WebhookDelivery, commerce.EmailLog | direct (read-only logs) | written by the engine | audit logs |
24
-
25
- **Every entity is admin-only on read *and* write** — including the catalog. Storefront access goes exclusively through the `commerce/storefront-*` functions, which run with the service role and project only what is safe; a direct entity read or write from a non-admin is rejected by the backend regardless of the UI. See [`references/guest-access-security.md`](../references/guest-access-security.md).
5
+ ## Data access
6
+
7
+ Two styles. **Reads are direct** entity SDK calls. **Mutations with side effects go through `commerce/admin-*` functions**; plain config entities are direct CRUD. Which entity is which, its key fields, its derived fields and the exact write path is the catalog table in [**`entities.md`**](./entities.md) including the addressing rule (`base44.entities["commerce.<Name>"]`, bracket syntax only) that a guessed spelling gets wrong.
8
+
9
+ **Every entity is admin-only on read *and* write**, catalog included. Storefront access goes exclusively through the `commerce/storefront-*` functions, which run with the service role and project only what is safe; a direct entity read or write from a non-admin is rejected by the backend regardless of the UI. See [`../references/guest-access-security.md`](../references/guest-access-security.md).
26
10
 
27
11
  ## Invocation & envelope
28
12
 
@@ -45,7 +29,7 @@ Success: `{ success: true, data }`. Failure: `{ success: false, error, code }` w
45
29
  Actions: `save` · `delete` · `batch` · `duplicate` · `set-stock` · `search` · `save-term` · `delete-term` · `list-terms`
46
30
 
47
31
  - **`save`** — `{ product, variations? }`. Upserts the product (create if no `id`); when `variations` is provided, diffs them (create/update/delete-missing).
48
- > **Selling variants takes both of these in *this* call**: the attribute listed **on the product** as `attributes: [{ attribute_id, name, position, options: [...] }]`, plus a `variations` array with one entry per stocked combination (each `{ attributes: [{ attribute_id, name, option }], sku, regular_price, manage_stock: "yes", stock_quantity, status: "publish" }`) and `default_attributes` for the pre-selected combination. There is **no `type` field**: carrying attributes is what makes a product sell variants, so a `commerce.ProductAttribute` record on its own changes nothing — and a product listing an attribute with no variations cannot be added to a cart at all (`400 variation_required`). Descriptive properties belong in `meta_data`, not `attributes`. **Don't set the parent's price** — `regular_price`, `price` and `on_sale` are derived from the cheapest publishable variant on every save, which is what makes catalog cards, price sorting and price filters agree. Enforces SKU + slug uniqueness across products *and* variations (auto-suffixes slug on collision; `duplicate_sku` on SKU clash). Derives `price`/`on_sale` from the sale window and `stock_status` when stock is managed; updates category/ribbon `count`; rolls parent stock **and price** up when the product has attributes; fires `product.created`/`product.updated`. → `{ product, variations }`.
32
+ > **Selling variants takes both of these in *this* call**: the attribute listed **on the product** as `attributes: [{ attribute_id, name, position, options: [...] }]`, plus a `variations` array with one entry per stocked combination (each `{ attributes: [{ attribute_id, name, option }], sku, regular_price, manage_stock: "yes", stock_quantity, status: "publish" }`) and `default_attributes` for the pre-selected combination. There is **no `type` field**: carrying attributes is what makes a product sell variants, so a `commerce.ProductAttribute` record on its own changes nothing — and a product listing an attribute with no variations cannot be added to a cart at all (`400 variation_required`). Descriptive properties belong in `meta_data`, not `attributes`. **Don't set the parent's price** — it is rolled up from the cheapest publishable variant on every save ([derived fields](./entities.md#derived-fields--never-write-these)). Enforces SKU + slug uniqueness across products *and* variations (auto-suffixes slug on collision; `duplicate_sku` on SKU clash); derives `price`/`on_sale` from the sale window, `stock_status` when stock is managed, and category/ribbon `count`; fires `product.created`/`product.updated`. → `{ product, variations }`.
49
33
  - **`delete`** — `{ id }`. Cascades variations, decrements counts, fires `product.deleted`.
50
34
  - **`batch`** — `{ create?: [], update?: [], delete?: [] }` (≤100 total) → per-item results.
51
35
  - **`duplicate`** — `{ id }` → new draft copy (name "(Copy)", suffixed SKU, reset sales/ratings) incl. variations.
@@ -142,7 +126,7 @@ Actions: `test` · `redeliver` (webhook definitions themselves are direct `comme
142
126
 
143
127
  ## commerce/admin-reports
144
128
 
145
- All actions scan orders on demand (counted = `date_paid` set, or status `processing`/`completed`). See the commerce skill reference `.agents/skills/commerce/references/limits-and-performance.md` for scaling.
129
+ All actions scan orders on demand (counted = `date_paid` set, or status `processing`/`completed`). Scaling: [`../references/operations.md`](../references/operations.md).
146
130
 
147
131
  | Action | Payload | Returns |
148
132
  |---|---|---|
@@ -175,16 +159,93 @@ Actions: `status` · `admin-email-recipients` · `recount-terms` · `recount-cou
175
159
 
176
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)).
177
161
 
178
- - **`create-link`** — `{ order_id }` → `{ url, reference }`: a provider-hosted payment page for an unpaid order, i.e. the **payment link** to send a customer. Accepts an order with **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`.
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`.
179
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.
180
- - **`complete-return`** — `{ order_id, order_key, payment?, return_url? }` → `{ state, paid, already_confirmed, status, order, payment_link, payment_instructions }`: what the storefront's **mandatory** `/order-received` page calls confirm, progress the order, and hand back a render-ready result plus a fresh payment link while unpaid (card) or the gateway's payment instructions (manual).
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).
181
165
 
182
166
  `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).
183
167
 
184
168
  ## commerce/seed-store
185
169
 
186
- Not action-routed. Body `{ store_name?, currency?, weight_unit?, dimension_unit?, payment_methods?, with_sample_data?, products?, coupons?, locations? }`. **`payment_methods`** (gateway slugs, e.g. `["offline", "card"]`) enables exactly the listed gateways and disables every other gateway row — the one-call way to honor "card-only"/"offline-only"/"both"; explicit values win on re-runs, unknown slugs fail as `400 invalid_payload`, and the response reports `payment_methods: { enabled, disabled }` (`null` when not passed). **Omitted, the seed enables `offline` and leaves `card` disabled** — cards are off by default, so passing `"card"` is what turns them on: only do so with a provider wired in `shared/commerce/card-payment.ts`, or about to be, since an enabled card option with nothing behind it answers `503 no_card_payment_provider` at checkout ([`post-installation.md` §4.1](../post-installation.md#41-off-by-default--enable-only-with-a-provider-wired)). **`store_name` is required** when the `emails` group doesn't exist yet (**400** `store_name_required` otherwise) — pass the app's name **as the platform shows it** — ask the user or read it from the dashboard. `base44/config.jsonc` → `name` is *not* authoritative: it can still say `New App` for an app the platform calls `Canvas`. A backend function can't read either, its environment being only `BASE44_APP_ID`. It lands in `emails.store_name` — one setting serving as both the store name in email subjects and the sender name on every transactional email; a nameless store renders subjects like `[]: New order #1002`, which is why seeding refuses one. Requires admin. Runs a **canary schema check** first — on any incompatibility returns **422** `{ success:false, code:"schema_incompatible", errors:[{ entity, error }] }` and writes nothing. Otherwise seeds defaults idempotently, then the catalog. On an already-seeded store a passed `store_name` fills a **blank** name and never overwrites one the merchant chose. **`currency`** (ISO code, validated against the shared currencies table) and **`weight_unit`/`dimension_unit`** set the `general` group; unknown values fail as **400** `invalid_payload`. Unlike `store_name` they **always win** — there is no blank state to distinguish a merchant's choice from the seeded default, so passing one on a re-run updates the store. Prices are formatted with `Intl.NumberFormat` — the currency is a value, there are no format settings.
170
+ Not action-routed the body **is** the payload. Requires admin. This is the **canonical seed contract**; the worked example lives in [`../install/03-data.md`](../install/03-data.md).
171
+
172
+ **Order of operations**, so a failure says how much was written: (1) validate — pure, so bad input is **`400 invalid_payload`** carrying every problem at once in `errors: [{ path, error }]`, before anything runs; (2) **canary schema probe** per entity the seeder writes — any incompatibility is **`422 schema_incompatible`** with `errors: [{ entity, error }]`, nothing written; (3) defaults, idempotently (settings groups, gateways, and the fallback location **only when the payload brings no `locations`**); (4) the catalog, rolling back everything this call created on a mid-write failure (**`500 catalog_seed_failed`**), reused taxonomy untouched.
173
+
174
+ ### Payload — store setup
175
+
176
+ | Key | Type | Effect |
177
+ |---|---|---|
178
+ | `store_name` | string | **Required on a first seed** (the `emails` group doesn't exist yet) — otherwise **`400 store_name_required`**. Lands in `emails.store_name`: both the name in email subjects and the sender name, so a nameless store renders `[]: New order #1002`. On an already-seeded store it **fills a blank** name and never overwrites one the merchant chose. |
179
+ | `currency` | ISO code | Sets `general.currency`, validated against the shared currencies table. Prices are formatted with `Intl.NumberFormat` — there are no format settings. |
180
+ | `weight_unit` | `kg`\|`g`\|`lbs`\|`oz` | Sets `general.weight_unit`. |
181
+ | `dimension_unit` | `cm`\|`m`\|`mm`\|`in`\|`yd` | Sets `general.dimension_unit`. |
182
+ | `payment_methods` | non-empty array of gateway slugs, e.g. `["card"]` | Enables exactly these and **disables every other gateway row**, so "card-only"/"offline-only"/"both" is one idempotent call, no entity editing. Unknown slugs fail `400 invalid_payload` listing the known ones. |
183
+ | `with_sample_data` | boolean | The template's demo catalog, **only when the store has zero products**. Cannot combine with `products`/`coupons`/`locations`. |
184
+ | `products` `coupons` `locations` | arrays | The catalog payload — below. |
185
+
186
+ `currency`, the units and `payment_methods` **always win**, first seed and re-runs alike: there is no blank state distinguishing a merchant's choice from the default, so passing one is an instruction. `store_name` is the exception (fill-blank only) — pass the app's name **as the platform shows it**: `base44/config.jsonc` → `name` is not authoritative (it can say `New App` for an app the platform calls `Canvas`) and a function's env is only `BASE44_APP_ID`, so ask the user or read the dashboard.
187
+
188
+ **Omit `payment_methods` and the seed enables `offline`, leaving `card` disabled** — cards are off by default and only belong on with a provider wired, since an enabled card option with nothing behind it answers `503 no_card_payment_provider` at checkout. The decision: [`../install/03-data.md`](../install/03-data.md).
189
+
190
+ ### Payload — `products` / `coupons`
191
+
192
+ The one-call catalog bootstrap. Entries reference categories/ribbons/attributes **by display name** — taxonomy is get-or-created (slugs/codes derived, existing records matched case-insensitively and reused, their casing canonicalized into the product), so no id juggling.
193
+
194
+ | Key | Shape | Notes |
195
+ |---|---|---|
196
+ | `categories` `ribbons` | `["Dresses"]` | display names; created if new, reused if not |
197
+ | `attributes` | `[{ name, options }]` or `{ <name>: [options] }` | declares the variant axes |
198
+ | `variations` | `[{ options: { <name>: <option> }, ...overrides }]` | the stocked combinations. **Omit it to auto-generate all** combinations, each inheriting the product-level price/sale fields |
199
+ | `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` |
201
+
202
+ 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).
187
203
 
188
- **`products`** is the one-call catalog bootstrap — the worked example and full semantics are in [`../post-installation.md` §3.1](../post-installation.md#31-the-products-payload). Each entry references categories/ribbons/attributes by **display name** (taxonomy is get-or-created: slugs/codes derived, existing records matched case-insensitively and reused, with the stored casing canonicalized into the product). `attributes: [{ name, options }]` (or `{ <name>: [options] }`) declares the variant axes; `variations: [{ options: { <name>: <option> }, ...overrides }]` lists the stocked combinations — omit it to auto-generate **all** combinations, each inheriting the product-level price/sale fields. A variation with its own `stock_quantity` gets `manage_stock: "yes"`; without one it draws on the parent's pooled stock (`"parent"`). Variation SKUs are synthesized from the parent SKU when absent. Parent `price`/`regular_price`/`on_sale` are rolled up from the cheapest publishable variant, `stock_status` derived, category/ribbon counts maintained — same helpers as `admin-products` `save`, but **no `product.created` webhooks are dispatched** (bootstrap precedes webhook subscribers; use `admin-products` for webhook-visible creates). Payload problems fail before any write as **400** `invalid_payload` with `errors: [{ path, error }]`; an explicit variation SKU already in use is **409** `duplicate_sku`; a mid-write failure rolls back everything the call created (**500** `catalog_seed_failed`) without touching reused taxonomy. **Re-runs converge**: a product whose `sku` (or, without one, derived slug) already exists is skipped and reported, so retries never duplicate. Limits: ≤100 products, ≤50 variations per product, ≤500 variations per call, ≤50 coupons/locations. `coupons` is a thin passthrough (code lowercased, skip-if-exists); **`locations`** creates Shipping & Tax Locations (`{ name, countries?: ["IL"], shipping_rates: [{ name, cost, free_over? }], tax_groups?: [{ name, rates: [{ name, rate }] }], shipping_tax?: { type: "percent"|"fixed", value } }` — the default `Products` tax group is added when missing; skip-if-exists by name). Passing any `locations` suppresses the seeded "Rest of the world" fallback — yours become the store's only locations; without them the free-shipping fallback is seeded. `with_sample_data: true` seeds the template's demo catalog instead (only when the store has zero products) and cannot be combined with the catalog keys.
204
+ **Limits:** ≤100 products, ≤50 variations per product, ≤500 variations per call, ≤50 coupons, ≤50 locations.
205
+
206
+ ### Payload — `locations`
207
+
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
+ ```
228
+
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).
230
+
231
+ ### Response
232
+
233
+ ```js
234
+ {
235
+ seeded: { settings_groups, gateways, locations }, // counts created by this call
236
+ sample_data: {…} | false, // same shape as `catalog`
237
+ catalog: { // null when no catalog keys were passed
238
+ categories, ribbons, attributes, terms, // each { created, reused }
239
+ products_created, products_skipped, variations_created,
240
+ coupons: { created, skipped }, locations: { created, skipped },
241
+ products: [ { name, id, slug, sku, variation_count }
242
+ | { name, skipped: true, reason: "sku_exists"|"slug_exists", existing_id } ],
243
+ } | null,
244
+ store_name: { value, action: "created"|"filled"|"unchanged"|"kept_existing" },
245
+ currency: { value, action: "created"|"updated"|"unchanged" } | null,
246
+ payment_methods: { enabled: [slug], disabled: [slug] } | null, // null when not passed
247
+ warnings: [ "no_catchall_location: …" ], // ALWAYS present; empty = nothing to flag
248
+ }
249
+ ```
189
250
 
190
- `{ seeded: { settings_groups, gateways, locations }, sample_data: {...} | false, catalog: { categories|ribbons|attributes|terms: { created, reused }, products_created, products_skipped, variations_created, coupons, locations, products: [{ name, id, slug, sku, variation_count } | { name, skipped: true, reason: "sku_exists"|"slug_exists", existing_id }] } | null, store_name: { value, action: "created" | "filled" | "unchanged" | "kept_existing" }, currency: { value, action: "created" | "updated" | "unchanged" } | null }`.
251
+ Record `products[].slug` those are the URLs your storefront routes to and read `warnings` unconditionally.