@base44/app-plugin-commerce 0.1.19 → 0.2.1

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/functions/commerce/admin-reports/entry.ts +1 -1
  3. package/base44/functions/commerce/seed-store/entry.ts +34 -0
  4. package/base44/functions/commerce/seed-store/seed-catalog.ts +39 -5
  5. package/base44/shared/commerce/card-payment.stripe.ts +178 -0
  6. package/base44/shared/commerce/scan.ts +1 -1
  7. package/base44/shared/commerce/sequence.ts +1 -1
  8. package/package.json +1 -1
  9. package/scripts/install.js +24 -14
  10. package/skills/commerce/SKILL.md +107 -51
  11. package/skills/commerce/docs/api-admin.md +89 -28
  12. package/skills/commerce/docs/api-storefront.md +113 -126
  13. package/skills/commerce/docs/entities.md +137 -0
  14. package/skills/commerce/install/01-install.md +101 -0
  15. package/skills/commerce/install/02-storefront.md +188 -0
  16. package/skills/commerce/install/03-data.md +162 -0
  17. package/skills/commerce/references/admin-product-form.md +10 -0
  18. package/skills/commerce/references/catalog-rendering.md +110 -0
  19. package/skills/commerce/references/emails.md +49 -12
  20. package/skills/commerce/references/guest-access-security.md +18 -5
  21. package/skills/commerce/references/online-payments.md +50 -149
  22. package/skills/commerce/references/operations.md +52 -0
  23. package/skills/commerce/references/reviews.md +31 -16
  24. package/skills/commerce/references/shipping-and-tax.md +110 -0
  25. package/skills/commerce/references/store-admin-agent.md +21 -0
  26. package/skills/commerce/references/store-settings.md +49 -0
  27. package/src/commerce/admin/README.md +2 -2
  28. package/src/commerce/admin/layout/AuthGuard.jsx +1 -1
  29. package/src/commerce/admin/pages/settings/InventorySettings.jsx +1 -1
  30. package/src/commerce/storefront/StorefrontProvider.jsx +106 -20
  31. package/src/commerce/storefront/blocks/AddToCartBlock.jsx +86 -0
  32. package/src/commerce/storefront/blocks/AddressFieldsBlock.jsx +96 -0
  33. package/src/commerce/storefront/blocks/BreadcrumbsBlock.jsx +52 -0
  34. package/src/commerce/storefront/blocks/CartLinesBlock.jsx +98 -0
  35. package/src/commerce/storefront/blocks/CheckoutBlock.jsx +247 -0
  36. package/src/commerce/storefront/blocks/CouponFieldBlock.jsx +84 -0
  37. package/src/commerce/storefront/blocks/OrderReceivedBlock.jsx +129 -0
  38. package/src/commerce/storefront/blocks/ProductGalleryBlock.jsx +66 -0
  39. package/src/commerce/storefront/blocks/ProductSpecsBlock.jsx +33 -0
  40. package/src/commerce/storefront/blocks/ProductStripBlock.jsx +55 -0
  41. package/src/commerce/storefront/blocks/QuantityStepper.jsx +62 -0
  42. package/src/commerce/storefront/blocks/ReviewsBlock.jsx +191 -0
  43. package/src/commerce/storefront/blocks/TotalsBlock.jsx +42 -0
  44. package/src/commerce/storefront/blocks/VariantSelectorBlock.jsx +81 -0
  45. package/src/commerce/storefront/blocks/index.js +44 -0
  46. package/src/commerce/storefront/index.js +59 -21
  47. package/src/commerce/storefront/internal/useAsyncData.js +86 -0
  48. package/src/commerce/storefront/pickers.jsx +20 -5
  49. package/src/commerce/storefront/useAddressForm.js +96 -0
  50. package/src/commerce/storefront/useCartLine.js +184 -0
  51. package/src/commerce/storefront/useCheckout.jsx +38 -11
  52. package/src/commerce/storefront/useProduct.js +227 -0
  53. package/src/commerce/storefront/useProductGallery.js +74 -0
  54. package/src/commerce/storefront/useProductList.js +153 -0
  55. package/src/commerce/storefront/useProductPrice.js +58 -0
  56. package/src/commerce/storefront/useProductReviews.js +242 -0
  57. package/src/commerce/storefront/useStorefrontSeo.js +204 -0
  58. package/src/commerce/storefront/useTotalsLines.js +109 -0
  59. package/src/commerce/utils/address-spec.js +89 -0
  60. package/src/commerce/utils/images.js +45 -0
  61. package/src/commerce/utils/index.js +18 -6
  62. package/src/commerce/utils/price.js +95 -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 +10 -2
  66. package/skills/commerce/installation-guidelines.md +0 -93
  67. package/skills/commerce/post-installation.md +0 -495
  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
