@base44/app-plugin-commerce 0.1.0

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 (173) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +117 -0
  3. package/base44/agents/commerce/StoreAdmin.jsonc +64 -0
  4. package/base44/entities/commerce.Cart.jsonc +73 -0
  5. package/base44/entities/commerce.Coupon.jsonc +113 -0
  6. package/base44/entities/commerce.Customer.jsonc +96 -0
  7. package/base44/entities/commerce.DownloadPermission.jsonc +54 -0
  8. package/base44/entities/commerce.EmailLog.jsonc +43 -0
  9. package/base44/entities/commerce.Order.jsonc +287 -0
  10. package/base44/entities/commerce.OrderNote.jsonc +31 -0
  11. package/base44/entities/commerce.OrderRefund.jsonc +64 -0
  12. package/base44/entities/commerce.PaymentGateway.jsonc +48 -0
  13. package/base44/entities/commerce.Product.jsonc +291 -0
  14. package/base44/entities/commerce.ProductAttribute.jsonc +39 -0
  15. package/base44/entities/commerce.ProductAttributeTerm.jsonc +38 -0
  16. package/base44/entities/commerce.ProductCategory.jsonc +51 -0
  17. package/base44/entities/commerce.ProductReview.jsonc +48 -0
  18. package/base44/entities/commerce.ProductTag.jsonc +30 -0
  19. package/base44/entities/commerce.ProductVariation.jsonc +167 -0
  20. package/base44/entities/commerce.ShippingClass.jsonc +30 -0
  21. package/base44/entities/commerce.ShippingZone.jsonc +41 -0
  22. package/base44/entities/commerce.ShippingZoneMethod.jsonc +84 -0
  23. package/base44/entities/commerce.StoreSettings.jsonc +23 -0
  24. package/base44/entities/commerce.TaxClass.jsonc +23 -0
  25. package/base44/entities/commerce.TaxRate.jsonc +68 -0
  26. package/base44/entities/commerce.Webhook.jsonc +57 -0
  27. package/base44/entities/commerce.WebhookDelivery.jsonc +45 -0
  28. package/base44/functions/commerce/admin-coupons/entry.ts +100 -0
  29. package/base44/functions/commerce/admin-customers/entry.ts +141 -0
  30. package/base44/functions/commerce/admin-orders/entry.ts +396 -0
  31. package/base44/functions/commerce/admin-orders/helpers.ts +246 -0
  32. package/base44/functions/commerce/admin-products/entry.ts +506 -0
  33. package/base44/functions/commerce/admin-refunds/entry.ts +158 -0
  34. package/base44/functions/commerce/admin-reports/entry.ts +283 -0
  35. package/base44/functions/commerce/admin-reviews/entry.ts +66 -0
  36. package/base44/functions/commerce/admin-tools/entry.ts +261 -0
  37. package/base44/functions/commerce/admin-webhooks/entry.ts +52 -0
  38. package/base44/functions/commerce/payment-webhook/entry.ts +135 -0
  39. package/base44/functions/commerce/payments/entry.ts +238 -0
  40. package/base44/functions/commerce/seed-store/defaults.ts +162 -0
  41. package/base44/functions/commerce/seed-store/entry.ts +310 -0
  42. package/base44/functions/commerce/seed-store/sample-data.ts +349 -0
  43. package/base44/functions/commerce/storefront-account/entry.ts +207 -0
  44. package/base44/functions/commerce/storefront-cart/cart-pricing.ts +258 -0
  45. package/base44/functions/commerce/storefront-cart/entry.ts +283 -0
  46. package/base44/functions/commerce/storefront-catalog/entry.ts +459 -0
  47. package/base44/functions/commerce/storefront-checkout/cart-pricing.ts +258 -0
  48. package/base44/functions/commerce/storefront-checkout/entry.ts +485 -0
  49. package/base44/shared/commerce/auth.ts +60 -0
  50. package/base44/shared/commerce/coupons.ts +257 -0
  51. package/base44/shared/commerce/data/continents.ts +75 -0
  52. package/base44/shared/commerce/data/countries.ts +307 -0
  53. package/base44/shared/commerce/data/currencies.ts +46 -0
  54. package/base44/shared/commerce/email-templates.ts +240 -0
  55. package/base44/shared/commerce/emails.ts +225 -0
  56. package/base44/shared/commerce/money.ts +66 -0
  57. package/base44/shared/commerce/orders.ts +251 -0
  58. package/base44/shared/commerce/payments.ts +495 -0
  59. package/base44/shared/commerce/reviews.ts +36 -0
  60. package/base44/shared/commerce/scan.ts +57 -0
  61. package/base44/shared/commerce/sequence.ts +35 -0
  62. package/base44/shared/commerce/settings.ts +57 -0
  63. package/base44/shared/commerce/shipping.ts +215 -0
  64. package/base44/shared/commerce/stock.ts +227 -0
  65. package/base44/shared/commerce/stripe.ts +463 -0
  66. package/base44/shared/commerce/tax.ts +136 -0
  67. package/base44/shared/commerce/totals.ts +314 -0
  68. package/base44/shared/commerce/webhooks.ts +116 -0
  69. package/package.json +37 -0
  70. package/scripts/install.js +156 -0
  71. package/skills/commerce/SKILL.md +62 -0
  72. package/skills/commerce/docs/api-admin.md +186 -0
  73. package/skills/commerce/docs/api-storefront.md +408 -0
  74. package/skills/commerce/installation-guidelines.md +91 -0
  75. package/skills/commerce/post-installation.md +157 -0
  76. package/skills/commerce/references/emails.md +13 -0
  77. package/skills/commerce/references/guest-access-security.md +18 -0
  78. package/skills/commerce/references/limits-and-performance.md +16 -0
  79. package/skills/commerce/references/media-and-downloads.md +4 -0
  80. package/skills/commerce/references/online-payments.md +201 -0
  81. package/skills/commerce/references/product-render.md +87 -0
  82. package/skills/commerce/references/scheduled-work.md +19 -0
  83. package/skills/commerce/references/storefront-product-page.md +83 -0
  84. package/skills/commerce/references/webhooks.md +8 -0
  85. package/src/commerce/admin/README.md +107 -0
  86. package/src/commerce/admin/bot/Markdown.jsx +138 -0
  87. package/src/commerce/admin/bot/StoreAdminBot.jsx +249 -0
  88. package/src/commerce/admin/bot/pipe-tables.js +116 -0
  89. package/src/commerce/admin/components/AddressForm.jsx +78 -0
  90. package/src/commerce/admin/components/ConfirmDialog.jsx +52 -0
  91. package/src/commerce/admin/components/CountrySelect.jsx +81 -0
  92. package/src/commerce/admin/components/DataTable.jsx +192 -0
  93. package/src/commerce/admin/components/DateRangePicker.jsx +91 -0
  94. package/src/commerce/admin/components/EmptyState.jsx +17 -0
  95. package/src/commerce/admin/components/MediaUploader.jsx +116 -0
  96. package/src/commerce/admin/components/MetaDataEditor.jsx +45 -0
  97. package/src/commerce/admin/components/MoneyInput.jsx +50 -0
  98. package/src/commerce/admin/components/PageHeader.jsx +29 -0
  99. package/src/commerce/admin/components/RichTextarea.jsx +21 -0
  100. package/src/commerce/admin/components/SearchSelect.jsx +142 -0
  101. package/src/commerce/admin/components/StatusBadge.jsx +17 -0
  102. package/src/commerce/admin/context/BasePathContext.jsx +26 -0
  103. package/src/commerce/admin/context/SettingsContext.jsx +207 -0
  104. package/src/commerce/admin/hooks/useAsync.js +46 -0
  105. package/src/commerce/admin/hooks/useDebounce.js +11 -0
  106. package/src/commerce/admin/hooks/useMoney.js +52 -0
  107. package/src/commerce/admin/hooks/usePagedList.js +83 -0
  108. package/src/commerce/admin/hooks/usePaymentProvider.js +27 -0
  109. package/src/commerce/admin/hooks/useRealtime.js +129 -0
  110. package/src/commerce/admin/index.jsx +34 -0
  111. package/src/commerce/admin/layout/AccessDenied.jsx +54 -0
  112. package/src/commerce/admin/layout/AdminLayout.jsx +33 -0
  113. package/src/commerce/admin/layout/AuthGuard.jsx +84 -0
  114. package/src/commerce/admin/layout/Sidebar.jsx +130 -0
  115. package/src/commerce/admin/layout/Topbar.jsx +94 -0
  116. package/src/commerce/admin/lib/api.js +55 -0
  117. package/src/commerce/admin/lib/constants.js +157 -0
  118. package/src/commerce/admin/lib/format.js +27 -0
  119. package/src/commerce/admin/lib/geo-data.js +125 -0
  120. package/src/commerce/admin/lib/order-utils.js +147 -0
  121. package/src/commerce/admin/lib/paths.js +35 -0
  122. package/src/commerce/admin/lib/product-utils.js +55 -0
  123. package/src/commerce/admin/pages/Dashboard.jsx +245 -0
  124. package/src/commerce/admin/pages/coupons/CouponEditor.jsx +565 -0
  125. package/src/commerce/admin/pages/coupons/CouponsList.jsx +172 -0
  126. package/src/commerce/admin/pages/customers/CustomerEditor.jsx +318 -0
  127. package/src/commerce/admin/pages/customers/CustomersList.jsx +169 -0
  128. package/src/commerce/admin/pages/orders/OrderEditor.jsx +952 -0
  129. package/src/commerce/admin/pages/orders/OrdersList.jsx +227 -0
  130. package/src/commerce/admin/pages/orders/components/AddProductDialog.jsx +149 -0
  131. package/src/commerce/admin/pages/orders/components/DownloadPermissionsPanel.jsx +119 -0
  132. package/src/commerce/admin/pages/orders/components/LineItemsTable.jsx +208 -0
  133. package/src/commerce/admin/pages/orders/components/OrderNotesPanel.jsx +123 -0
  134. package/src/commerce/admin/pages/orders/components/PaymentPanel.jsx +199 -0
  135. package/src/commerce/admin/pages/orders/components/RefundPanel.jsx +239 -0
  136. package/src/commerce/admin/pages/orders/components/TotalsBox.jsx +52 -0
  137. package/src/commerce/admin/pages/products/AttributeTerms.jsx +180 -0
  138. package/src/commerce/admin/pages/products/Attributes.jsx +183 -0
  139. package/src/commerce/admin/pages/products/Categories.jsx +236 -0
  140. package/src/commerce/admin/pages/products/ProductEditor.jsx +267 -0
  141. package/src/commerce/admin/pages/products/ProductsList.jsx +391 -0
  142. package/src/commerce/admin/pages/products/Reviews.jsx +255 -0
  143. package/src/commerce/admin/pages/products/Tags.jsx +150 -0
  144. package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +132 -0
  145. package/src/commerce/admin/pages/products/components/PublishBox.jsx +101 -0
  146. package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +243 -0
  147. package/src/commerce/admin/pages/products/components/tabs/AdvancedTab.jsx +48 -0
  148. package/src/commerce/admin/pages/products/components/tabs/AttributesTab.jsx +208 -0
  149. package/src/commerce/admin/pages/products/components/tabs/DownloadsTab.jsx +91 -0
  150. package/src/commerce/admin/pages/products/components/tabs/ExternalTab.jsx +41 -0
  151. package/src/commerce/admin/pages/products/components/tabs/GeneralTab.jsx +103 -0
  152. package/src/commerce/admin/pages/products/components/tabs/InventoryTab.jsx +93 -0
  153. package/src/commerce/admin/pages/products/components/tabs/LinkedTab.jsx +102 -0
  154. package/src/commerce/admin/pages/products/components/tabs/ShippingTab.jsx +86 -0
  155. package/src/commerce/admin/pages/products/components/tabs/VariationsTab.jsx +377 -0
  156. package/src/commerce/admin/pages/reports/Reports.jsx +416 -0
  157. package/src/commerce/admin/pages/settings/EmailsSettings.jsx +240 -0
  158. package/src/commerce/admin/pages/settings/GeneralSettings.jsx +232 -0
  159. package/src/commerce/admin/pages/settings/InventorySettings.jsx +146 -0
  160. package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +260 -0
  161. package/src/commerce/admin/pages/settings/ProductsSettings.jsx +118 -0
  162. package/src/commerce/admin/pages/settings/SettingsLayout.jsx +53 -0
  163. package/src/commerce/admin/pages/settings/ShippingSettings.jsx +304 -0
  164. package/src/commerce/admin/pages/settings/ShippingZoneEditor.jsx +514 -0
  165. package/src/commerce/admin/pages/settings/TaxRatesTable.jsx +231 -0
  166. package/src/commerce/admin/pages/settings/TaxSettings.jsx +281 -0
  167. package/src/commerce/admin/pages/settings/useGroupForm.jsx +76 -0
  168. package/src/commerce/admin/pages/status/WebhookEditor.jsx +296 -0
  169. package/src/commerce/admin/pages/status/Webhooks.jsx +53 -0
  170. package/src/commerce/admin/routes.jsx +151 -0
  171. package/src/commerce/utils/index.js +19 -0
  172. package/src/commerce/utils/shipping-promos.js +99 -0
  173. package/src/commerce/utils/variants.js +411 -0
