@base44/app-plugin-commerce 0.1.3 → 0.1.5

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 (86) hide show
  1. package/README.md +11 -11
  2. package/base44/agents/commerce/StoreAdmin.jsonc +7 -7
  3. package/base44/entities/commerce.Order.jsonc +1 -1
  4. package/base44/entities/commerce.PaymentGateway.jsonc +3 -3
  5. package/base44/entities/commerce.Product.jsonc +9 -45
  6. package/base44/entities/commerce.ProductAttribute.jsonc +7 -19
  7. package/base44/entities/commerce.ProductAttributeTerm.jsonc +4 -10
  8. package/base44/entities/commerce.ProductTag.jsonc +1 -8
  9. package/base44/entities/commerce.ProductVariation.jsonc +1 -1
  10. package/base44/entities/commerce.StoreSettings.jsonc +1 -1
  11. package/base44/functions/commerce/admin-products/entry.ts +73 -121
  12. package/base44/functions/commerce/admin-refunds/entry.ts +1 -1
  13. package/base44/functions/commerce/admin-reports/entry.ts +1 -1
  14. package/base44/functions/commerce/admin-tools/entry.ts +4 -3
  15. package/base44/functions/commerce/payments/entry.ts +0 -2
  16. package/base44/functions/commerce/seed-store/defaults.ts +13 -39
  17. package/base44/functions/commerce/seed-store/entry.ts +50 -166
  18. package/base44/functions/commerce/seed-store/sample-data.ts +28 -66
  19. package/base44/functions/commerce/seed-store/seed-catalog.ts +717 -0
  20. package/base44/functions/commerce/storefront-cart/entry.ts +0 -7
  21. package/base44/functions/commerce/storefront-catalog/entry.ts +12 -21
  22. package/base44/functions/commerce/storefront-checkout/entry.ts +5 -12
  23. package/base44/shared/commerce/catalog.ts +134 -0
  24. package/base44/shared/commerce/coupons.ts +4 -7
  25. package/base44/shared/commerce/email-templates.ts +15 -13
  26. package/base44/shared/commerce/emails.ts +4 -3
  27. package/base44/shared/commerce/payments.ts +8 -20
  28. package/base44/shared/commerce/products.ts +24 -0
  29. package/base44/shared/commerce/settings.ts +2 -3
  30. package/base44/shared/commerce/stock.ts +2 -4
  31. package/base44/shared/commerce/totals.ts +37 -42
  32. package/package.json +1 -1
  33. package/scripts/install.js +23 -7
  34. package/skills/commerce/SKILL.md +9 -7
  35. package/skills/commerce/docs/api-admin.md +20 -14
  36. package/skills/commerce/docs/api-storefront.md +39 -35
  37. package/skills/commerce/installation-guidelines.md +8 -7
  38. package/skills/commerce/post-installation.md +205 -84
  39. package/skills/commerce/references/admin-product-form.md +56 -0
  40. package/skills/commerce/references/emails.md +4 -3
  41. package/skills/commerce/references/guest-access-security.md +2 -2
  42. package/skills/commerce/references/limits-and-performance.md +1 -1
  43. package/skills/commerce/references/online-payments.md +8 -8
  44. package/skills/commerce/references/product-render.md +25 -23
  45. package/skills/commerce/references/reviews.md +20 -0
  46. package/skills/commerce/references/storefront-product-page.md +9 -9
  47. package/skills/commerce/references/webhooks.md +3 -1
  48. package/src/commerce/admin/README.md +4 -4
  49. package/src/commerce/admin/context/BasePathContext.jsx +5 -5
  50. package/src/commerce/admin/context/SettingsContext.jsx +4 -4
  51. package/src/commerce/admin/index.jsx +3 -3
  52. package/src/commerce/admin/layout/Sidebar.jsx +1 -9
  53. package/src/commerce/admin/lib/constants.js +0 -7
  54. package/src/commerce/admin/lib/paths.js +6 -6
  55. package/src/commerce/admin/lib/product-utils.js +18 -15
  56. package/src/commerce/admin/pages/orders/components/AddProductDialog.jsx +8 -8
  57. package/src/commerce/admin/pages/products/ProductEditor.jsx +10 -12
  58. package/src/commerce/admin/pages/products/ProductsList.jsx +3 -15
  59. package/src/commerce/admin/pages/products/components/AttributesSection.jsx +426 -0
  60. package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +33 -69
  61. package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +1 -1
  62. package/src/commerce/admin/pages/products/components/tabs/LinkedTab.jsx +1 -14
  63. package/src/commerce/admin/pages/products/components/tabs/ModifiersTab.jsx +19 -0
  64. package/src/commerce/admin/pages/products/components/tabs/PriceInventoryTab.jsx +728 -0
  65. package/src/commerce/admin/pages/reports/Reports.jsx +3 -3
  66. package/src/commerce/admin/pages/settings/EmailsSettings.jsx +12 -14
  67. package/src/commerce/admin/pages/settings/GeneralSettings.jsx +6 -116
  68. package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +101 -62
  69. package/src/commerce/admin/pages/settings/SettingsLayout.jsx +2 -6
  70. package/src/commerce/admin/pages/settings/TaxSettings.jsx +0 -1
  71. package/src/commerce/admin/pages/status/WebhookEditor.jsx +3 -3
  72. package/src/commerce/admin/pages/status/Webhooks.jsx +2 -2
  73. package/src/commerce/admin/routes.jsx +10 -19
  74. package/src/commerce/utils/index.js +2 -2
  75. package/src/commerce/utils/shipping-promos.js +9 -6
  76. package/src/commerce/utils/variants.js +21 -21
  77. package/src/commerce/admin/pages/products/AttributeTerms.jsx +0 -180
  78. package/src/commerce/admin/pages/products/Attributes.jsx +0 -183
  79. package/src/commerce/admin/pages/products/Tags.jsx +0 -150
  80. package/src/commerce/admin/pages/products/components/tabs/AdvancedTab.jsx +0 -48
  81. package/src/commerce/admin/pages/products/components/tabs/AttributesTab.jsx +0 -208
  82. package/src/commerce/admin/pages/products/components/tabs/ExternalTab.jsx +0 -41
  83. package/src/commerce/admin/pages/products/components/tabs/GeneralTab.jsx +0 -103
  84. package/src/commerce/admin/pages/products/components/tabs/InventoryTab.jsx +0 -93
  85. package/src/commerce/admin/pages/products/components/tabs/ShippingTab.jsx +0 -86
  86. package/src/commerce/admin/pages/products/components/tabs/VariationsTab.jsx +0 -377
@@ -1,6 +1,6 @@
1
1
  # Installation Guidelines
2
2
 