@@ -13,17 +13,29 @@
13
13
  * checkout/return-page calls. Create ONE instance and import it everywhere.
14
14
  * - `variants.js` — variant selection: map attribute selections (Size, Color)
15
15
  * to a `ProductVariation` and back, per-option availability, variant price
16
- * ranges. See `.agents/skills/commerce/references/storefront-product-page.md`.
16
+ * ranges. See `.agents/skills/commerce/references/catalog-rendering.md`.
17
17
  * - `shipping-promos.js` — read the store's real free-shipping configuration so
18
18
  * "Free shipping over €150" copy states a configured rule, not an invented
19
19
  * number. See `.agents/skills/commerce/docs/api-storefront.md`.
20
+ * - `price.js` — `productPrice`: the from-price and price-range rules, encoded
21
+ * once so a card and a product page cannot disagree.
22
+ * - `totals.js` — `cartTotalsLines` / `orderTotalsLines` / `orderLines` /
23
+ * `attributesLabel`: one summary shape for the cart, the checkout and the
24
+ * confirmation (a cart's totals are nested, an order's are flat).
25
+ * - `address-spec.js` — `addressFieldSpec`: the checkout address form as data,
26
+ * with country/state options that are always arrays.
27
+ * - `images.js` — `productImages`: images normalized to `{src, name, alt}`.
20
28
  *
21
- * Building the storefront in React? `@/commerce/storefront` layers hooks on
22
- * top of this module a shared-cart provider, the guided-checkout hook
23
- * (automatic shipping/tax recalculation on address changes), headless
24
- * shipping/payment pickers and the order-received hook. Prefer those for cart
25
- * and checkout; use this module directly for catalog views and non-React code.
29
+ * Building the storefront in React? **Prefer `@/commerce/storefront`** it
30
+ * layers hooks and ready-made UI blocks on top of this module, and a hook that
31
+ * pre-composes these helpers is the difference between a rule that holds and a
32
+ * rule you have to remember. Use this module directly for non-React code, and
33
+ * inside your own custom logic.
26
34
  */
27
35
  export * from "./storefront.js";
28
36
  export * from "./variants.js";
29
37
  export * from "./shipping-promos.js";
38
+ export * from "./price.js";
39
+ export * from "./totals.js";
40
+ export * from "./address-spec.js";
41
+ export * from "./images.js";
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Price presentation — the one place the store's pricing *rules* turn into
3
+ * strings. Framework-free; `useProductPrice` binds it to the store currency.
4
+ *
5
+ * Two rules live here, and both are easy to get wrong in one view while
6
+ * getting them right in another:
7
+ *
8
+ * - **A variant parent's `price` is a *from* price** (rolled up from the
9
+ * cheapest publishable variant), so a card must render "From €19.99" — not
10
+ * "€19.99". Pass a `list-products` row and you get the "From" prefix for
11
+ * free; the alternative is remembering `product.attributes?.length` at every
12
+ * call site.
13
+ * - **An incomplete variant selection has a range, not a price.** Pass a
14
+ * `resolveSelection` view and an unresolved selection renders
15
+ * "€19.99 – €23.99" instead of silently showing the parent's from-price as
16
+ * if the customer had chosen it.
17
+ *
18
+ * Same call for both, so neither view can drift from the other:
19
+ *
20
+ * productPrice(row, { formatMoney }) // a card in a grid
21
+ * productPrice(view, { formatMoney }) // the product page, any selection
22
+ */
23
+
24
+ /** Is this a `resolveSelection` result rather than a raw product row? */
25
+ function isView(input) {
26
+ return Boolean(input) && typeof input === "object" && "display" in input && "isVariable" in input;
27
+ }
28
+
29
+ const money = (fn, n) => (typeof fn === "function" ? fn(n) : String(n ?? ""));
30
+
31
+ /**
32
+ * The rendered price of a product row **or** a resolved selection view.
33
+ *
34
+ * @param {object} rowOrView a `list-products`/`get-product` product, or a
35
+ * `resolveSelection` view
36
+ * @param {{formatMoney?: (n: number) => string}} [opts]
37
+ * @returns {{label: string, compareAtLabel: string|null, onSale: boolean,
38
+ * isFrom: boolean, isRange: boolean, min: number|null, max: number|null}}
39
+ * `label` is what to render as the price; `compareAtLabel` is the
40
+ * struck-through original, or null when the product is not on sale.
41
+ */
42
+ export function productPrice(rowOrView, { formatMoney } = {}) {
43
+ const fmt = (n) => money(formatMoney, n);
44
+ const blank = {
45
+ label: "", compareAtLabel: null, onSale: false, isFrom: false,
46
+ isRange: false, min: null, max: null,
47
+ };
48
+ if (!rowOrView) return blank;
49
+
50
+ if (isView(rowOrView)) {
51
+ const { display, priceRange, complete, isVariable } = rowOrView;
52
+ // An unresolved variant selection genuinely has a range of prices. Showing
53
+ // the parent's from-price here reads as "this is the price" — the single
54
+ // most common variant-pricing bug.
55
+ if (isVariable && !complete && priceRange && priceRange.min !== priceRange.max) {
56
+ return {
57
+ label: `${fmt(priceRange.min)} – ${fmt(priceRange.max)}`,
58
+ compareAtLabel: null,
59
+ onSale: Boolean(priceRange.on_sale),
60
+ isFrom: false,
61
+ isRange: true,
62
+ min: priceRange.min,
63
+ max: priceRange.max,
64
+ };
65
+ }
66
+ const price = display?.price ?? priceRange?.min ?? null;
67
+ const onSale = Boolean(display?.on_sale);
68
+ return {
69
+ label: price == null ? "" : fmt(price),
70
+ compareAtLabel: onSale && display?.regular_price != null ? fmt(display.regular_price) : null,
71
+ onSale,
72
+ isFrom: false,
73
+ isRange: false,
74
+ min: price,
75
+ max: price,
76
+ };
77
+ }
78
+
79
+ // A raw product row. `attributes` non-empty is the ONLY signal that a
80
+ // product sells variants (there is no product-type field), and it is what
81
+ // makes `price` a from-price.
82
+ const row = rowOrView;
83
+ const isFrom = Array.isArray(row.attributes) && row.attributes.length > 0;
84
+ const price = row.price ?? null;
85
+ const onSale = Boolean(row.on_sale);
86
+ return {
87
+ label: price == null ? "" : isFrom ? `From ${fmt(price)}` : fmt(price),
88
+ compareAtLabel: onSale && row.regular_price != null ? fmt(row.regular_price) : null,
89
+ onSale,
90
+ isFrom,
91
+ isRange: false,
92
+ min: price,
93
+ max: price,
94
+ };
95
+ }
@@ -72,10 +72,14 @@ export function createStorefront(base44, { storageKey = "cart_token", storage }
72
72
  listProducts(params = {}) {
73
73
  return inv("commerce/storefront-catalog", { action: "list-products", ...params });
74
74
  },
75
- /** getProduct("slug") or getProduct({ id }) */
76
- getProduct(ref) {
75
+ /**
76
+ * getProduct("slug") or getProduct({ id }). Optional `params` pass through
77
+ * to the action — `{ reviews_page, reviews_per_page }` page the reviews
78
+ * block that rides along with the product.
79
+ */
80
+ getProduct(ref, params = {}) {
77
81
  const by = typeof ref === "string" ? { slug: ref } : ref;
78
- return inv("commerce/storefront-catalog", { action: "get-product", ...by });
82
+ return inv("commerce/storefront-catalog", { action: "get-product", ...by, ...params });
79
83
  },
80
84
  /** ARRAY of root categories, subcategories nested under `children`. */
81
85
  listCategories() {
@@ -86,6 +90,46 @@ export function createStorefront(base44, { storageKey = "cart_token", storage }
86
90
  return inv("commerce/storefront-catalog", { action: "list-ribbons" }).then((d) => d.ribbons);
87
91
  },
88
92
 
93
+ // ── reviews (public: anyone can review with an email) ─────────────────
94
+ /**
95
+ * One page of a product's approved reviews plus its aggregates. Reviews
96
+ * ride along on `get-product`, so this is that call with the review
97
+ * paging params — use it to page the list without re-rendering the page.
98
+ *
99
+ * @returns {Promise<{items: object[], page: number, per_page: number,
100
+ * has_next: boolean, average_rating: number, rating_count: number}>}
101
+ */
102
+ getProductReviews(ref, { page = 1, per_page = 10 } = {}) {
103
+ const by = typeof ref === "string" ? { slug: ref } : ref;
104
+ return inv("commerce/storefront-catalog", {
105
+ action: "get-product",
106
+ ...by,
107
+ reviews_page: page,
108
+ reviews_per_page: per_page,
109
+ }).then((d) => d.reviews);
110
+ },
111
+ /**
112
+ * Submit a review. `email` is required for a guest and ignored for a
113
+ * signed-in caller (the session's email always wins, so nobody can
114
+ * review as someone else). `rating` is optional, 0–5.
115
+ *
116
+ * Resolves to `{ review_id, status, verified }` — **`status` is
117
+ * `"approved"` or `"hold"` depending on the store's `auto_approve_reviews`
118
+ * setting, so derive the confirmation copy from it** instead of assuming
119
+ * moderation. Rejects with `email_required` | `review_incomplete` |
120
+ * `invalid_rating` | `not_found`.
121
+ */
122
+ submitReview({ product_id, review, rating, reviewer, email } = {}) {
123
+ return inv("commerce/storefront-catalog", {
124
+ action: "submit-review",
125
+ product_id,
126
+ review,
127
+ ...(rating == null ? {} : { rating }),
128
+ ...(reviewer ? { reviewer } : {}),
129
+ ...(email ? { email } : {}),
130
+ });
131
+ },
132
+
89
133
  // ── cart (token handled internally; every call returns the full view) ─
90
134
  /** The current cart view, or null when there is none (an expired token self-clears). */
91
135
  async getCart() {
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Totals and line projections — one shape rendered by the cart page, the
3
+ * checkout summary AND the order-received page, so the three can't disagree.
4
+ *
5
+ * Why this exists: hand-written totals blocks reliably omit the lines that are
6
+ * usually zero. A summary that renders subtotal → shipping → total looks
7
+ * correct until the store issues a coupon or charges tax, at which point it
8
+ * silently stops adding up. Here `discount` and `tax` are always *produced*
9
+ * and merely flagged `hidden` when zero — a renderer that maps over the array
10
+ * cannot drop them, and one that wants to show a zero discount can.
11
+ *
12
+ * The other trap this absorbs: **a cart's totals are nested
13
+ * (`cart.totals.total`) and an order's are flat (`order.total`)** — there is no
14
+ * `order.totals`. `cartTotalsLines` and `orderTotalsLines` return the same
15
+ * shape from both, so one component renders either.
16
+ */
17
+
18
+ const num = (v) => (Number.isFinite(Number(v)) ? Number(v) : 0);
19
+ const money = (fn, n) => (typeof fn === "function" ? fn(n) : String(n ?? ""));
20
+
21
+ /**
22
+ * `[{ name, option }]` → `"Size: 42 · Color: Ivory"`. Cart lines and order
23
+ * lines both carry `attributes` as an **array of objects**, never a map — this
24
+ * is the label every cart, summary and confirmation needs, derived once.
25
+ *
26
+ * @param {Array<{name?: string, option?: string}>} attributes
27
+ * @param {{separator?: string}} [opts]
28
+ * @returns {string} empty for a product with no attributes
29
+ */
30
+ export function attributesLabel(attributes, { separator = " · " } = {}) {
31
+ if (!Array.isArray(attributes)) return "";
32
+ return attributes
33
+ .map((a) => (a?.name ? `${a.name}: ${a.option ?? ""}` : String(a?.option ?? "")))
34
+ .filter(Boolean)
35
+ .join(separator);
36
+ }
37
+
38
+ function line(key, label, amount, { formatMoney, emphasis = false, hideWhenZero = false }) {
39
+ const value = num(amount);
40
+ return {
41
+ key,
42
+ label,
43
+ amount: value,
44
+ formatted: money(formatMoney, value),
45
+ emphasis,
46
+ hidden: hideWhenZero && value === 0,
47
+ };
48
+ }
49
+
50
+ /**
51
+ * The priced cart's summary lines.
52
+ *
53
+ * @param {object} cart the cart view (`cart.totals` is where the numbers live)
54
+ * @param {{formatMoney?: (n: number) => string, labels?: object}} [opts]
55
+ * @returns {Array<{key: string, label: string, amount: number,
56
+ * formatted: string, emphasis: boolean, hidden: boolean}>}
57
+ * keys, in order: `subtotal` `discount` `shipping` `tax` `total`
58
+ */
59
+ export function cartTotalsLines(cart, { formatMoney, labels = {} } = {}) {
60
+ const t = cart?.totals ?? {};
61
+ const opt = { formatMoney };
62
+ return [
63
+ line("subtotal", labels.subtotal ?? "Subtotal", t.subtotal, opt),
64
+ line("discount", labels.discount ?? "Discount", -Math.abs(num(t.discount_total)), { ...opt, hideWhenZero: true }),
65
+ line("shipping", labels.shipping ?? "Shipping", t.shipping_total, opt),
66
+ line("tax", labels.tax ?? "Tax", t.total_tax, { ...opt, hideWhenZero: true }),
67
+ line("total", labels.total ?? "Total", t.total, { ...opt, emphasis: true }),
68
+ ];
69
+ }
70
+
71
+ /**
72
+ * The same lines for a placed order. **An order carries flat totals** —
73
+ * `order.total`, `order.shipping_total`, `order.total_tax`, `order.subtotal`,
74
+ * `order.discount_total` — with no `totals` object to dot into.
75
+ *
76
+ * @param {object} order
77
+ * @param {{formatMoney?: (n: number) => string, labels?: object}} [opts]
78
+ * @returns {Array<object>} same shape as `cartTotalsLines`
79
+ */
80
+ export function orderTotalsLines(order, { formatMoney, labels = {} } = {}) {
81
+ const o = order ?? {};
82
+ const opt = { formatMoney };
83
+ return [
84
+ line("subtotal", labels.subtotal ?? "Subtotal", o.subtotal, opt),
85
+ line("discount", labels.discount ?? "Discount", -Math.abs(num(o.discount_total)), { ...opt, hideWhenZero: true }),
86
+ line("shipping", labels.shipping ?? "Shipping", o.shipping_total, opt),
87
+ line("tax", labels.tax ?? "Tax", o.total_tax, { ...opt, hideWhenZero: true }),
88
+ line("total", labels.total ?? "Total", o.total, { ...opt, emphasis: true }),
89
+ ];
90
+ }
91
+
92
+ /**
93
+ * An order's line items in the same shape as decorated cart lines, so one
94
+ * component renders the bag, the checkout summary and the confirmation.
95
+ *
96
+ * @param {object} order
97
+ * @returns {Array<{name: string, attributesLabel: string, quantity: number,
98
+ * total: number, image: object|null, sku: string}>}
99
+ */
100
+ export function orderLines(order) {
101
+ return (order?.line_items ?? []).map((it) => ({
102
+ ...it,
103
+ name: it.name ?? "",
104
+ attributesLabel: attributesLabel(it.attributes ?? it.meta_data),
105
+ quantity: num(it.quantity),
106
+ total: num(it.total ?? it.subtotal),
107
+ image: it.image ?? null,
108
+ sku: it.sku ?? "",
109
+ }));
110
+ }
@@ -12,7 +12,7 @@
12
12
  * Framework-free, dependency-free, no I/O — feed them the `{ product,
13
13
  * variations }` pair from `commerce/storefront-catalog` `get-product` (the
14
14
  * admin's variation editor can use them too). See
15
- * `.agents/skills/commerce/references/storefront-product-page.md` for the UI rules.
15
+ * `.agents/skills/commerce/references/catalog-rendering.md` for the UI rules.
16
16
  *
17
17
  * A `selection` throughout is a plain object keyed by axis key (see
18
18
  * `attributeKey`): `{ "<color-attr-id>": "Red", "<size-attr-id>": "M" }`.
@@ -271,10 +271,18 @@ export function priceRange(product, variations, selection = {}) {
271
271
  * `{src, name, alt}` with a non-empty `src`, or `null` when there is nothing
272
272
  * renderable. Run gallery entries through it too: `product.images.map(normalizeImage)`.
273
273
  *
274
- * @param {object|null|undefined} img a stored image entry (`product.images[n]` or `variation.image`)
274
+ * A bare URL string is accepted too `seed-store` takes `images: ["https://…"]`,
275
+ * so both shapes turn up in code that moves catalog data around.
276
+ *
277
+ * @param {object|string|null|undefined} img a stored image entry
278
+ * (`product.images[n]` or `variation.image`), or a URL string
275
279
  * @returns {{src: string, name: string, alt: string}|null}
276
280
  */
277
281
  export function normalizeImage(img) {
282
+ if (typeof img === "string") {
283
+ const src = img.trim();
284
+ return src ? { src, name: "", alt: "" } : null;
285
+ }
278
286
  const src = typeof img?.src === "string" ? img.src.trim() : "";
279
287
  if (!src) return null;
280
288
  const name = img.name ?? "";
@@ -1,93 +0,0 @@
1
- # Installation Guidelines
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 `.agents/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 [`.agents/skills/commerce/SKILL.md`](./SKILL.md), alongside the API references in [`.agents/skills/commerce/docs/`](./docs/).
4
-
5
- > **If you are a Base44 agent working inside the runtime, read this first:**
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.
7
- > - **Don't grant admin roles.** Granting a user the `admin` role is not an install step for you; instead, validate that the admin-role gating (see [`post-installation.md`](./post-installation.md)) is preserved when you merge the admin routes into the app's router.
8
- > - **Don't read the whole codebase up front.** Copy the files, wire the router, and open only what your current task touches.
9
-
10
- ---
11
-
12
- ## 1. Static installation (copying the files)
13
-
14
- **Scripted.** Copy this entire repository into the target app at `examples/commerce/`, then run from the app root:
15
-
16
- ```bash
17
- node examples/commerce/scripts/install.js
18
- ```
19
-
20
- Relative to the script's own folder (`examples/commerce/scripts/`), it copies:
21
-
22
- | From (template) | To (app) |
23
- |---|---|
24
- | `../base44/entities/commerce.*.jsonc` | `../../../base44/entities/` |
25
- | `../base44/functions/commerce/` | `../../../base44/functions/commerce/` |
26
- | `../base44/shared/commerce/` | `../../../base44/shared/commerce/` |
27
- | `../base44/agents/commerce/` | `../../../base44/agents/commerce/` |
28
- | `../src/commerce/admin/` | `../../../src/commerce/admin/` |
29
- | `../src/commerce/utils/` | `../../../src/commerce/utils/` |
30
- | `../src/commerce/storefront/` | `../../../src/commerce/storefront/` |
31
- | `../skills/commerce/` | `../../../.agents/skills/commerce/` |
32
-
33
- Directories are merged: files owned by the template are overwritten (re-running after a template update is safe); everything else in your app is left untouched. Files the template has since **renamed or retired** are deleted on install (it reports each one) — otherwise stale guidance would sit in `.agents/skills/` forever, and agents read whatever is there. The skill folder carries all the documentation — `SKILL.md`, this file, `post-installation.md`, the topic references in `references/` and the API docs in `docs/` — so the installed app gets it at `.agents/skills/commerce/` where agents pick it up natively; the template repo itself also stays under `examples/commerce/` for reference.
34
-
35
- **Manual.** Equivalently, copy by hand:
36
-
37
- 1. Copy `base44/entities/commerce.*`, `base44/functions/commerce/*`, `base44/shared/commerce/*` and `base44/agents/commerce/*` into your app's `base44/` dir (merge, don't overwrite unrelated files). `shared/` is bundled into every function at deploy time.
38
- 2. Copy `src/commerce/admin/` → `src/commerce/admin/`, `src/commerce/utils/` → `src/commerce/utils/` (framework-free storefront helpers — API client, variant selection; no deps) and `src/commerce/storefront/` → `src/commerce/storefront/` (the storefront React layer — shared-cart provider, guided-checkout hook, headless pickers; needs React only).
39
- 3. Copy `skills/commerce/` → `.agents/skills/commerce/` (the commerce skill — `SKILL.md`, this file, `post-installation.md`, the `references/` topic guides and the `docs/` API references — for agents working on the app).
40
-
41
- Confirm your `base44/config.jsonc` `entitiesDir`/`functionsDir` point at these folders (the defaults do).
42
-
43
- ## 2. Deploy and wire up
44
-
45
- 1. `npx base44 entities push` — creates/updates the 20 entity schemas. *(CLI path only — the Base44 runtime deploys on write.)*
46
- 2. `npx base44 functions deploy` — deploys the 16 functions. *(CLI path only.)*
47
- 3. `npx base44 agents push` — registers the `commerce/StoreAdmin` agent (§3). *(CLI path only.)*
48
- 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.
49
- 5. Mount the admin router (see [`post-installation.md`](./post-installation.md)).
50
- 6. Grant your user the `admin` role.
51
- 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). The defaults include payment methods: the manual **`offline`** option enabled and **`card` switched off**. Cards are optional and not part of installing — enable them only with a payment provider wired, or about to be, or checkout answers `503 no_card_payment_provider` ([`post-installation.md`](./post-installation.md) §4.1). Skipping the call entirely leaves the operator the **Initialize store defaults** first-run screen. See [`post-installation.md`](./post-installation.md) §3; any seed call marks the business as ready and the first-run screen never appears.
52
-
53
- Check the install at any time:
54
-
55
- ```js
56
- const { data } = (await base44.functions.invoke("commerce/admin-tools", { action: "status" })).data;
57
- // → { template_version, seeded, settings_groups, counts: { "commerce.Product": n, ... },
58
- // checks: { has_payment_gateways, has_default_location } }
59
- ```
60
-
61
- `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:
62
-
63
- ```json
64
- { "success": false, "code": "schema_incompatible",
65
- "errors": [{ "entity": "commerce.Product", "error": "..." }] }
66
- ```
67
-
68
- 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).
69
-
70
- ---
71
-
72
- ## 3. The StoreAdmin agent (admin copilot)
73
-
74
- The template ships an AI copilot for store operators:
75
-
76
- - **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`.
77
- - **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.
78
- - **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.
79
- - **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.
80
- - **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.
81
- - **Config it can't change, it links to.** Store settings, Shipping & Tax Locations, gateways and webhook definitions have no function tool, so the agent is instructed to name the screen and emit an `admin:`-scheme link (`[Settings → Shipping & Tax](admin:settings/shipping-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.
82
-
83
- **Do not weaken the tool set.** The agent's power comes only from the admin functions' own `requireAdmin()` layer — don't add entity tools or service-role calls to the agent config, and keep `commerce/storefront-*` tools limited to the read-only catalog.
84
-
85
- ---
86
-
87
- ## 4. Building on it — the four storefront requirements
88
-
89
- If your work includes a customer-facing shopfront, the quick start in [`post-installation.md` §2](./post-installation.md#2-storefront-quick-start--logic-only) already walks all four requirements — you don't need any other reading. In short: attribute-level variant selectors resolved to a `variation_id`; shipping options presented and chosen; the payment methods the store actually enables rendered from `get-store-info` (a default store offers `offline` only — cards are off unless enabled with a provider wired, [`post-installation.md`](./post-installation.md) §4.1) with the card redirect handled if they're on, plus an `/order-received` page; and no offers the store isn't configured for. In a React app the shipped `@/commerce/storefront` hooks implement the shipping and order-received requirements outright — build on them rather than re-deriving the flow. The API enforces the first three — a storefront that skips them cannot complete a purchase. (Day-2 deep dives: [`SKILL.md` → *these four are not optional*](./SKILL.md) and [`docs/api-storefront.md`](./docs/api-storefront.md)'s *Required behaviors* table.)
90
-
91
- ## 5. Next steps
92
-
93
- 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, [`.agents/skills/commerce/SKILL.md`](./SKILL.md) is the map for all day-2 work.