@@ -0,0 +1,408 @@
1
+ # Storefront API Reference
2
+
3
+ Everything needed to build a customer-facing shopfront (or headless client) against the Base44 Commerce Template. No visitor UI ships with the template — this is your integration surface. Some storefront *logic* does ship: the framework-free helpers in `src/commerce/utils/` (variant selection, free-shipping rules).
4
+
5
+ ## Required behaviors — a storefront that skips these cannot sell
6
+
7
+ Read this list before writing any of it. Each item is a hard requirement enforced by the API, not a nicety:
8
+
9
+ | # | Requirement | Enforced by |
10
+ |---|---|---|
11
+ | 1 | **Variable products: one selector per attribute**, resolved to a `variation_id` before adding to the cart — never a flat list of combinations | `add-item` → `400 variation_required` ([product page](#get-product)) |
12
+ | 2 | **Present shipping options and send a choice** — drive it off the cart's `shipping_status`; a single option is auto-applied, several mean you must ask | `place-order` → `400 shipping_method_required` ([shipping](#shipping-is-not-optional--read-this-before-building-checkout)) |
13
+ | 3 | **Redirect to `payment.checkout_url`** when the customer pays by card | the order stays `pending` otherwise |
14
+ | 4 | **Implement the return page** (`/order-received`, or your own route set in Settings → General → *Payment return path*) and call `commerce/payments` `complete-return` there | without it a paid order is never confirmed, and a wrong path is a 404 |
15
+ | 5 | **Only advertise offers the store is configured for** (free-shipping thresholds come from zone methods) | nothing — this one is on you |
16
+
17
+ Four public functions: **`commerce/storefront-catalog`**, **`commerce/storefront-cart`**, **`commerce/storefront-checkout`**, **`commerce/storefront-account`**. All are invoked the same way and return the same envelope.
18
+
19
+ ## What the API already gives you (read before designing the UI)
20
+
21
+ The backend is considerably richer than a minimal "grid → cart → pay" shop. **Build to this surface, not to the basics** — everything below already exists server-side, so leaving it out means shipping a storefront that is weaker than the store behind it. Each row links to the section with the exact payload.
22
+
23
+ | Capability | Already supported | Where |
24
+ |---|---|---|
25
+ | **Product discovery** | full-text `search`, filter by category (incl. descendants), tag, attribute+term, price range, `featured`, `on_sale`, `in_stock_only`; sort by menu order / price / newest / **popularity** / **rating**; paging | [`list-products`](#list-products) |
26
+ | **Rich product pages** | galleries, variable products with per-variation price/stock/image, categories, tags, **upsells**, **cross-sells**, grouped-product children | [`get-product`](#get-product) |
27
+ | **Taxonomy navigation** | category tree, **tag list with counts**, attributes + terms for filter UIs | [`list-categories`](#list-categories), [`list-tags`](#list-tags), [`list-attributes`](#list-attributes) |
28
+ | **Customer reviews** | paginated reviews per product with **average rating + rating count**, `verified` owner flag, and **review submission by signed-in customers**; plus "my reviews" | [`get-product`](#get-product), [`submit-review`](#submit-review), [`my-reviews`](#commercestorefront-account) |
29
+ | **Cart** | guest carts via `cart_token`, add/update/remove, quantity merging, `sold_individually` caps, live re-pricing, stock revalidation | [`commerce/storefront-cart`](#commercestorefront-cart) |
30
+ | **Coupons** | apply/remove by code with full server-side validation (eligibility, limits, per-user usage) | [`apply-coupon`](#commercestorefront-cart) |
31
+ | **Shipping** | address → matched zone → **selectable shipping methods with live costs**, free-shipping rules | [`set-shipping-address`](#commercestorefront-cart) |
32
+ | **Tax** | inclusive/exclusive pricing, per-class rates, itemized or single display — all resolved server-side | [`get-store-info`](#get-store-info) |
33
+ | **Stock states** | in stock / out of stock / **on backorder**, low-stock signalling, configurable out-of-stock hiding | [`list-products`](#list-products) |
34
+ | **Checkout** | guest or authenticated, billing/shipping, per-gateway routing, **payment instructions** for manual methods, order cancel, post-payment confirm | [`commerce/storefront-checkout`](#commercestorefront-checkout) |
35
+ | **Accounts** | order history, saved billing/shipping addresses, and **guest order tracking by `order_key`** with customer-visible order notes | [`commerce/storefront-account`](#commercestorefront-account) |
36
+ | **Digital products** | entitlement-checked downloads with remaining-count, expiry, and signed URLs for private files | [`get-download`](#commercestorefront-account) |
37
+ | **Store config** | currency + formatting, review toggles, catalog/cart price display — read it and honour it instead of hardcoding | [`get-store-info`](#get-store-info) |
38
+
39
+ Two things that are **not** in the backend and are yours to build: the visitor UI itself, and live card capture (see [`references/online-payments.md`](../references/online-payments.md)). Payment methods, currency and formatting are all admin-owned data — always render them from `get-store-info` rather than a hardcoded list.
40
+
41
+ ## Conventions
42
+
43
+ - **Invoke:** `base44.functions.invoke("commerce/<function>", { action, ...payload })`. Response body is on `res.data`; the envelope is `{ success, data }` on success or `{ success, error, code }` on failure (HTTP status set accordingly).
44
+ ```js
45
+ const res = await base44.functions.invoke("commerce/storefront-catalog", { action: "get-store-info" });
46
+ const info = res.data.data; // envelope → payload
47
+ ```
48
+ - **Auth modes:** most actions are anonymous. Some require an authenticated Base44 session (marked **auth**). Guest order access uses `order_key`; cart access uses `cart_token` — both are bearer credentials (HTTPS only).
49
+ - **Identity is never a payload field.** Anything attributed to a person — a review's author, a saved profile — is taken from the session. Passing an email in the body does not make you that customer, and no request can assert that an order was paid. Don't design a feature around a client-supplied identity; it will be ignored.
50
+ - **Money:** numbers, 2-dp. **Dates:** ISO strings.
51
+ - **Errors:** every failure has a stable `code` (listed per action) plus a human `error` message.
52
+
53
+ ---
54
+
55
+ ## commerce/storefront-catalog
56
+
57
+ Public catalog browsing. No auth.
58
+
59
+ ### `get-store-info`
60
+ Bootstrap data for a storefront. No payload.
61
+
62
+ **Response:**
63
+ ```json
64
+ {
65
+ "settings": { "currency": "USD", "currency_position": "left", "thousand_sep": ",", "decimal_sep": ".",
66
+ "num_decimals": 2, "weight_unit": "kg", "dimension_unit": "cm",
67
+ "prices_include_tax": false, "enable_reviews": true, "review_rating_required": true },
68
+ "payment_gateways": [ { "slug": "cod", "title": "Cash on delivery", "description": "..." } ],
69
+ "countries": [ { "code": "US", "name": "United States", "states": [ { "code": "CA", "name": "California" } ] } ],
70
+ "currencies": [ { "code": "USD", "name": "US Dollar", "symbol": "$", "decimals": 2 } ]
71
+ }
72
+ ```
73
+ `settings` is a safe projection — only display/behavior keys, never admin config.
74
+
75
+ ### `list-products`
76
+ **Payload** (all optional): `search`, `category_id` (includes descendants), `tag_id`, `attribute_id` + `attribute_term`, `min_price`, `max_price`, `featured` (bool), `on_sale` (bool), `in_stock_only` (bool), `sort` (`menu_order`|`price`|`-price`|`-created_date`|`popularity`|`rating`, default `menu_order`), `page` (default 1), `per_page` (default 12, max 100).
77
+
78
+ Only `status: "publish"` products are returned. Visibility: passing `search` uses **search context** (hides `catalog_visibility: hidden|catalog`); no `search` uses **browse context** (hides `hidden|search`). `inventory.hide_out_of_stock` (or `in_stock_only`) drops out-of-stock products.
79
+
80
+ **Response:** `{ "products": [Product...], "page": 1, "per_page": 12, "has_next": true }`
81
+
82
+ Each row is the product record (minus paywalled fields) **plus a resolved `tags` array** (`[{ id, name, slug }]`), so cards can show tags without a second call. `categories` are **not** resolved on rows — `category_ids` only.
83
+
84
+ > **What to render, and what a row can't show:** [`../references/product-render.md`](../references/product-render.md) — field-availability table for list vs. product page, tags in both views, variable-product pricing on cards, and how to add a `get-product`-only field to the listing call instead of fetching per card.
85
+
86
+ **Filters stack.** `category_id`, `tag_id`, `attribute_id` + `attribute_term`, `min_price`/`max_price`, `on_sale`, `featured` and `in_stock_only` are ANDed, so "Dresses + gift + on sale" is one request. Build the controls from [`list-categories`](#list-categories), [`list-tags`](#list-tags) (its `count` gives you "Gift (12)") and [`list-attributes`](#list-attributes), and mirror active filters into the URL so a filtered listing is shareable and survives reload.
87
+
88
+ ### `get-product`
89
+ **Payload:** `{ id }` **or** `{ slug }`; optional `reviews_page` (1), `reviews_per_page` (10, max 50).
90
+
91
+ **Response:**
92
+ ```json
93
+ {
94
+ "product": { Product },
95
+ "variations": [ ProductVariation... ], // publishable only; [] for non-variable
96
+ "categories": [ ProductCategory... ],
97
+ "tags": [ ProductTag... ],
98
+ "reviews": { "items": [ { "id", "reviewer", "review", "rating", "verified", "created_date" } ],
99
+ "page": 1, "per_page": 10, "has_next": false,
100
+ "average_rating": 4.5, "rating_count": 12 },
101
+ "upsells": [ { "id", "name", "slug", "price", "on_sale", "image", "stock_status" } ],
102
+ "cross_sells": [ ...summaries ],
103
+ "grouped_products": [ ...summaries ]
104
+ }
105
+ ```
106
+ **Errors:** `404 not_found` (missing / not published / hidden).
107
+
108
+ > **Rendering this response as a product page — the rule, not just a link.** `variations[]` is **not** a list of choices to show. Build **one control per `product.attributes[]` entry with `variation: true`** (Size, Color, …), and resolve the combination to a variation client-side; `add-item` needs that `variation_id`. Variable-product prices come from `variations[]`, never from `product.price` (the backend rolls stock up to the parent, not price). The shipped helper does all of it:
109
+ > ```js
110
+ > import { resolveSelection, defaultSelection, selectOption } from "@/commerce/utils";
111
+ > const [selection, setSelection] = useState(() => defaultSelection(product, variations));
112
+ > const view = resolveSelection(product, variations, selection);
113
+ > // view.axes → render a control per axis, options from axis.options
114
+ > // view.availability[axis.key][option] → "available" | "out_of_stock" | "unavailable" (disable, don't hide)
115
+ > // view.display → image / price / sku / stock for the resolved variation
116
+ > // view.purchasable + view.addToCart → { product_id, variation_id } for add-item
117
+ > const pick = (axisKey, option) => setSelection((s) => selectOption(product, variations, s, axisKey, option));
118
+ > ```
119
+ > Full variant rules (availability states, incomplete-selection pricing, defaults, URL state): [`../references/storefront-product-page.md`](../references/storefront-product-page.md).
120
+ > What to render on the page vs. a card, and which fields exist in only one of the two calls: [`../references/product-render.md`](../references/product-render.md).
121
+
122
+ ### `list-categories`
123
+ No payload. Returns a nested tree: `{ "categories": [ { ...category, "children": [...] } ] }` sorted by `menu_order` then name.
124
+
125
+ ### `list-tags`
126
+ **Payload** (optional): `{ with_products_only?: boolean }` (default `true`).
127
+
128
+ Returns `{ "tags": [ { id, name, slug, description, count } ] }` sorted by name. By default it omits tags no published product carries, so a tag nav never links to an empty listing; pass `with_products_only: false` for the full set (e.g. an admin-facing picker).
129
+
130
+ `count` is tallied from the products this API would actually list — published, browse-visible, and honoring `inventory.hide_out_of_stock` — **not** from `ProductTag.count`, which also counts drafts and hidden products and drifts until `commerce/admin-tools` `recount-terms` runs. So `Gift (12)` and the tag's listing agree.
131
+
132
+ This is the **only** way a storefront can enumerate tags — `commerce.ProductTag` is admin-only RLS — and it's what makes `list-products` `tag_id` usable, since that filter needs an id. Tags are a flat, cross-cutting axis ("gift", "summer", "vegan"); categories are the hierarchical spine. Show both: see [*Tags*](../references/product-render.md#2-tags--render-them-in-both-views) for where they belong in each view.
133
+
134
+ ### `list-attributes`
135
+ No payload. Returns `{ "attributes": [ { ...attribute, "terms": [ ...terms ] } ] }` — for building filter UIs.
136
+
137
+ ### `submit-review` — **auth**
138
+ **Payload:** `{ product_id, reviewer?, review, rating }`.
139
+
140
+ **Requires a signed-in customer.** The reviewer's email is taken from the session, never from the payload — a `reviewer_email` field is ignored. `reviewer` is the display name only, and defaults to the account's `full_name`. Build the review form behind a login prompt: anonymous callers get `401 login_required`.
141
+
142
+ Requires `products.enable_reviews` and the product's `reviews_allowed`. Rating required when `review_rating_required`. If `only_verified_reviews`, the signed-in customer must have a `processing`/`completed` order containing the product. `verified` is derived from their own order history. Status is `hold` unless `auto_approve_reviews`.
143
+
144
+ **Response:** `{ "review_id", "status": "hold"|"approved", "verified": true }`
145
+ **Errors:** `401 login_required`, `403 reviews_disabled|verified_only|forbidden`, `404 not_found`, `400 review_incomplete|rating_required|invalid_rating`.
146
+
147
+ ---
148
+
149
+ ## commerce/storefront-cart
150
+
151
+ Token-scoped cart (guest + member). Every action **except `create`** takes `cart_token`. **Every mutating action returns the full priced cart view.** Carts have a rolling 48h TTL (refreshed on each touch). An authenticated caller's other active carts are merged in (quantities summed, `sold_individually` capped, coupon codes unioned; source carts marked abandoned).
152
+
153
+ ### The priced cart view (returned by every action)
154
+ ```json
155
+ {
156
+ "cart_token": "uuid",
157
+ "items": [ {
158
+ "item_key": "uuid", "product_id": "...", "variation_id": "", "quantity": 2,
159
+ "attributes": [ { "name": "Color", "option": "Red" } ],
160
+ "name": "T-Shirt", "sku": "TS-RED-M", "image": "https://...",
161
+ "price": 20, "subtotal": 40, "total": 36, "total_tax": 3.6,
162
+ "virtual": false, "sold_individually": false,
163
+ "purchasable": { "ok": true }
164
+ } ],
165
+ "coupon_codes": ["welcome10"],
166
+ "coupons": [ { "code": "welcome10", "coupon_id": "...", "discount": 4, "discount_tax": 0, "free_shipping": false } ],
167
+ "coupon_notices": [ { "code": "old", "error": "Coupon has expired.", "error_code": "expired" } ],
168
+ "removed_items": [ { "item_key": "...", "product_id": "...", "reason": "...", "code": "unavailable" } ],
169
+ "shipping_address": { "country": "US", "state": "CA", "postcode": "90210", "city": "LA" },
170
+ "chosen_shipping_method": "<zoneMethodId>",
171
+ "available_shipping_methods": [ { "id": "<zoneMethodId>", "method_id": "flat_rate", "title": "Flat rate", "cost": 5 } ],
172
+ "shipping_status": "chosen", // not_needed | chosen | auto_selected | choice_required | none_available
173
+ "totals": { "subtotal": 40, "discount_total": 4, "discount_tax": 0, "shipping_total": 5, "shipping_tax": 0,
174
+ "cart_tax": 3.6, "total_tax": 3.6, "total": 44.6, "prices_include_tax": false,
175
+ "tax_lines": [ { "rate_id": "...", "label": "CA Tax", "tax_total": 3.6 } ] },
176
+ "expires_at": "2025-..."
177
+ }
178
+ ```
179
+ Stored coupons that stop validating are **auto-removed** and reported in `coupon_notices`; items whose product vanished/unpublished appear in `removed_items`.
180
+
181
+ ### Actions
182
+ | Action | Payload | Notes / errors |
183
+ |---|---|---|
184
+ | `create` | `{ items?: [{product_id, variation_id?, quantity, attributes?}] }` | Mints and returns a new `cart_token`. Initial items go through add validation. |
185
+ | `get` | `{ cart_token }` | Priced view (also re-prices + merges). |
186
+ | `totals` | `{ cart_token }` | Alias of `get`. |
187
+ | `add-item` | `{ cart_token, product_id, variation_id?, quantity?, attributes? }` | Variable product requires `variation_id` (`400 variation_required`); `sold_individually` caps qty at 1; merges same product+variation. `400 <stock code>`, `404 product_not_found|variation_not_found`. |
188
+ | `update-item` | `{ cart_token, item_key, quantity }` | qty ≤ 0 removes the line. `404 item_not_found`, `400 <stock code>`. |
189
+ | `remove-item` | `{ cart_token, item_key }` | |
190
+ | `apply-coupon` | `{ cart_token, code }` | Full validation. `400 coupons_disabled|code_required|already_applied|<coupon code>`. |
191
+ | `remove-coupon` | `{ cart_token, code }` | |
192
+ | `set-shipping-address` | `{ cart_token, address: {country, state?, postcode?, city?} }` | `400 country_required`. Returns the matched zone's `available_shipping_methods` and a `shipping_status`. |
193
+ | `choose-shipping-method` | `{ cart_token, method_id }` | `method_id` = a `commerce.ShippingZoneMethod` id from `available_shipping_methods`. `400 invalid_shipping_method` (the error body carries `available_shipping_methods`). |
194
+
195
+ Common: `400 cart_token_required`, `404 cart_not_found`, `404 cart_expired`.
196
+
197
+ ### Shipping is not optional — read this before building checkout
198
+ <a id="shipping-is-not-optional--read-this-before-building-checkout"></a>
199
+
200
+ A shippable order with no shipping line ships **for free**, so the API refuses to
201
+ create one. Every priced cart view carries `shipping_status`; drive the UI from it
202
+ rather than assuming:
203
+
204
+ | `shipping_status` | Meaning | What the storefront must do |
205
+ |---|---|---|
206
+ | `not_needed` | every line is virtual, or shipping is disabled store-wide | show no shipping step |
207
+ | `auto_selected` | exactly **one** method is offered, so the cart already applied it | show it as the (only) delivery option — **don't** make the customer pick from a list of one |
208
+ | `chosen` | the customer's pick is still offered | show it, allow changing |
209
+ | `choice_required` | **several** methods are offered and none is chosen | render `available_shipping_methods` and call `choose-shipping-method`; checkout will refuse until then |
210
+ | `none_available` | nothing ships to this address | say so, and don't let checkout proceed |
211
+
212
+ Rules the cart enforces on every price, so you get them for free:
213
+
214
+ - **A single available method is auto-selected.** One option is not a choice; requiring a round trip for it is exactly how a storefront ends up never sending a method at all.
215
+ - **A stale choice is dropped.** If the address or cart changes so the chosen method is no longer offered, it is cleared (and re-auto-selected when only one remains) instead of silently pricing zero shipping.
216
+ - **Costs change with the cart.** `available_shipping_methods` is recomputed per price — free-shipping thresholds and coupon-gated methods appear and disappear — so re-read it after every cart mutation, not just after setting the address.
217
+
218
+ ### Never advertise what the store isn't configured to do
219
+
220
+ Copy like *"Free shipping on orders over €150"* is a **claim about store configuration**, and the checkout will only honour what's actually configured. Don't write such a line into a storefront unless a matching rule exists — either configure it (with the operator's agreement) or leave the copy out.
221
+
222
+ `commerce.ShippingZone` and `commerce.ShippingZoneMethod` are **public-read**, so the real rule is available to the storefront, and [`src/commerce/utils/shipping-promos.js`](../../../src/commerce/utils/shipping-promos.js) reads it:
223
+
224
+ ```js
225
+ import { freeShippingRules, freeShippingThreshold, freeShippingProgress } from "@/commerce/utils";
226
+
227
+ const [zones, methods] = await Promise.all([
228
+ base44.entities["commerce.ShippingZone"].list("order", 100),
229
+ base44.entities["commerce.ShippingZoneMethod"].list(undefined, 200),
230
+ ]);
231
+ const threshold = freeShippingThreshold(freeShippingRules(zones, methods));
232
+ // null → the store has no free-shipping rule: show no banner, no progress bar.
233
+ const progress = freeShippingProgress(threshold, cart.totals.subtotal); // { threshold, qualifies, remaining }
234
+ ```
235
+
236
+ A free-shipping rule is a `free_shipping` zone method whose `settings.requires` is `min_amount`/`either`/`both` with a `min_amount` (a `coupon` requirement is **not** a spend threshold, and thresholds are **per zone** — a rule in one zone must not become a site-wide banner). The same applies to any other promise: a discount claim needs a real `commerce.Coupon` (admin-read only — only advertise a code the operator gave you), and delivery-time or returns claims the template cannot enforce need the operator's confirmation.
237
+
238
+ ---
239
+
240
+ ## commerce/storefront-checkout
241
+
242
+ ### `place-order`
243
+ Converts a cart into an order. **Payload:**
244
+ ```json
245
+ { "cart_token": "uuid", "payment_method": "cod",
246
+ "billing": { "first_name", "last_name", "address_1", "address_2?", "city", "state?", "postcode?", "country", "email", "phone?" },
247
+ "shipping": { ...address without email },
248
+ "chosen_shipping_method?": "<zoneMethodId>",
249
+ "customer_note?": "...", "create_account?": false }
250
+ ```
251
+ **Mandatory fields:**
252
+ - **Billing:** `first_name, last_name, address_1, city, country, email`.
253
+ - **Shipping address** and a **shipping method** — mandatory whenever the cart has any non-virtual (physical) line and shipping is enabled. Your checkout UI has to *offer* the options; there is no default. The whole contract:
254
+ ```js
255
+ cart = await inv("commerce/storefront-cart", { action: "set-shipping-address", cart_token, address });
256
+ if (cart.shipping_status === "choice_required") {
257
+ // MUST render cart.available_shipping_methods and let the customer pick
258
+ cart = await inv("commerce/storefront-cart", { action: "choose-shipping-method", cart_token, method_id: picked.id });
259
+ } else if (cart.shipping_status === "none_available") {
260
+ // the store doesn't ship there — stop, don't try to place the order
261
+ }
262
+ // auto_selected / chosen: nothing to do, one option was applied for you
263
+ ```
264
+ Skipping this is the most common storefront bug: `place-order` answers `400 shipping_method_required` with the methods attached, and no order is created. Set the address on the cart first (`set-shipping-address`); the method can come either from the cart (`choose-shipping-method`) or straight from this call via `chosen_shipping_method`, which wins if both are present. What happens then:
265
+ - **one method offered** → applied automatically, nothing to send;
266
+ - **several offered, none chosen** → `400 shipping_method_required`, with `available_shipping_methods` in the error body so you can prompt;
267
+ - **`chosen_shipping_method` not offered** for this address → `400 invalid_shipping_method` (never silently swapped for another);
268
+ - **nothing offered** → `400 no_shipping_available`.
269
+
270
+ The order is priced with the resolved method, so its `shipping_total` always matches what the customer was shown.
271
+
272
+ > **Payment.** `payment_method` is a `commerce.PaymentGateway` slug from `get-store-info`. Card payments are **implemented**: choosing the online gateway leaves the order `pending` and returns `payment: { status: "requires_payment", checkout_url, session_id }` — send the customer to `checkout_url` (the provider's hosted page), then confirm on their return with `commerce/payments` `verify`. Never treat a return URL as proof of payment. The manual gateways (`cod`, `bacs`, `cheque`) settle outside the store and return `payment_instructions` instead. An online gateway is only listed while a payment provider is connected, so anything `get-store-info` offers is payable. Optional `success_url` / `cancel_url` / `return_url` in the payload override the default return URLs. See [`../references/online-payments.md`](../references/online-payments.md).
273
+
274
+ **Steps:** releases expired holds → revalidates stock & coupons → computes authoritative totals → finds-or-creates the Customer by billing email → creates a `pending` order (with `order_key`, `hold_expires_at`) → reduces stock, fires `new_order` email + `order.created` webhook → marks cart `converted` → routes by gateway: **cod → processing**, **bacs/cheque → on-hold** (with `payment_instructions`), **stripe → stays pending** with a `not_implemented` placeholder, **custom → pending_external**.
275
+
276
+ > **Saved profiles are only written by their owner.** The order always stores the `billing`/`shipping` it was placed with. The **`commerce.Customer`** record behind it — the one a storefront prefills from — is refreshed only when the caller is signed in *as* the billing email. A guest checkout quoting an existing customer's address still attaches the order to them (so it shows up in their `my-orders`), but cannot change their saved name or addresses. Don't build an "edit my details at checkout" flow for guests; use `update-my-addresses` behind a login instead.
277
+
278
+ **Response:**
279
+ ```json
280
+ {
281
+ "order_id": "...", "order_number": 1001, "order_key": "order_...",
282
+ "status": "processing", "currency": "USD",
283
+ "payment_method": "cod", "payment_method_title": "Cash on delivery",
284
+ "payment_instructions": { "type": "cod", "description": "...", "account_details?": [...] },
285
+ "payment": null, // or { "status": "not_implemented" | "pending_external", "note": "..." } for stripe/custom
286
+ "notices": [], // e.g. ["account_creation_requires_login"]
287
+ "totals": { "subtotal", "discount_total", "shipping_total", "shipping_tax", "cart_tax", "total_tax", "total" },
288
+ "order": { ...customer-safe order (internal flags/ip stripped) }
289
+ }
290
+ ```
291
+ **Errors:** `401 login_required` (guest checkout disabled), `400 billing_incomplete`, `400 empty_cart`, `409 items_unavailable`, `409 coupon_invalid`, `400 shipping_method_required` / `400 invalid_shipping_method` / `400 no_shipping_available` (each carrying `available_shipping_methods`; these replace the earlier catch-all `shipping_required`), `400 invalid_payment_method`.
292
+
293
+ ### `commerce/payments` — online payment for an order
294
+
295
+ Separate function, same guest-bearer rule (`order_id` + `order_key`; an admin may act without a key).
296
+
297
+ | Action | Payload | Returns |
298
+ |---|---|---|
299
+ | `create-link` | `{ order_id, order_key, success_url?, cancel_url?, return_url? }` | `{ provider, session_id, url, expires_at? }` — a fresh hosted payment page for an **unpaid** order, whatever its current payment method (the order is switched onto the online gateway and the change is logged). This is how a storefront offers "pay now" on an order-received page or an emailed link, and what the admin's payment-link button calls. `409 already_paid`, `400 online_payments_disabled` (the store turned card payment off), `503 payment_provider_unavailable` |
300
+ | `complete-return` | `{ order_id, order_key, payment?, return_url? }` | `{ state: "paid"\|"cancelled"\|"unpaid", paid, already_confirmed, status, order, payment_link }` — the whole return flow in one call for your `/order-received` page: confirms with the provider, progresses the order, and includes a fresh `payment_link` while unpaid. `payment` is only a hint; a hand-edited `?payment=success` can never yield `paid` |
301
+ | `verify` | `{ order_id, order_key, session_id? }` | `{ paid, already_confirmed, status, order }` — the same confirmation without the render-ready extras. **Idempotent**: call it on every return, and as often as you like. A `session_id` you pass must be one the provider opened for *this* order, or `409 session_order_mismatch`; omit it and the order's own session is used |
302
+
303
+ > **You must build the `/order-received` page** — it is mandatory for payment links to work. No storefront UI ships with the template, and without that route a paying customer lands on a 404 *and* the order never gets marked paid. It only needs to call `complete-return` and render its three states: see [`../references/online-payments.md`](../references/online-payments.md).
304
+
305
+ Provider callbacks land on `commerce/payment-webhook` (signed, server-to-server) — the second confirmation path, for buyers who pay and close the tab. Whichever path runs second is a no-op.
306
+
307
+ ### `confirm-payment`
308
+ Post-payment hook for an order on the online gateway. **Payload:** `{ order_id, order_key, session_id? }`.
309
+
310
+ **The request cannot make an order paid.** The `order_key` says who is asking; the payment itself is confirmed against the provider, and the session it names must be one the provider opened for *this* order. Only then does the order move `pending`/`on-hold` → `processing`, set `date_paid` and bump customer stats. There is no caller-supplied `transaction_id` — it comes from the provider. Idempotent.
311
+
312
+ `commerce/payments` `complete-return` is the richer version of this and what an `/order-received` page should call; use `confirm-payment` when you only need the transition.
313
+
314
+ **Response:** `{ "order": { ...customer-safe order }, "paid": true, "already_confirmed": false }`
315
+ **Errors:** `400 order_key_required|not_an_online_payment`, `404 order_not_found`, `409 invalid_status|payment_not_confirmed|session_order_mismatch`.
316
+
317
+ ### `cancel-order`
318
+ Customer-initiated cancel. **Payload:** `{ order_id, order_key }`. Only `pending`/`on-hold` (restores stock). **Response:** `{ "order": { ...customer-safe order } }` · **Errors:** `400 order_key_required`, `404 order_not_found`, `409 invalid_status`.
319
+
320
+ ---
321
+
322
+ ## commerce/storefront-account
323
+
324
+ Two access modes: **auth** (Base44 session) or **`order_key` bearer** (guest tracking).
325
+
326
+ | Action | Auth | Payload | Response |
327
+ |---|---|---|---|
328
+ | `my-orders` | auth | `{ page?, per_page? }` | `{ orders: [customer-safe], page, per_page, has_next }` |
329
+ | `get-order` | order_key | `{ order_id, order_key }` | `{ order: customer-safe }` |
330
+ | `order-notes` | order_key | `{ order_id, order_key }` | `{ notes: [{ id, note, created_date }] }` (customer notes only) |
331
+ | `my-downloads` | auth | — | `{ downloads: [{ permission_id, order_id, product_id, download_name, downloads_remaining, access_expires, download_count }] }` |
332
+ | `get-download` | auth **or** order_key | `{ permission_id, order_key? }` | `{ url, download_name, downloads_remaining }` — decrements remaining; returns a signed URL for private files |
333
+ | `update-my-addresses` | auth | `{ billing?, shipping? }` | `{ customer: { email, first_name, last_name, billing, shipping } }` |
334
+ | `my-reviews` | auth | — | `{ reviews: [{ id, product_id, review, rating, status, verified, created_date }] }` |
335
+
336
+ **Errors:** `401 login_required`, `400 order_key_required|permission_required|nothing_to_update`, `404 order_not_found|download_not_found`, `403 forbidden|download_limit_reached|download_expired`.
337
+
338
+ ---
339
+
340
+ ## Walkthrough A — guest checkout
341
+
342
+ ```js
343
+ const inv = (fn, payload) => base44.functions.invoke(fn, payload).then(r => r.data.data);
344
+
345
+ // 1. Browse
346
+ const { products } = await inv("commerce/storefront-catalog", { action: "list-products", per_page: 12 });
347
+
348
+ // 2. New cart with one item
349
+ let cart = await inv("commerce/storefront-cart", { action: "create",
350
+ items: [{ product_id: products[0].id, quantity: 1 }] });
351
+ const token = cart.cart_token;
352
+
353
+ // 3. (variable product) fetch options, then add the chosen variation
354
+ const detail = await inv("commerce/storefront-catalog", { action: "get-product", id: products[0].id });
355
+ if (detail.product.type === "variable") {
356
+ cart = await inv("commerce/storefront-cart", { action: "add-item",
357
+ cart_token: token, product_id: detail.product.id, variation_id: detail.variations[0].id });
358
+ }
359
+
360
+ // 4. Coupon (optional)
361
+ cart = await inv("commerce/storefront-cart", { action: "apply-coupon", cart_token: token, code: "welcome10" });
362
+
363
+ // 5. Shipping address → method. A single option is already applied for you;
364
+ // several mean the customer has to choose or checkout will refuse.
365
+ cart = await inv("commerce/storefront-cart", { action: "set-shipping-address",
366
+ cart_token: token, address: { country: "US", state: "CA", postcode: "90210", city: "Los Angeles" } });
367
+ if (cart.shipping_status === "choice_required") {
368
+ const picked = await askCustomer(cart.available_shipping_methods); // your UI
369
+ cart = await inv("commerce/storefront-cart", { action: "choose-shipping-method",
370
+ cart_token: token, method_id: picked.id });
371
+ } else if (cart.shipping_status === "none_available") {
372
+ throw new Error("We don't ship to this address");
373
+ }
374
+
375
+ // 6. Place the order (COD → processing immediately)
376
+ const order = await inv("commerce/storefront-checkout", { action: "place-order",
377
+ cart_token: token, payment_method: "cod",
378
+ billing: { first_name: "Ada", last_name: "Lovelace", address_1: "1 St",
379
+ city: "Los Angeles", state: "CA", postcode: "90210", country: "US", email: "ada@example.com" } });
380
+
381
+ // 7. Track it later without an account (order_key from step 6)
382
+ const tracked = await inv("commerce/storefront-account", { action: "get-order",
383
+ order_id: order.order_id, order_key: order.order_key });
384
+ ```
385
+
386
+ ## Walkthrough B — member experience
387
+
388
+ ```js
389
+ const inv = (fn, payload) => base44.functions.invoke(fn, payload).then(r => r.data.data);
390
+ // (caller is authenticated via base44 auth — the SDK sends the session automatically)
391
+
392
+ // Browsing carts made while logged out merge into this one automatically on `get`/`create`.
393
+ let cart = await inv("commerce/storefront-cart", { action: "create", items: [{ product_id, quantity: 2 }] });
394
+
395
+ // Saved addresses speed up checkout
396
+ await inv("commerce/storefront-account", { action: "update-my-addresses",
397
+ billing: { first_name: "Ada", last_name: "Lovelace", address_1: "1 St", city: "LA",
398
+ state: "CA", postcode: "90210", country: "US", email: "ada@example.com" } });
399
+
400
+ const order = await inv("commerce/storefront-checkout", { action: "place-order",
401
+ cart_token: cart.cart_token, payment_method: "bacs", billing: { /* ... */ } });
402
+ // bacs → on-hold; show order.payment_instructions.account_details
403
+
404
+ // Order history, downloads, reviews (all auth, no order_key needed)
405
+ const { orders } = await inv("commerce/storefront-account", { action: "my-orders", per_page: 10 });
406
+ const { downloads } = await inv("commerce/storefront-account", { action: "my-downloads" });
407
+ const file = await inv("commerce/storefront-account", { action: "get-download", permission_id: downloads[0]?.permission_id });
408
+ ```
@@ -0,0 +1,91 @@
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 `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/).
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
+ | `../skills/commerce/` | `../../../skills/commerce/` |
31
+
32
+ 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 `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 `skills/commerce/` where agents pick it up natively; the template repo itself also stays under `examples/commerce/` for reference.
33
+
34
+ **Manual.** Equivalently, copy by hand:
35
+
36
+ 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.
37
+ 2. Copy `src/commerce/admin/` → `src/commerce/admin/` and `src/commerce/utils/` → `src/commerce/utils/` (the storefront variant-selection helpers; framework-free, no deps).
38
+ 3. Copy `skills/commerce/` → `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).
39
+
40
+ Confirm your `base44/config.jsonc` `entitiesDir`/`functionsDir` point at these folders (the defaults do).
41
+
42
+ ## 2. Deploy and wire up
43
+
44
+ 1. `npx base44 entities push` — creates/updates the 24 entity schemas. *(CLI path only — the Base44 runtime deploys on write.)*
45
+ 2. `npx base44 functions deploy` — deploys the 16 functions. *(CLI path only.)*
46
+ 3. `npx base44 agents push` — registers the `commerce/StoreAdmin` agent (§3). *(CLI path only.)*
47
+ 4. Verify `sonner` and `recharts` are installed and install them if not (`npm i sonner recharts`) — they ship with the default Base44 template, as does `react-markdown`; 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
+ 5. Mount the admin router (see [`post-installation.md`](./post-installation.md)).
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.
51
+
52
+ Check the install at any time:
53
+
54
+ ```js
55
+ const { data } = (await base44.functions.invoke("commerce/admin-tools", { action: "status" })).data;
56
+ // → { template_version, seeded, settings_groups, counts: {...}, checks: [...] }
57
+ ```
58
+
59
+ `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
+ ```json
62
+ { "success": false, "code": "schema_incompatible",
63
+ "errors": [{ "entity": "Product", "error": "..." }] }
64
+ ```
65
+
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
+
68
+ ---
69
+
70
+ ## 3. The StoreAdmin agent (admin copilot)
71
+
72
+ The template ships an AI copilot for store operators:
73
+
74
+ - **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
+ - **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
+ - **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
+ - **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
+ - **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
+ **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.
82
+
83
+ ---
84
+
85
+ ## 4. Building on it — the four storefront requirements
86
+
87
+ If your work includes a customer-facing shopfront, read **[`SKILL.md` → *If you build a storefront, these four are not optional*](./SKILL.md)** before writing it, and [`docs/api-storefront.md`](./docs/api-storefront.md)'s *Required behaviors* table. In short: attribute-level variant selectors resolved to a `variation_id`; shipping options presented and chosen; card payment redirect plus an `/order-received` page; and no offers the store isn't configured for. The API enforces the first three — a storefront that skips them cannot complete a purchase.
88
+
89
+ ## 5. Next steps
90
+
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.