3
- How to install the Base44 Commerce Template into an existing Base44 app. This file lives inside the **commerce skill** folder and is installed into the app at `skills/commerce/installation-guidelines.md`. Once the files are in place, continue with [`post-installation.md`](./post-installation.md) (embedding the admin pages, `AGENTS.md` registration); day-2 guidance lives in [`skills/commerce/SKILL.md`](./SKILL.md), alongside the API references in [`skills/commerce/docs/`](./docs/).
3
+ How to install the Base44 Commerce Template into an existing Base44 app. This file lives inside the **commerce skill** folder and is installed into the app at `skills/commerce/installation-guidelines.md`. Once the files are in place, continue with [`post-installation.md`](./post-installation.md) (embedding the admin pages, seeding the store's data, the storefront quick start); day-2 guidance lives in [`skills/commerce/SKILL.md`](./SKILL.md), alongside the API references in [`skills/commerce/docs/`](./docs/).
4
4
 
5
5
  > **If you are a Base44 agent working inside the runtime, read this first:**
6
6
  > - **Skip the `npx base44` commands.** The runtime deploys functions and pushes entities automatically the moment you write the files — writing a resource file *is* the deploy.
@@ -47,23 +47,24 @@ Confirm your `base44/config.jsonc` `entitiesDir`/`functionsDir` point at these f
47
47
  4. Check the app's `package.json` for `sonner`, `recharts` and `react-markdown`, and run `npm i` **only** for the ones actually absent — all three ship with the default Base44 template, so the normal outcome is no install at all. Do not re-install a package that is already a dependency. Nothing else is needed. Verify the shadcn primitives listed in [`src/commerce/admin/README.md`](../../src/commerce/admin/README.md) exist in your app.
48
48
  5. Mount the admin router (see [`post-installation.md`](./post-installation.md)).
49
49
  6. Grant your user the `admin` role.
50
- 7. Decide the store's data with the user — generate a real starter catalog, seed the generic demo data, or leave the store empty and let the operator hit **Initialize store defaults** on the admin's first-run screen. See [`post-installation.md`](./post-installation.md) §2; both seeded options mark the business as ready and the first-run screen never appears.
50
+ 7. Seed the store's data — one `commerce/seed-store` call creates the business defaults and, via its `products` payload, the whole catalog (or `with_sample_data: true` for the generic demo; or neither for defaults only). Skipping the call entirely leaves the operator the **Initialize store defaults** first-run screen. See [`post-installation.md`](./post-installation.md) §2; any seed call marks the business as ready and the first-run screen never appears.
51
51
 
52
52
  Check the install at any time:
53
53
 
54
54
  ```js
55
55
  const { data } = (await base44.functions.invoke("commerce/admin-tools", { action: "status" })).data;
56
- // → { template_version, seeded, settings_groups, counts: {...}, checks: [...] }
56
+ // → { template_version, seeded, settings_groups, counts: { "commerce.Product": n, ... },
57
+ // checks: { has_payment_gateways, has_default_zone } }
57
58
  ```
58
59
 
59
60
  `commerce/seed-store` is **idempotent** and starts with a **canary schema check**: it probe-writes one record per entity it will touch and deletes it. If you've modified an entity schema incompatibly, it aborts with HTTP 422 `schema_incompatible` and writes nothing:
60
61
 
61
62
  ```json
62
63
  { "success": false, "code": "schema_incompatible",
63
- "errors": [{ "entity": "Product", "error": "..." }] }
64
+ "errors": [{ "entity": "commerce.Product", "error": "..." }] }
64
65
  ```
65
66
 
66
- The admin setup screen surfaces these errors verbatim. Sample catalog data is only created when `with_sample_data: true` **and** the store has zero products.
67
+ The admin setup screen surfaces these errors verbatim. Sample catalog data is only created when `with_sample_data: true` **and** the store has zero products; a caller-supplied `products` catalog runs regardless, skipping (not duplicating) products whose SKU or slug already exists — see [`docs/api-admin.md`](./docs/api-admin.md#commerceseed-store).
67
68
 
68
69
  ---
69
70
 
@@ -74,7 +75,7 @@ The template ships an AI copilot for store operators:
74
75
  - **Agent definition** — [`base44/agents/commerce/StoreAdmin.jsonc`](../../base44/agents/commerce/StoreAdmin.jsonc), registered as **`commerce/StoreAdmin`** (the folder namespaces the agent, exactly like functions). The hosted runtime registers it when the file lands; on the CLI path run `npx base44 agents push`.
75
76
  - **Tools** — the `commerce/*` backend functions attached directly (`commerce/admin-products`, `commerce/admin-orders`, …, `commerce/seed-store`, plus read-only `commerce/storefront-catalog` for enumerating product variations). Tool calls run **with the chatting user's credentials**, so `requireAdmin()` in every admin function still authorizes the actual user — the agent has no entity tools and no service-role shortcut; a non-admin chatting with it gets `401/403` from every admin operation.
76
77
  - **No `model` field, on purpose.** The platform's default-model path accepts slash-namespaced tool names; explicitly setting a `model` currently rejects them (LLM tool names must match `^[a-zA-Z0-9_-]{1,128}$`). If you set a model, the bot fails at message time with a `tools.0.custom.name` error.
77
- - **Variant safety** — the agent is instructed to never auto-pick a variation: for variable products it fetches `{product, variations}` via `commerce/storefront-catalog get-product`, presents the variations as a table, and asks the operator which `variation_id` to use before touching an order, stock, or download grant.
78
+ - **Variant safety** — the agent is instructed to never auto-pick a variation: for a product carrying attributes it fetches `{product, variations}` via `commerce/storefront-catalog get-product`, presents the variants as a table, and asks the operator which `variation_id` to use before touching an order, stock, or download grant.
78
79
  - **Bot UI** — `src/commerce/admin/bot/` (chat panel; "StoreAdmin bot" launcher at the bottom of the admin sidebar). Responses render as markdown via `react-markdown`, with the agent's GFM tables (`| col |` with `|---|` separators) rendered by the template's own `bot/pipe-tables.js` — no markdown plugin dependency. The panel lives behind the same `AuthGuard` as the rest of the admin.
79
80
  - **Config it can't change, it links to.** Store settings, tax rates, shipping zones, gateways and webhook definitions have no function tool, so the agent is instructed to name the screen and emit an `admin:`-scheme link (`[Settings → Tax](admin:settings/tax)`) rather than telling the operator to "do it manually". `bot/Markdown.jsx` resolves `admin:` through `useAdminHref()`, so links follow your actual mount point (`basePath`) and navigate in-app instead of opening a tab. If you add screens, add the path to the table in the agent's instructions.
80
81
 
@@ -88,4 +89,4 @@ If your work includes a customer-facing shopfront, read **[`SKILL.md` → *If yo
88
89
 
89
90
  ## 5. Next steps
90
91
 
91
- Continue with [`post-installation.md`](./post-installation.md): embedding the admin pages (router mount, admin-role enforcement) and registering the template + skill in the app's `AGENTS.md`. After that, [`skills/commerce/SKILL.md`](./SKILL.md) is the map for all day-2 work.
92
+ Continue with [`post-installation.md`](./post-installation.md): embedding the admin pages (router mount, admin-role enforcement), seeding the store's data, and the logic-only storefront quick start. After that, [`skills/commerce/SKILL.md`](./SKILL.md) is the map for all day-2 work.
@@ -1,6 +1,6 @@
1
1
  # Post-installation
2
2
 
3
- What to do right after the static installation ([`installation-guidelines.md`](./installation-guidelines.md)): embed the admin pages into the app, decide with the user how the store's data gets created, and register the template + skill in `AGENTS.md`. Installed into the app at `skills/commerce/post-installation.md`.
3
+ What to do right after the static installation ([`installation-guidelines.md`](./installation-guidelines.md)): embed the admin pages into the app, seed the store's data, and build the storefront from the quick start below. Installed into the app at `skills/commerce/post-installation.md`.
4
4
 
5
5
  ---
6
6
 
@@ -15,13 +15,13 @@ The admin UI is a self-contained React app under `src/commerce/admin/`. Its only
15
15
  3. Mount the router:
16
16
  ```jsx
17
17
  import AdminApp from "@/commerce/admin";
18
- <Route path="/admin/*" element={<AdminApp />} />
18
+ <Route path="/store-admin/*" element={<AdminApp />} />
19
19
  ```
20
- **You must also build a payment return page** (`/order-received` by default) — this is **mandatory for payment links to work at all**. If your route differs, set it in Settings → General → *Payment return path*, or payment links will send customers to a 404. Every link (checkout, the admin's payment link, emails) returns there; without the route a paying customer hits a 404, and since confirming is what marks an order paid, orders would stay unpaid. The page is a thin wrapper over one backend call, `commerce/payments` `complete-return`, which confirms the payment and returns `{ state, order, payment_link }` for the paid / unpaid / cancelled cases — the contract and rules are in [`references/online-payments.md`](./references/online-payments.md).
20
+ **You must also build a payment return page** (`/order-received` by default) — this is **mandatory for payment links to work at all**. If your route differs, set it in Settings → General → *Payment return path*, or payment links will send customers to a 404. Every link (checkout, the admin's payment link, emails) returns there; without the route a paying customer hits a 404, and since confirming is what marks an order paid, orders would stay unpaid. The page is a thin wrapper over one backend call — step 4 of the [storefront quick start](#3-storefront-quick-start--logic-only) below covers it completely.
21
21
 
22
- **Give the app root something too.** A blank Base44 app has no `/` route, so after mounting only `/admin/*` the app's own URL still renders its "page not found" screen — which reads exactly like a broken install. Until a storefront exists, redirect: `<Route path="/" element={<Navigate to="/admin" replace />} />`.
22
+ **Give the app root something too.** A blank Base44 app has no `/` route, so after mounting only `/store-admin/*` the app's own URL still renders its "page not found" screen — which reads exactly like a broken install. Until a storefront exists, redirect: `<Route path="/" element={<Navigate to="/store-admin" replace />} />`.
23
23
 
24
- The **`/*` splat is required** — the admin renders its own nested routes, so a bare `path="/admin"` matches only the dashboard and every deeper link 404s. If you mount at a different base path, pass the prefix *without* the splat: `<AdminApp basePath="/store-admin" />` (a pattern passed by mistake, `basePath="/store-admin/*"`, is normalized rather than baked into every link). Opening the literal mount pattern `/admin/*` — pasted from these docs, or left in a hand-written nav link — lands on the dashboard instead of a "page not found" dead end; a genuinely wrong path like `/admin/ordrs` still 404s, with a link back.
24
+ The **`/*` splat is required** — the admin renders its own nested routes, so a bare `path="/store-admin"` matches only the dashboard and every deeper link 404s. If you mount at a different base path, pass the prefix *without* the splat: `<AdminApp basePath="/backoffice" />` (a pattern passed by mistake, `basePath="/backoffice/*"`, is normalized rather than baked into every link). Opening the literal mount pattern `/store-admin/*` — pasted from these docs, or left in a hand-written nav link — lands on the dashboard instead of a "page not found" dead end; a genuinely wrong path like `/store-admin/ordrs` still 404s, with a link back.
25
25
 
26
26
  ### Admin-role enforcement (do not weaken)
27
27
 
@@ -42,116 +42,237 @@ Even if the client guard were bypassed, layers 2 and 3 keep the store data safe.
42
42
 
43
43
  ---
44
44
 
45
- ## 2. Store data — ask the user before seeding anything
45
+ ## 2. Store data — seeding
46
46
 
47
- A fresh install has **no settings and no catalog**. Don't decide this silently and don't default to the demo catalog: **ask the user which of these three they want**, then do exactly that.
47
+ A fresh install has **no settings and no catalog**. One call to `commerce/seed-store` (admin-only, idempotent) initializes both. It always creates the business defaults — the six settings groups (`general`, `products`, `inventory`, `tax`, `shipping`, `emails`; USD, kg/cm, taxes off prices), the `offline` and `stripe` payment gateways, three tax classes and a free "Rest of the world" fallback shipping zone — and, depending on the payload, the catalog:
48
48
 
49
- > Suggested wording: *"Before you open the admin — should I generate a starter catalog for your store (products, descriptions, prices, images, categories, variants) based on what you're selling, seed the template's generic demo catalog so you can click around, or leave the store empty and set it up yourself? Generating one costs tokens — I write every product's copy and generate an image each — while the demo catalog is static and effectively free."*
49
+ | Mode | Body | Products created |
50
+ |---|---|---|
51
+ | **Real catalog** | `{ store_name, products: [...] }` (+ optional `coupons`, `tax_rates`) | Yours — categories, attributes, variants and all, in this one call |
52
+ | **Demo data** | `{ store_name, with_sample_data: true }` | The template's 10 generic demo products (skipped if any product exists) |
53
+ | **No products** | `{ store_name }` | None — defaults only |
50
54
 
51
- | Option | What you do | Result | Cost |
52
- |---|---|---|---|
53
- | **A — Generate the store's data** (best when you know the app's niche) | `commerce/seed-store` with `{ store_name, with_sample_data: false }`, then create real categories, attributes, products and images through the admin API (§2.1) | Store is **ready** — the admin opens straight onto a populated dashboard, no first-run screen | **Spends tokens**: you author every name, description and price, and image generation is billed per image |
54
- | **B — Default demo data** | `commerce/seed-store` with `{ store_name, with_sample_data: true }` | Store is **ready**, populated with the template's ~12 generic demo products | **Near-zero**: one function call, static content and stock image URLs |
55
- | **C — Nothing** | don't call `seed-store` at all | The admin shows its first-run **"Set up your store"** screen; the operator initializes defaults themselves | none |
55
+ `with_sample_data` cannot be combined with `products` (**400** `invalid_payload`). Not calling `seed-store` at all leaves the admin's first-run **"Set up your store"** screen for the operator — that screen keys off the `general` settings group, which the seed creates, so don't suppress it in code.
56
56
 
57
- Say the cost part out loud when you ask — it's the main trade-off between A and B, and the user is the one paying for it. A generated catalog of a dozen products with images is a real chunk of generation (text for every product plus one image call each); the static demo seed is a single backend call with no generation at all. If the user wants a populated store only to click around the admin, B is the better deal; A is worth it when the catalog is meant to survive into the real store.
57
+ **`store_name` is required on a first seed** (**400** `store_name_required`) — pass the app's name as the platform shows it (`base44/config.jsonc` → `name` can be stale; ask the user if unsure). It lands in `emails.store_name` and is the email subject/sender name and the public shop name (`get-store-info` → `settings.store_name`). On a re-run it fills a blank name but never overwrites one the merchant chose; the response reports which happened as `store_name: { value, action: "created" | "filled" | "unchanged" | "kept_existing" }`.
58
58
 
59
- **Both A and B must pass `store_name`** — the app's name as the platform shows it. It is required on a first seed (**400** `store_name_required` without it), because nothing server-side can read the app's name and an unnamed store sends subjects like `[]: New order #1002`. On an already-seeded store the call fills a blank name and never overwrites one the merchant chose; the response reports which happened as `store_name: { value, action }` (`created` · `filled` · `unchanged` · `kept_existing`). The sender name is separate and needs nothing: leave `emails.from_name` blank and Base44 sends as the app's name.
59
+ The call runs a canary schema check first — on **422** `schema_incompatible` fix the reported entities before continuing. Bad catalog payloads fail as **400** `invalid_payload` with an `errors: [{ path, error }]` list before anything is written.
60
60
 
61
- Both A and B call `seed-store`, which creates the business defaults — the seven settings groups (`general`, `products`, `inventory`, `downloadable`, `tax`, `shipping`, `emails`), payment gateways, tax classes and a fallback shipping zone. **Creating the `general` settings group is what marks the business as ready:** the admin's `SettingsProvider` (`src/commerce/admin/context/SettingsContext.jsx`) renders the first-run setup screen only while that group is missing. So under A and B the modal never appears — do **not** add a separate "ready" flag or suppress the screen in code; leave it working for option C, which is the only case it's meant for.
61
+ ### 2.1 The `products` payload
62
62
 
63
- Confirm afterwards:
63
+ Reference everything by **display name** — categories, tags, attributes and their options are get-or-created (slugs and codes derived, existing records matched case-insensitively and reused). Variants come from `attributes`: list each axis with the options the product comes in, and either pass explicit `variations` (only the combinations you stock, with per-variation overrides) or omit them to auto-generate **every combination**. Prices and the sale window are inherited from the product spec unless a variation overrides them; a variation with its own `stock_quantity` tracks it, one without draws on the parent's pooled `stock_quantity`.
64
64
 
65
65
  ```js
66
- const { data } = (await base44.functions.invoke("commerce/admin-tools", { action: "status" })).data;
67
- // → seeded: true (i.e. settings_groups includes "general"),
68
- // settings_groups: ["general", "products", ...], counts: { Product: n, ... }
66
+ await base44.functions.invoke("commerce/seed-store", {
67
+ store_name: "Aurora Threads",
68
+ products: [
69
+ { // simple product
70
+ name: "Classic T-Shirt",
71
+ sku: "TEE-CLASSIC", // optional, but makes re-runs idempotent
72
+ regular_price: 19.99,
73
+ stock_quantity: 50, // implies manage_stock: true
74
+ categories: ["Clothing"], // get-or-create by name
75
+ tags: ["bestseller"],
76
+ images: ["https://…/tee.jpg"], // URLs or { src, alt }
77
+ short_description: "A soft, breathable everyday tee.",
78
+ description: "<p>Cut from combed cotton…</p><ul><li>100% combed cotton</li><li>Pre-shrunk</li></ul>",
79
+ },
80
+ { // variant product — attributes declare the axes, variations the stocked combos
81
+ name: "Runner Sneaker",
82
+ sku: "SNK-RUN",
83
+ regular_price: 89, // inherited by variations that don't override
84
+ categories: ["Shoes"],
85
+ images: [{ src: "https://…/sneaker.jpg", alt: "Runner Sneaker, side view" }],
86
+ attributes: [
87
+ { name: "Size", options: ["41", "42", "43"] },
88
+ { name: "Color", options: ["Black", "White"] },
89
+ ],
90
+ default_options: { Size: "42", Color: "Black" }, // pre-selected combination
91
+ variations: [ // omit entirely → all 6 combos auto-generated
92
+ { options: { Size: "41", Color: "Black" }, stock_quantity: 4 },
93
+ { options: { Size: "42", Color: "Black" }, stock_quantity: 6 },
94
+ { options: { Size: "43", Color: "Black" }, stock_quantity: 2 },
95
+ { options: { Size: "42", Color: "White" }, regular_price: 94, stock_quantity: 3,
96
+ image: "https://…/sneaker-white.jpg" }, // give a visual axis per-variation images
97
+ ],
98
+ },
99
+ ],
100
+ coupons: [{ code: "WELCOME10", discount_type: "percent", amount: 10 }], // optional
101
+ });
69
102
  ```
70
103
 
71
- ### 2.1 Generating the store's data (option A)
72
-
73
- The goal is a catalog that looks like **this** business, not a demo. Order of operations:
74
-
75
- 1. **Defaults first** — `commerce/seed-store` with `{ with_sample_data: false, store_name: "<the app's name>" }`. `store_name` is **required** on a first seed (else **400** `store_name_required`) — use the app's name as the platform shows it (ask the user if unsure — `base44/config.jsonc` → `name` can be stale, e.g. `New App` for an app called `Canvas`), since the function cannot read it. It lands as the store name and, by inheritance, the "from" name on transactional emails. It's idempotent and runs a canary schema check; on `422 schema_incompatible` fix the reported entities before continuing. Never follow it with `with_sample_data: true` — you'd mix demo products into a real catalog (the sample seeder is skipped once any product exists, so this usually silently does nothing, which is worse).
76
- 2. **Business settings** — ask for, or infer from the app, the store name, currency, base country/state and units, and write them into the relevant `commerce.StoreSettings` groups (direct CRUD, one record per `group_id`; patch `values`, don't replace groups you weren't asked about).
77
- 3. **Taxonomy** — create `commerce.ProductCategory` (direct CRUD, nest via `parent_id`) and, if products vary, `commerce.ProductAttribute` + `commerce.ProductAttributeTerm` (e.g. Size, Color). Reuse one global attribute across products rather than duplicating per-product local ones.
78
- 4. **Products** — `commerce/admin-products` `save` (or `batch`, ≤100 items) per product. Write real merchandising copy: a distinct `name`, a one-line `short_description`, an HTML `description` (paragraph + `<ul>` of specifics), plus `sku`, `regular_price`, optional `sale_price`, `manage_stock: true` + `stock_quantity` where stock is tracked, `category_ids` and `images[]`. (`manage_stock` is a boolean on Product but `"yes"|"no"|"parent"` on ProductVariation.)
79
- 5. **Variable products — an attribute in the attributes list does NOTHING on its own.** This is the step agents get wrong: they create a global `Size` attribute with terms, and every product stays simple, unsellable as a variant. A product is only variable when **all four** of these are true, and the last two happen in the *same* `commerce/admin-products` `save` call:
80
- 1. `type: "variable"` on the product;
81
- 2. the attribute is listed **on the product** with `variation: true` and its `options` — not just in `commerce.ProductAttribute`;
82
- 3. a `variations` array is sent with one entry per combination you actually stock;
83
- 4. `default_attributes` names the combination to pre-select.
84
-
85
- ```js
86
- await call("admin-products", "save", {
87
- product: {
88
- name: "Runner Sneaker", type: "variable", status: "publish", sku: "SNK",
89
- regular_price: 89, // lowest variation price, so cards/filters work
90
- category_ids: [shoesId], images: [{ src: "…" }],
91
- attributes: [{ attribute_id: sizeAttrId, name: "Size", position: 0,
92
- visible: true, variation: true, options: ["41", "42", "43"] }],
93
- default_attributes: [{ attribute_id: sizeAttrId, name: "Size", option: "42" }],
94
- },
95
- variations: [
96
- { attributes: [{ attribute_id: sizeAttrId, name: "Size", option: "41" }],
97
- sku: "SNK-41", regular_price: 89, manage_stock: "yes", stock_quantity: 4, status: "publish" },
98
- { attributes: [{ attribute_id: sizeAttrId, name: "Size", option: "42" }],
99
- sku: "SNK-42", regular_price: 89, manage_stock: "yes", stock_quantity: 6, status: "publish" },
100
- { attributes: [{ attribute_id: sizeAttrId, name: "Size", option: "43" }],
101
- sku: "SNK-43", regular_price: 89, manage_stock: "yes", stock_quantity: 2, status: "publish" },
102
- ],
103
- });
104
- ```
104
+ What the seeder does per product: derives a unique slug, checks SKU uniqueness, prices variations (`sale_price` + optional `date_on_sale_from/to` supported at both levels), rolls the parent's `price`/`regular_price`/`on_sale` up from the cheapest publishable variant (never set a variant parent's price yourself — it's derived), sets `stock_status`, and maintains category/tag counts. Products default to `status: "publish"`; pass `"draft"` to review first. Other `commerce.Product` fields (`weight`, `dimensions`, `virtual`, `downloadable`, `downloads`, `meta_data`, …) pass through; unknown keys are rejected so typos surface instead of vanishing.
105
+
106
+ **Re-runs converge instead of duplicating**: a product whose `sku` (or, without one, derived slug) already exists is skipped and reported — safe for retries after a timeout, and for seeding into a store that already has products. Limits: ≤100 products and ≤500 variations per call, ≤50 variations per product (an auto-generated cartesian above that is rejected — pass explicit `variations`).
107
+
108
+ The response reports everything:
109
+
110
+ ```jsonc
111
+ { "seeded": { "settings_groups": 6, "gateways": 2, ... },
112
+ "catalog": {
113
+ "categories": { "created": 2, "reused": 0 }, "tags": { ... }, "attributes": { ... }, "terms": { ... },
114
+ "products_created": 2, "products_skipped": 0, "variations_created": 4,
115
+ "coupons": { "created": 1, "skipped": 0 }, "tax_rates": { "created": 0, "skipped": 0 },
116
+ "products": [
117
+ { "name": "Classic T-Shirt", "id": "…", "slug": "classic-t-shirt", "sku": "TEE-CLASSIC", "variation_count": 0 },
118
+ { "name": "Runner Sneaker", "id": "…", "slug": "runner-sneaker", "sku": "SNK-RUN", "variation_count": 4 }
119
+ ]
120
+ },
121
+ "store_name": { "value": "Aurora Threads", "action": "created" } }
122
+ ```
105
123
 
106
- Give a visual axis (Color) a per-variation `image`. `manage_stock` is `"yes"|"no"|"parent"` on a variation but a boolean on the product. **Then check your work**: re-read the product and assert `variations.length > 0` — a variable product with no variations cannot be added to a cart at all (`400 variation_required`), and the storefront has nothing to show.
107
- 6. **Images** — every product needs at least one. Use whatever image generation the app has available and store the returned URL, upload real assets with `base44.integrations.Core.UploadFile({ file })` → public URL, or fall back to stable public stock URLs (`base44/functions/commerce/seed-store/sample-data.ts` shows the `{ src, name, alt }` shape and a working Unsplash pattern). Match the image to the product — a generated catalog with mismatched or missing images reads as broken.
108
- 7. **Optional extras** — a launch coupon via `commerce/admin-coupons` `save`; tax rates via `commerce.TaxRate` direct CRUD if the user sells into taxed regions.
109
- 8. **Payments — once the store has something to sell.** Leave this until the catalog, shipping and settings are in place: connecting a payment provider is a step the *user* has to take in the platform dashboard, so asking for it first interrupts setup and leaves nothing to test the payment against. When the store is otherwise ready, raise it — don't wait to be asked.** Card payments are **already implemented** (hosted payment page, payment links, refunds); they only need a provider connected. The other gateways (`cod`, `bacs`, `cheque`) are **manual** — nobody pays online and someone reconciles by hand. So tell the user card payments are ready and ask them to connect the **Stripe** connector for the app, then **redeploy the backend functions** — the platform injects `STRIPE_SECRET_KEY` at deploy time, so a provider connected after the last deploy is invisible to already-deployed functions and the store still reports *no payment provider connected*. On a hosted app, edit any file under `base44/shared/commerce/` (bump the deploy marker at the top of `payments.ts`) to redeploy every `commerce/*` function; on the CLI, `npx base44 functions deploy`. Then confirm with `commerce/admin-tools` → `payment-connector-status`. Nothing to code. The card gateway is enabled by default and stays hidden from customers until a provider is connected, so enabling it early is safe. Declining is a legitimate choice — then switch that gateway off and make the manual flow explicit in the checkout copy. Details: [`references/online-payments.md`](./references/online-payments.md).
110
- 9. **Report back** — tell the user what you created (counts by category/type, currency, enabled gateways) and that the store is ready at `/admin`.
124
+ **Images**: every product needs at least one. Use whatever image generation the app has available and store the returned URL, upload real assets with `base44.integrations.Core.UploadFile({ file })` → public URL, or fall back to stable public stock URLs (`base44/functions/commerce/seed-store/sample-data.ts` shows a working Unsplash pattern). Match the image to the product.
111
125
 
112
- Keep the generated catalog small unless asked — roughly 8–15 products across 3–5 categories, with at least one variable product if the business plausibly has options. Cost scales with the catalog: every product is generated copy plus at least one generated image, so confirm before going past that range rather than quietly producing a 50-product store. Products are created as `status: "publish"` only if you set it; leave them `draft` if the user wants to review before going live.
126
+ A successful response means the data is in — the catalog and settings are live exactly as reported. Write any store-specific settings into `commerce.StoreSettings` (direct CRUD, one record per `group_id` — currency and units are the usual ones; patch `values`, don't replace groups you weren't asked about).
127
+
128
+ ### 2.2 Payments
129
+
130
+ Card payments are **already implemented** (hosted payment page, payment links, refunds) — make sure a payment provider (i.e. the platform's **Stripe** integration) is configured for the app, and the store takes cards; the `offline` gateway (manual reconciliation) works with nothing to configure. Provider internals, webhooks and refunds are day-2 material: [`references/online-payments.md`](./references/online-payments.md).
113
131
 
114
132
  ---
115
133
 
116
- ## 3. Register the template and skill in `AGENTS.md`
134
+ ## 3. Storefront quick start — logic only
135
+
136
+ No visitor UI ships; the storefront **API** is complete. The four chunks below are the whole happy path — product list → product page → cart → checkout — showing what to call, what comes back, and what to carry into the next step. Open [`docs/api-storefront.md`](./docs/api-storefront.md) only for what's beyond them (attribute/price filters, reviews, customer accounts, refunds), and [`references/product-render.md`](./references/product-render.md) for which fields belong in which view.
117
137
 
118
- So future agent sessions know the store exists and read the skill before touching it, **edit the app's existing `AGENTS.md` in place** — add the template to an **Installed templates** section and point at the skill from a **My Skills** section. Do **not** replace or overwrite the file: keep everything already in it and only append these two entries, creating either section only if it doesn't already exist (if a section is already there, add to it rather than duplicating it). The snippet below shows the entries to merge in, not a full-file replacement:
138
+ Every function returns the envelope `{ success, data }`; with the SDK the payload is `res.data.data`, so define one helper and use it everywhere:
119
139
 
120
- ```md
121
- ## Installed templates
140
+ ```js
141
+ const inv = (fn, payload) => base44.functions.invoke(fn, payload).then((r) => r.data.data);
142
+ ```
122
143
 
123
- - commerce
144
+ The cart is identified by a **`cart_token`** the backend mints — persist it in `localStorage` and send it with every cart/checkout call. Rolling 48 h expiry, refreshed on every touch.
124
145
 
125
- ## My Skills
146
+ ### 3.1 Product list
126
147
 
127
- - `skills/commerce/SKILL.md` — Base44 Commerce template: 24 `commerce.*` entities, 16 `commerce/*` backend functions (storefront + admin APIs + online payments), the shared commerce engine under `base44/shared/commerce/`, the store admin UI mounted at `/admin`, and the `commerce/StoreAdmin` agent (admin copilot bot in the admin sidebar). Read before working on store features — catalog, cart, checkout, orders, payments, emails, webhooks, or the admin UI.
148
+ ```js
149
+ const info = await inv("commerce/storefront-catalog", { action: "get-store-info" });
150
+ // info.settings → { store_name, currency, currency_position, num_decimals, … } — format money with these
151
+ // info.payment_gateways → [{ slug, title, description, online }] — you'll need this at checkout
152
+ // info.countries / info.currencies → static tables for address forms and money display
153
+
154
+ const { products, page, per_page, has_next } = await inv("commerce/storefront-catalog", {
155
+ action: "list-products",
156
+ page: 1, per_page: 12, // optional: search, category_id, tag_id, featured, on_sale,
157
+ sort: "-created_date", // min_price, max_price, in_stock_only
158
+ }); // sort: -created_date | name | price | -price | popularity | rating
128
159
  ```
129
160
 
130
- ---
161
+ Each row is a full product record — for a card use `name`, `images[0]?.src`, `price`, `regular_price`, `on_sale`, `short_description`, `stock_status`, `average_rating`/`rating_count` (stars cost no extra call) and `tags` (`[{ id, name }]`, may be absent). **There is no product type flag**: `product.attributes?.length > 0` means the product sells variants and its `price` is a *from*-price rolled up from the cheapest variant — render it as "From …". Categories for the nav come from `{ action: "list-categories" }` (a tree via `parent_id`).
162
+
163
+ **Carry forward:** each card links to the product page by **`slug`**.
164
+
165
+ ### 3.2 Product page — variant selection included
166
+
167
+ ```js
168
+ const { product, variations, categories, tags, reviews } =
169
+ await inv("commerce/storefront-catalog", { action: "get-product", slug }); // or { id }
170
+
171
+ // One selector PER product.attributes[] entry — never a flat list of variations.
172
+ import { defaultSelection, selectOption, resolveSelection } from "@/commerce/utils";
131
173
 
132
- ## 4. Reviews — build them unless the user rules it out
174
+ let selection = defaultSelection(product, variations); // merchant defaults + single-option axes
175
+ // on user pick: selection = selectOption(product, variations, selection, axisKey, option);
133
176
 
134
- **Unless you are sure the store doesn't want reviews, implement them: the backend already exists.** Skipping them is the common miss, and it costs the store the social proof most shoppers look for before buying.
177
+ const view = resolveSelection(product, variations, selection);
178
+ // view.axes → [{ key, name, options }] — render one control each
179
+ // view.availability → { [axisKey]: { [option]: "available" | "out_of_stock" | "unavailable" } }
180
+ // view.display → { price, regular_price, on_sale, sku, stock_status, image, … } for the selection
181
+ // view.purchasable → gate the Add-to-cart button on this
182
+ // view.addToCart → { product_id, variation_id } — null until the selection resolves
183
+ ```
135
184
 
136
- What ships, with nothing to write on the backend:
185
+ Add to cart — **create** mints the `cart_token` on first add; every later call reuses it:
137
186
 
138
- - `commerce/storefront-catalog` `get-product` returns **paginated `reviews`** plus `average_rating` and `rating_count`, and a `verified` flag per review;
139
- - `list-products` rows carry `average_rating`/`rating_count`, so **stars on cards cost no extra call**;
140
- - `commerce/storefront-catalog` `submit-review` accepts `{ product_id, reviewer?, review, rating }` from a **signed-in** customer — the email comes from the session, never the payload;
141
- - `commerce/storefront-account` `my-reviews` lists a customer's own;
142
- - moderation is already in the admin (Products → Reviews), and `commerce/admin-reviews` recalculates the product's rating on every status change.
187
+ ```js
188
+ let cart_token = localStorage.getItem("cart_token");
189
+ if (!cart_token) {
190
+ const cart = await inv("commerce/storefront-cart", { action: "create" });
191
+ cart_token = cart.cart_token;
192
+ localStorage.setItem("cart_token", cart_token);
193
+ }
194
+ await inv("commerce/storefront-cart", { action: "add-item", cart_token, ...view.addToCart, quantity: 1 });
195
+ ```
143
196
 
144
- So the work is UI: stars on cards and the product page, a reviews list, and a submit form. Behaviors to respect rather than fight:
197
+ A product with attributes is **rejected without a `variation_id`** (`400 variation_required`) — that is why `view.addToCart` and not a bare `product_id` goes into the call. Show `reviews` (`{ items, has_next, average_rating, rating_count }`) and the `upsells`/`cross_sells` summaries the same response carries.
145
198
 
146
- - anonymous callers get **`401 login_required`** — put the form behind a login prompt instead of hiding reviews from guests;
147
- - a new review is **`hold`** unless `auto_approve_reviews`, so tell the submitter it's awaiting approval rather than showing it as live;
148
- - `only_verified_reviews` requires the customer to have a `processing`/`completed` order with that product (**`403 verified_only`**), and `review_rating_required` makes the rating mandatory (**`400 rating_required`**);
149
- - the store can switch reviews off (`products.enable_reviews`) or per product (`reviews_allowed`) — read those and hide the UI when false.
199
+ **Carry forward:** the **`cart_token`**.
200
+
201
+ ### 3.3 Cart
202
+
203
+ **Every cart action returns the same full priced view**, so re-render from whatever the last call returned — no separate refresh:
204
+
205
+ ```js
206
+ let cart = await inv("commerce/storefront-cart", { action: "get", cart_token });
207
+ // cart.items → [{ item_key, name, image, quantity, price, subtotal, total, attributes, purchasable }]
208
+ // cart.totals → { subtotal, discount_total, shipping_total, cart_tax, total_tax, total, … }
209
+ // cart.coupon_notices / cart.removed_items → tell the customer what auto-dropped and why
210
+
211
+ cart = await inv("commerce/storefront-cart", { action: "update-item", cart_token, item_key, quantity }); // ≤0 removes
212
+ cart = await inv("commerce/storefront-cart", { action: "remove-item", cart_token, item_key });
213
+ cart = await inv("commerce/storefront-cart", { action: "apply-coupon", cart_token, code });
214
+ ```
215
+
216
+ Shipping is chosen **on the cart, before place-order** — this is the step storefronts most often skip, and `place-order` refuses without it (`400 shipping_method_required`):
217
+
218
+ ```js
219
+ cart = await inv("commerce/storefront-cart", { action: "set-shipping-address", cart_token,
220
+ address: { country, state, postcode, city } });
221
+ switch (cart.shipping_status) {
222
+ case "auto_selected": // one option — already applied, nothing to do
223
+ case "chosen": break; // customer's earlier choice still valid
224
+ case "choice_required": // MUST render cart.available_shipping_methods [{ id, title, cost }]
225
+ cart = await inv("commerce/storefront-cart", { action: "choose-shipping-method", cart_token,
226
+ method_id: picked.id }); // the entry's id, not its method_id type
227
+ break;
228
+ case "none_available": /* the store doesn't ship there — say so, don't place the order */ break;
229
+ case "not_needed": break; // fully virtual cart
230
+ }
231
+ ```
232
+
233
+ **Carry forward:** the **`cart_token`** (address and method choice live on the cart).
234
+
235
+ ### 3.4 Checkout & order-received
236
+
237
+ Payment methods are `info.payment_gateways` from step 1 — render them as they come (`online: true` marks the card/redirect gateway; `offline` is manual reconciliation). Make sure a payment provider (i.e. Stripe) is configured for the app. Then one call places the order:
238
+
239
+ ```js
240
+ // card payments ride the app's Stripe integration — setup & provider details:
241
+ // skills/commerce/references/online-payments.md
242
+ const res = await inv("commerce/storefront-checkout", {
243
+ action: "place-order", cart_token,
244
+ payment_method: gateway.slug, // from info.payment_gateways
245
+ billing: { first_name, last_name, address_1, city, country, email }, // the required set; phone, state, postcode optional
246
+ // shipping: { … } if it differs from billing; customer_note?; return_url: window.location.origin
247
+ });
248
+ // res → { order_id, order_number, order_key, status, totals, order,
249
+ // payment_instructions, // offline: { description, account_details } — render them
250
+ // payment } // online: { status: "requires_payment", checkout_url, … } | null
251
+
252
+ if (res.payment?.status === "requires_payment") window.location.href = res.payment.checkout_url;
253
+ else showConfirmation(res); // offline order placed — show payment_instructions
254
+ ```
255
+
256
+ Every payment link returns to **`/order-received`** — the page from §1 step 3. It is one call, idempotent, safe on every visit:
257
+
258
+ ```js
259
+ // GET /order-received?order_id=…&order_key=…&payment=success|cancel
260
+ const params = new URLSearchParams(window.location.search);
261
+ const { state, order, payment_link } = await inv("commerce/payments", {
262
+ action: "complete-return",
263
+ order_id: params.get("order_id"), order_key: params.get("order_key"),
264
+ payment: params.get("payment"), // only a hint — the server verifies with the provider
265
+ return_url: window.location.origin,
266
+ });
267
+ // state === "paid" → thank-you + order summary (order is now marked paid)
268
+ // state === "unpaid" → offer payment_link.url to try again
269
+ // state === "cancelled" → payment was cancelled — offer payment_link.url or support
270
+ ```
150
271
 
151
- Shapes and error codes: [`docs/api-storefront.md`](./docs/api-storefront.md#submit-review). Where ratings belong per view: [`references/product-render.md`](./references/product-render.md).
272
+ **Carry forward:** `order_id` + `order_key` are the guest's proof of ownership — `commerce/storefront-account` `get-order` with both returns the order for a tracking page (signed-in customers get `my-orders` with no key).
152
273
 
153
274
  ---
154
275
 
155
- ## 5. Next
276
+ ## 4. Next
156
277
 
157
- Continue with the commerce skill — [`skills/commerce/SKILL.md`](./SKILL.md) — for day-2 work: UI changes, storefront building (start with [`references/product-render.md`](./references/product-render.md) for what to render, then [`references/storefront-product-page.md`](./references/storefront-product-page.md) for variants), Stripe wiring, scheduled maintenance, emails, webhooks, and operational limits.
278
+ Continue with the commerce skill — [`skills/commerce/SKILL.md`](./SKILL.md) — for day-2 work: UI changes, deeper storefront features ([`references/product-render.md`](./references/product-render.md) for what to render per view, [`references/storefront-product-page.md`](./references/storefront-product-page.md) for variant edge cases, [`references/reviews.md`](./references/reviews.md) for the ready-made reviews backend), Stripe wiring, scheduled maintenance, emails, webhooks, and operational limits.
@@ -0,0 +1,56 @@
1
+ # The admin product form
2
+
3
+ Where to change what, in `src/commerce/admin/pages/products/`. The shape follows one
4
+ rule: **a product sells variants because it carries attributes** — there is no product
5
+ type — so everything a customer actually buys is edited in one place.
6
+
7
+ ## Tabs
8
+
9
+ `components/ProductDataPanel.jsx` builds the list. There are four, and two are conditional:
10
+
11
+ | Tab | File | Holds |
12
+ |---|---|---|
13
+ | **Price & Inventory** | `components/tabs/PriceInventoryTab.jsx` | Tax status/class and *limit to 1 per order* (product-wide) → the **Attributes** section → a row per purchasable thing |
14
+ | **Modifiers** | `components/tabs/ModifiersTab.jsx` | `meta_data` key/value pairs — descriptive properties (Material, Care, GTIN). They never affect price, stock or variants |
15
+ | **Downloads** | `components/tabs/DownloadsTab.jsx` | Only when `downloadable` |
16
+ | **Linked products** | `components/tabs/LinkedTab.jsx` | Upsells and cross-sells |
17
+
18
+ `Virtual` and `Downloadable` are checkboxes in the panel header, not tabs. There is no
19
+ General, Inventory, Variations, Attributes, Shipping, Advanced or External tab — all of
20
+ those were removed, and `scripts/install.js` retires the files on upgrade.
21
+
22
+ ## Price & Inventory
23
+
24
+ - **Rows.** A product with no attributes shows one collapsed **Base price** row, which
25
+ edits the product's own fields. A product with attributes shows a row per variant.
26
+ Both go through the same `PriceFields` / inventory / `ShippingFields` / `DownloadsFields`
27
+ components, so there is one definition to change, not two.
28
+ - **Shipping is per row.** Weight, dimensions and shipping class live on each variant (a
29
+ large mug weighs more than a small one); the Base price row edits the product's own.
30
+ Hidden when the product is virtual.
31
+ - **The parent price is not editable** when variants exist — `commerce/admin-products`
32
+ derives `regular_price`, `price` and `on_sale` from the cheapest publishable variant on
33
+ save, and the seeder does the same. Don't add an input for it.
34
+ - **Copy from.** Pick a variant, tick Price / Inventory / Shipping, push it onto the rest.
35
+ `sku` and `image` are deliberately excluded — a copied SKU fails the save with
36
+ `duplicate_sku`.
37
+
38
+ ## Attributes drive the variants
39
+
40
+ `components/AttributesSection.jsx` edits `product.attributes[]`; the variant list
41
+ reconciles itself in a `useEffect` in `PriceInventoryTab.jsx`:
42
+
43
+ - adding a value adds the missing combinations, **inheriting the first existing variant** —
44
+ or the product's own price when there is none yet, so adding Size to a €19.99 mug gives
45
+ priced variants;
46
+ - removing a value drops the combinations it made impossible.
47
+
48
+ So there is no *Generate variants* button and no per-variant delete: the attributes are the
49
+ control. Reconciliation is keyed on an attribute signature and **skipped on first render** —
50
+ a product whose stored variants don't match its attributes must not be rewritten before the
51
+ merchant touches anything. Keep that guard if you touch the effect.
52
+
53
+ Attribute and value *records* are shared between products, so the **Manage attributes**
54
+ dialog saves them immediately, unlike the rest of the form which batches until Save. There
55
+ are no Attributes or Tags pages in the side menu: attributes are reachable only from here,
56
+ tags from the `TaxonomyPanel` card in the product sidebar.
@@ -5,9 +5,10 @@ Transactional email is sent via `base44.integrations.Core.SendEmail` from the sh
5
5
  - Per-type enable/subject/heading/recipient/additional_content overrides live at the **top level** of the `emails` StoreSettings group, keyed by type id (`emails.new_order.enabled`) — that is the path `shared/commerce/emails.ts` reads. Editable in Settings → Emails; blank = built-in default. Don't nest them under a sub-object: the backend won't see them.
6
6
  - Admin notifications (`new_order`, `cancelled_order`, `failed_order`) resolve in three steps: the per-type `recipient` if it holds an address, else `emails.admin_recipients`, else — at send time — **the app's users with `role: "admin"`** (`sr.entities.User.filter({role:"admin"})`, memoized per isolate). Both settings accept a comma-separated list and drop blank entries, so a cleared per-type override falls back rather than sending to an empty address. Nothing is seeded: a fresh store notifies its admin users until someone sets explicit recipients, and promoting an admin is enough to add them. Only when there is also no admin user does the email go nowhere, logged in `commerce.EmailLog` with `success: false` and an `error` naming what was empty. Settings → Emails shows the addresses actually in effect (via `commerce/admin-tools` `admin-email-recipients`).
7
7
  - Stock notifications follow the same chain: `inventory.notification_recipient`, else the first `emails.admin_recipients` entry, else the first admin user.
8
- - The **"from" name** is `emails.from_name`, and it seeds **blank on purpose**: it inherits `general.store_name`, and when both are blank `SendEmail` is called without `from_name`, in which case **Base44 sends as the app's name** (verified: an app named *Canvas* with both blank delivered mail from "Canvas"). So the sender is the app name unless the store overrides it — one place to rename, and no hardcoded default. Settings → Emails shows the inherited value as the field's placeholder.
9
- - That fallback does **not** reach subjects or headings: those are rendered by this template, which has no access to the app name, so a store with no `general.store_name` sends `New order #1002` rather than `[Canvas]: New order #1002`. Set the store name if you want it in the subject line.
8
+ - The **store name** is `emails.store_name` — one setting, used for both the `{store_name}` in subjects/headings and the sender name. It lives nowhere else, but it is not email-only: `storefrontSafeSettings()` publishes it as `settings.store_name` on `get-store-info`, so it is also the public shop name. `commerce/seed-store` fills it from the `store_name` it is passed, and Settings → Emails is where a merchant changes it.
9
+ - It seeds **blank on purpose**: a blank name means `SendEmail` is called without `from_name`, in which case **Base44 sends as the app's name** (verified: an app named *Canvas* with it blank delivered mail from "Canvas"). So the sender is the app name unless the store overrides it — one place to rename, and no hardcoded default.
10
+ - That fallback does **not** reach subjects or headings: those are rendered by this template, which has no access to the app name, so a store with no `emails.store_name` sends `New order #1002` rather than `[Canvas]: New order #1002`. Set the store name if you want it in the subject line.
10
11
  - Subjects and headings tolerate a **blank** store name: `{store_name}` substitution drops empty brackets and collapses the gap, so an unnamed store sends `New order #1002`, never `[]: New order #1002`.
11
12
  - `emails_sent[]` on each order dedupes lifecycle emails so a re-entered status won't re-send.
12
- - Deliverability (SPF/DKIM) and the sending address depend on your Base44 email configuration — the store only sets `from_name`. Every send is recorded in the `commerce.EmailLog` entity.
13
+ - Deliverability (SPF/DKIM) and the sending address depend on your Base44 email configuration — the store only sets the sender *name*. Every send is recorded in the `commerce.EmailLog` entity.
13
14
  - **Reading `commerce.EmailLog`:** one row per address per attempt. `target` is `admin` or `customer` — the same split the Recipient column shows in Settings → Emails (stock notifications count as `admin`). `order_id` is the id **users are shown**: the `order_number` from the admin Orders list and every email subject (`#1023` → `1023`). The internal Order record id is in `order_record_id`, which is what the admin routes on. Rows written before these fields existed still carry the record id in `order_id` and have no `target`; they are not backfilled.
@@ -1,8 +1,8 @@
1
1
  # Guest access & security
2
2
 
3
3
  - Storefront functions are **public** (anonymous invocation); confirm your app allows unauthenticated function calls. Checkout accepts guests unconditionally — there is no store setting that forces login. A login-required store has to enforce that in the shopfront you build (and, if it must not be bypassable, by adding the check to `commerce/storefront-checkout` `place-order`).
4
- - **All entities are admin-only RLS**, including the catalog (Product, ProductVariation, categories, tax/shipping config, payment gateways). Storefronts read the catalog through `commerce/storefront-catalog` (service role), never by querying entities directly with the client SDK — a direct read from a non-admin is rejected by the backend. This is deliberate: a world-readable catalog row exposes more than a shopper should see (`downloads[].file_url`, `purchase_note`, draft/hidden products, stock and sales internals, gateway `settings` such as BACS bank details). Do **not** relax any catalog entity's read RLS to `true`; if a shopfront needs a field, surface it through the storefront function instead.
5
- - The storefront catalog function strips the paywalled fields (`downloads`, `download_limit`, `download_expiry`, `purchase_note`) from every product/variation it returns. Downloadable files are reached only through `commerce/storefront-account` `get-download`, which enforces ownership, download limits, expiry and short-lived signed URLs — keep that the only path to a `file_url`.
4
+ - **All entities are admin-only RLS**, including the catalog (Product, ProductVariation, categories, tax/shipping config, payment gateways). Storefronts read the catalog through `commerce/storefront-catalog` (service role), never by querying entities directly with the client SDK — a direct read from a non-admin is rejected by the backend. This is deliberate: a world-readable catalog row exposes more than a shopper should see (`downloads[].file_url`, draft/hidden products, stock and sales internals, gateway `settings` such as the offline gateway's bank details). Do **not** relax any catalog entity's read RLS to `true`; if a shopfront needs a field, surface it through the storefront function instead.
5
+ - The storefront catalog function strips the paywalled fields (`downloads`, `download_limit`, `download_expiry`) from every product/variation it returns. Downloadable files are reached only through `commerce/storefront-account` `get-download`, which enforces ownership, download limits, expiry and short-lived signed URLs — keep that the only path to a `file_url`.
6
6
  - `cart_token` and `order_key` are **bearer credentials** — possession grants access to that cart/order. Always serve over HTTPS; don't log them; treat them like secrets.
7
7
  - Carts and orders have admin-only RLS; customers never touch those entities directly — all access is mediated by `commerce/storefront-*` functions using the service role after verifying the caller.
8
8
  - The admin side has three enforcement layers (UI guard, entity RLS, `requireAdmin()` function guard) — see [`post-installation.md`](../post-installation.md); never weaken them.
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Limits & concurrency
4
4
 
5
- - **Pagination.** The SDK `filter`/`list` cap out at 5,000 records/page and there is **no total-count API**. Server-side scans use paged loops (`shared/scan.ts` `scanAll`, page size 500). Admin lists use limit+skip with a `limit+1` "has-next" probe — the UI shows *Page N ‹ ›*, never a total.
5
+ - **Pagination.** The SDK `filter`/`list` cap out at 5,000 records/page and there is **no total-count API**. Server-side scans use paged loops (`base44/shared/commerce/scan.ts` `scanAll`, page size 500). Admin lists use limit+skip with a `limit+1` "has-next" probe — the UI shows *Page N ‹ ›*, never a total.
6
6
  - **Search** is server-side (`search` actions scan + JS-filter) because entity `filter` is exact-match only.
7
7
  - **Reports** scan orders on demand — fine to ~10k orders per range. Beyond that, materialize an `OrderStats` entity updated on each order transition and aggregate from it (sketch: one record per day per status with summed totals; `commerce/admin-reports` reads the pre-aggregated rows instead of scanning `commerce.Order`).
8
8
  - **No transactions.** A few consequences, all documented in code: