@base44/app-plugin-commerce 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/base44/agents/commerce/StoreAdmin.jsonc +1 -1
- package/base44/functions/commerce/admin-products/entry.ts +11 -131
- package/base44/functions/commerce/seed-store/entry.ts +70 -167
- package/base44/functions/commerce/seed-store/seed-catalog.ts +717 -0
- package/base44/shared/commerce/catalog.ts +134 -0
- package/package.json +1 -1
- package/scripts/install.js +5 -6
- package/skills/commerce/SKILL.md +4 -3
- package/skills/commerce/docs/api-admin.md +7 -3
- package/skills/commerce/installation-guidelines.md +4 -4
- package/skills/commerce/post-installation.md +216 -78
- package/skills/commerce/references/online-payments.md +5 -30
- package/skills/commerce/references/reviews.md +20 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog write helpers shared by commerce/admin-products and
|
|
3
|
+
* commerce/seed-store, so both produce byte-identical records: slug/code/SKU
|
|
4
|
+
* derivation and uniqueness, price/stock derivation, taxonomy counts, and the
|
|
5
|
+
* variant parent's price/stock rollup.
|
|
6
|
+
*/
|
|
7
|
+
import { HttpError } from "./auth.ts";
|
|
8
|
+
import { round2 } from "./money.ts";
|
|
9
|
+
import { deriveStockStatus } from "./stock.ts";
|
|
10
|
+
import { isVariable } from "./products.ts";
|
|
11
|
+
|
|
12
|
+
export function slugify(name: string): string {
|
|
13
|
+
return String(name || "")
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.trim()
|
|
16
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
17
|
+
.replace(/^-+|-+$/g, "") || "product";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** price/on_sale from the sale window. Mutates rec. */
|
|
21
|
+
export function derivePricing(rec: any): void {
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
const from = rec.date_on_sale_from ? new Date(rec.date_on_sale_from).getTime() : -Infinity;
|
|
24
|
+
const to = rec.date_on_sale_to ? new Date(rec.date_on_sale_to).getTime() : Infinity;
|
|
25
|
+
const saleActive = rec.sale_price != null && rec.sale_price !== "" && now >= from && now <= to;
|
|
26
|
+
rec.on_sale = !!saleActive;
|
|
27
|
+
const effective = saleActive ? rec.sale_price : rec.regular_price;
|
|
28
|
+
if (effective != null && effective !== "") rec.price = round2(Number(effective));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** stock_status derivation when stock is managed. Mutates rec. */
|
|
32
|
+
export function deriveStock(rec: any, outThreshold: number, managed: boolean): void {
|
|
33
|
+
if (managed) {
|
|
34
|
+
rec.stock_status = deriveStockStatus(
|
|
35
|
+
Number(rec.stock_quantity ?? 0),
|
|
36
|
+
rec.backorders ?? "no",
|
|
37
|
+
outThreshold,
|
|
38
|
+
);
|
|
39
|
+
} else if (!rec.stock_status) {
|
|
40
|
+
rec.stock_status = "instock";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function ensureUniqueSlug(
|
|
45
|
+
sr: any,
|
|
46
|
+
slug: string,
|
|
47
|
+
selfId?: string,
|
|
48
|
+
entity = "commerce.Product",
|
|
49
|
+
scope?: Record<string, any>,
|
|
50
|
+
): Promise<string> {
|
|
51
|
+
let candidate = slug;
|
|
52
|
+
for (let i = 2; i < 100; i++) {
|
|
53
|
+
const hits = (await sr.entities[entity].filter({ ...(scope ?? {}), slug: candidate }, undefined, 2)) ?? [];
|
|
54
|
+
if (!hits.some((p: any) => p.id !== selfId)) return candidate;
|
|
55
|
+
candidate = `${slug}-${i}`;
|
|
56
|
+
}
|
|
57
|
+
return `${slug}-${crypto.randomUUID().slice(0, 6)}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** `code` is what a storefront filter URL carries, so it has to be unique. */
|
|
61
|
+
export async function ensureUniqueCode(sr: any, code: string, selfId?: string): Promise<string> {
|
|
62
|
+
let candidate = code;
|
|
63
|
+
for (let i = 2; i < 100; i++) {
|
|
64
|
+
const hits = (await sr.entities["commerce.ProductAttribute"].filter({ code: candidate }, undefined, 2)) ?? [];
|
|
65
|
+
if (!hits.some((a: any) => a.id !== selfId)) return candidate;
|
|
66
|
+
candidate = `${code}-${i}`;
|
|
67
|
+
}
|
|
68
|
+
return `${code}-${crypto.randomUUID().slice(0, 6)}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** SKU must be unique across products AND variations. Throws duplicate_sku. */
|
|
72
|
+
export async function assertUniqueSku(sr: any, sku: string, opts: { productId?: string; variationId?: string }): Promise<void> {
|
|
73
|
+
if (!sku) return;
|
|
74
|
+
const prods = (await sr.entities["commerce.Product"].filter({ sku }, undefined, 2)) ?? [];
|
|
75
|
+
if (prods.some((p: any) => p.id !== opts.productId)) {
|
|
76
|
+
throw new HttpError(409, `SKU "${sku}" is already in use by another product.`, "duplicate_sku");
|
|
77
|
+
}
|
|
78
|
+
const vars = (await sr.entities["commerce.ProductVariation"].filter({ sku }, undefined, 2)) ?? [];
|
|
79
|
+
if (vars.some((v: any) => v.id !== opts.variationId)) {
|
|
80
|
+
throw new HttpError(409, `SKU "${sku}" is already in use by a product variation.`, "duplicate_sku");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function adjustTermCounts(sr: any, entity: string, prevIds: string[], nextIds: string[]): Promise<void> {
|
|
85
|
+
const added = nextIds.filter((id) => !prevIds.includes(id));
|
|
86
|
+
const removed = prevIds.filter((id) => !nextIds.includes(id));
|
|
87
|
+
for (const id of added) await bumpCount(sr, entity, id, +1);
|
|
88
|
+
for (const id of removed) await bumpCount(sr, entity, id, -1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function bumpCount(sr: any, entity: string, id: string, delta: number): Promise<void> {
|
|
92
|
+
try {
|
|
93
|
+
const rec = await sr.entities[entity].get(id);
|
|
94
|
+
if (rec) await sr.entities[entity].update(id, { count: Math.max(0, (rec.count ?? 0) + delta) });
|
|
95
|
+
} catch { /* stale reference — recount-terms repairs */ }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Roll a variant parent's stock and price up from its variations.
|
|
100
|
+
*
|
|
101
|
+
* Price matters beyond display: `list-products` sorts and filters on
|
|
102
|
+
* `product.price`, so a parent left empty sorts as free and drops out of every
|
|
103
|
+
* price filter. Deriving it removes a number nobody could keep in sync by hand —
|
|
104
|
+
* the parent is simply the cheapest thing a customer can actually buy.
|
|
105
|
+
*/
|
|
106
|
+
export async function rollUpParent(sr: any, parent: any, variations: any[]): Promise<void> {
|
|
107
|
+
if (!isVariable(parent)) return;
|
|
108
|
+
const live = variations.filter((v) => (v.status ?? "publish") === "publish");
|
|
109
|
+
const patch: Record<string, any> = {};
|
|
110
|
+
|
|
111
|
+
// manage_stock on the parent means the merchant tracks one pooled quantity,
|
|
112
|
+
// and with it off an explicit stock_status is their own choice to keep.
|
|
113
|
+
if (!parent.manage_stock) {
|
|
114
|
+
const status = live.length && live.every((v) => v.stock_status === "outofstock")
|
|
115
|
+
? "outofstock"
|
|
116
|
+
: "instock";
|
|
117
|
+
if (status !== parent.stock_status) patch.stock_status = status;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const cheapest = (key: string): number | null => {
|
|
121
|
+
const values = live.map((v) => Number(v[key])).filter((n) => Number.isFinite(n));
|
|
122
|
+
return values.length ? round2(Math.min(...values)) : null;
|
|
123
|
+
};
|
|
124
|
+
const regular = cheapest("regular_price");
|
|
125
|
+
const price = cheapest("price") ?? regular;
|
|
126
|
+
const onSale = live.some((v) => !!v.on_sale);
|
|
127
|
+
if (regular !== (parent.regular_price ?? null)) patch.regular_price = regular;
|
|
128
|
+
if (price !== (parent.price ?? null)) patch.price = price;
|
|
129
|
+
if (onSale !== !!parent.on_sale) patch.on_sale = onSale;
|
|
130
|
+
|
|
131
|
+
if (!Object.keys(patch).length) return;
|
|
132
|
+
await sr.entities["commerce.Product"].update(parent.id, patch);
|
|
133
|
+
Object.assign(parent, patch);
|
|
134
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@base44/app-plugin-commerce",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Base44 Commerce plugin — entities, backend functions, shared commerce engine, admin UI and the commerce skill, shipped as copyable source",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"base44",
|
package/scripts/install.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* This is only the static part of the install. The remaining steps live in
|
|
28
28
|
* ../skills/commerce/installation-guidelines.md (deps, deploy, seeding) and
|
|
29
29
|
* ../skills/commerce/post-installation.md (mounting the /store-admin route, admin
|
|
30
|
-
* role,
|
|
30
|
+
* role, seeding, the storefront quick start); day-2 guidance starts at
|
|
31
31
|
* ../skills/commerce/SKILL.md. The skill folder carries all of this
|
|
32
32
|
* documentation into the app.
|
|
33
33
|
*
|
|
@@ -165,10 +165,9 @@
|
|
|
165
165
|
" do not re-install packages already listed as dependencies\n" +
|
|
166
166
|
' 2. Mount the admin router: <Route path="/store-admin/*" element={<AdminApp />} />\n' +
|
|
167
167
|
" and implement the payment return page /order-received (post-installation.md)\n" +
|
|
168
|
-
" 3. Grant your user the admin role, then
|
|
169
|
-
"
|
|
170
|
-
"
|
|
171
|
-
" 4.
|
|
172
|
-
" 5. CLI installs only: npx base44 agents push (the hosted runtime syncs agents on write)"
|
|
168
|
+
" 3. Grant your user the admin role, then seed the store's data — one commerce/seed-store\n" +
|
|
169
|
+
" call (store_name required) takes the whole catalog via products, or the demo data\n" +
|
|
170
|
+
" via with_sample_data, or defaults only (post-installation.md §2)\n" +
|
|
171
|
+
" 4. CLI installs only: npx base44 agents push (the hosted runtime syncs agents on write)"
|
|
173
172
|
);
|
|
174
173
|
})();
|
package/skills/commerce/SKILL.md
CHANGED
|
@@ -13,12 +13,12 @@ Operational guidance for extending, operating and building on the Base44 Commerc
|
|
|
13
13
|
|
|
14
14
|
## IMPORTANT — first-time installation
|
|
15
15
|
|
|
16
|
-
If the template was just installed (or you are installing it right now), read [`skills/commerce/post-installation.md`](./post-installation.md) **before anything else**: embedding the admin pages, the three-layer admin-role enforcement (do not weaken),
|
|
16
|
+
If the template was just installed (or you are installing it right now), read [`skills/commerce/post-installation.md`](./post-installation.md) **before anything else**: embedding the admin pages, the three-layer admin-role enforcement (do not weaken), seeding the store's data — **one `commerce/seed-store` call takes the whole catalog** (products with attributes; variants, categories and taxonomy created internally — §2) — and the **storefront quick start** (§3): logic-only chunks for product list → product page → cart → checkout that make reading the full API docs unnecessary for the happy path. The full install-from-scratch steps are in [`skills/commerce/installation-guidelines.md`](./installation-guidelines.md).
|
|
17
17
|
|
|
18
18
|
## Working on the UI
|
|
19
19
|
|
|
20
20
|
- **Admin UI** (`src/commerce/admin/`) — a complete store back office ships with the template, and it is **yours to change**: restyle it, add or remove pages, rework flows, extend it however the app needs. To understand the backend it talks to, read [`skills/commerce/docs/api-admin.md`](./docs/api-admin.md) — every admin function/action plus the direct-entity-CRUD contract. The only invariant is the admin-role gating (see above).
|
|
21
|
-
- **Storefront** — **no visitor UI ships**; the storefront *API* is complete (token-based cart), plus framework-free helpers in [`src/commerce/utils/`](../../src/commerce/utils/) (`variants.js`, `shipping-promos.js` — import from `@/commerce/utils`).
|
|
21
|
+
- **Storefront** — **no visitor UI ships**; the storefront *API* is complete (token-based cart), plus framework-free helpers in [`src/commerce/utils/`](../../src/commerce/utils/) (`variants.js`, `shipping-promos.js` — import from `@/commerce/utils`). Start from the logic-only quick start in [`post-installation.md` §3](./post-installation.md#3-storefront-quick-start--logic-only) — it covers the whole buy path; go to [`skills/commerce/docs/api-storefront.md`](./docs/api-storefront.md) for anything beyond it. Navigation comes from three actions — `list-categories` (tree), `list-tags` (flat, with counts) and `list-attributes` (filter UIs). What to render in the grid vs. the product page — and which fields only one of the two calls returns — is [`references/product-render.md`](./references/product-render.md); **tags are the most-skipped part of it and belong in both views**.
|
|
22
22
|
|
|
23
23
|
### If you build a storefront, these four are not optional
|
|
24
24
|
|
|
@@ -34,7 +34,7 @@ Agents keep shipping storefronts that miss these, and each one breaks buying out
|
|
|
34
34
|
|
|
35
35
|
2. **Checkout must present shipping options and send a choice.** After `set-shipping-address`, read `shipping_status` on the cart: `auto_selected` (one option, already applied) · `chosen` · `choice_required` → **you must show `available_shipping_methods` and call `choose-shipping-method`** · `none_available` → say so. `place-order` refuses with `400 shipping_method_required` until then — that is not a bug to work around.
|
|
36
36
|
|
|
37
|
-
3. **Take card payments, and build `/order-received`.** (
|
|
37
|
+
3. **Take card payments, and build `/order-received`.** (Have the Stripe integration connected at the beginning of the implementation, so it's already live when you create the checkout.) Choosing the online gateway returns `payment.checkout_url` — redirect there. Every payment link comes back to `/order-received`, which **you must implement**: call `commerce/payments` `complete-return` with the query params and render its `state`. Without that page a customer pays into a 404 and the order is never marked paid.
|
|
38
38
|
|
|
39
39
|
4. **Never advertise what isn't configured.** "Free shipping over €150" must come from a real `free_shipping` zone method. Zones are admin-only data, so a storefront cannot read them: the live answer is the cart's `available_shipping_methods` after `set-shipping-address`, and `shipping-promos.js` normalizes the rules wherever the records *are* in hand. No rule means no banner.
|
|
40
40
|
|
|
@@ -53,6 +53,7 @@ Open the matching file under `skills/commerce/references/` only when a task touc
|
|
|
53
53
|
|---|---|---|
|
|
54
54
|
| Product rendering (list + page) | what to show in a product grid vs. the product page, field availability across `list-products`/`get-product`, **tags in both views**, variant pricing on cards, adding a page-only field to the listing call | [`references/product-render.md`](./references/product-render.md) |
|
|
55
55
|
| Variant selection | attribute-level selectors, resolving a selection to a variation, availability states, incomplete-selection pricing, add-to-cart contract | [`references/storefront-product-page.md`](./references/storefront-product-page.md) |
|
|
56
|
+
| Reviews | stars on cards and the product page, the review list + submit form (backend ships complete), moderation statuses, verified-only and rating-required behaviors | [`references/reviews.md`](./references/reviews.md) |
|
|
56
57
|
| Admin product form | changing the product editor — its tabs are **Price & Inventory** (tax, then the attributes, then a row per variant, or a single *Base price* row when there are none), **Modifiers** (`meta_data`), **Downloads**, **Linked products**. Variants reconcile from the attribute values automatically: no generate step, no per-variant delete. Weight, dimensions and shipping class are per variant. | [`references/admin-product-form.md`](./references/admin-product-form.md) |
|
|
57
58
|
| Online payments | **any storefront or checkout work** — card payments ship implemented (hosted page, payment links, signed webhook, refunds) behind a provider-neutral utility wired to Stripe; connect a provider to go live, or implement one adapter to use another | [`references/online-payments.md`](./references/online-payments.md) |
|
|
58
59
|
| Scheduled work | recurring maintenance — stock-hold release, abandoned-cart cleanup, webhook-log pruning, counter-drift repair | [`references/scheduled-work.md`](./references/scheduled-work.md) |
|
|
@@ -62,7 +62,7 @@ Actions: `save` · `delete` · `batch` · `duplicate` · `set-stock` · `search`
|
|
|
62
62
|
|
|
63
63
|
Only a category has a slug, derived from its name and made unique; its `parent_id` pointing at itself is coerced to `""` (`categoryWithDescendants` would loop). An attribute's **`code`** is derived from the name and made unique — it is the key a storefront filter URL should carry. Creating a **tag** is get-or-create: a name that already exists case-insensitively returns the existing record instead of splitting the tag in two. Renaming an **attribute value** rewrites `attributes[].options` and `default_attributes` on every product using it, and the matching `option` on their variations — products store a value by name, so the rename would otherwise orphan them.
|
|
64
64
|
|
|
65
|
-
This is how a non-UI caller (notably the StoreAdmin agent) creates the records that `category_ids`/`tag_ids`/`attributes[].attribute_id` reference — assigning an id is useless if the record can't be created. **A product's `commerce.ProductAttribute` and its values must exist first**, so create those, then `save` the product with `attributes[]`/`variations[]`.
|
|
65
|
+
This is how a non-UI caller (notably the StoreAdmin agent) creates the records that `category_ids`/`tag_ids`/`attributes[].attribute_id` reference — assigning an id is useless if the record can't be created. **A product's `commerce.ProductAttribute` and its values must exist first**, so create those, then `save` the product with `attributes[]`/`variations[]`. (At install time, `commerce/seed-store` can do all of this in one call — it takes whole products by display name and get-or-creates the taxonomy internally; see below.)
|
|
66
66
|
- **`delete-term`** — `{ taxonomy, id, detach? }` → `{ deleted, detached, terms_deleted }`. Deleting an **attribute** always deletes its terms (a term outliving its attribute is unreachable); `detach: true` additionally strips the attribute from every product's `attributes[]`. For a category or tag, products keep the id by default (the storefront skips ids that no longer resolve); `detach: true` strips it from every product first. **For an attribute *value* `detach` is a no-op** — deleting a value leaves its name in every product's `attributes[].options` and leaves the variations that use it in place, so remove the value from the products first (or expect variants the storefront can no longer resolve).
|
|
67
67
|
- **`list-terms`** — `{ taxonomy, q?, attribute_id?, limit?, skip? }` → `{ rows, has_next }`. Categories sort by `menu_order`, attributes and attribute values by `order`, tags by `name`; `attribute_id` filters values to one attribute. Use it to reuse an existing record instead of creating a duplicate.
|
|
68
68
|
|
|
@@ -163,7 +163,7 @@ All actions scan orders on demand (counted = `date_paid` set, or status `process
|
|
|
163
163
|
Actions: `status` · `payment-connector-status` · `admin-email-recipients` · `recount-terms` · `recount-coupon-usage` · `recalculate-customer-stats-all` · `prune-webhook-deliveries` · `clear-abandoned-carts` · `regenerate-download-permissions`
|
|
164
164
|
|
|
165
165
|
- **`status`** — `{ template_version, seeded, settings_groups, counts: { "commerce.Product": n | "1000+", ... }, checks: { has_payment_gateways, has_default_zone } }` — `counts` is keyed by the **namespaced** entity name, and `checks` is an object, not an array. — mini system-status; also the seeded/health check for install verification.
|
|
166
|
-
- **`payment-connector-status`** — no payload → `{ provider, provider_label, gateway_slug, connected, error?, connector }`. Whether an online payment provider is usable **right now**, answered by the payment utility for whichever provider is wired — so UI derives payment readiness instead of hardcoding a "not set up" notice, and shows "No payment provider connected" rather than a brand. Connectors are service-role only, hence the round trip; any failure reports `connected: false`. A provider connected **after** the function's last deploy reads as `connected: false` until the functions are redeployed — env vars are injected at deploy time ([`references/online-payments.md`](../references/online-payments.md) §
|
|
166
|
+
- **`payment-connector-status`** — no payload → `{ provider, provider_label, gateway_slug, connected, error?, connector }`. Whether an online payment provider is usable **right now**, answered by the payment utility for whichever provider is wired — so UI derives payment readiness instead of hardcoding a "not set up" notice, and shows "No payment provider connected" rather than a brand. Connectors are service-role only, hence the round trip; any failure reports `connected: false`. A provider connected **after** the function's last deploy reads as `connected: false` until the functions are redeployed — env vars are injected at deploy time ([`references/online-payments.md`](../references/online-payments.md) §1). (`connector` repeats `provider` for callers written against the older shape.)
|
|
167
167
|
- **`admin-email-recipients`** — no payload → `{ recipients: string[], source: "settings" | "admin_users", admin_users: string[] }`. Where admin notifications go **right now**: `recipients` is the configured `emails.admin_recipients`, or the app's admin users when that is empty (the runtime fallback), with `source` saying which. `admin_users` is returned either way, so Settings → Emails can show the fallback as the field's placeholder even while explicit recipients are set. A client can't resolve it itself — listing users needs service role.
|
|
168
168
|
- **`recount-terms`** — repairs category/tag/term `count`.
|
|
169
169
|
- **`recount-coupon-usage`** — repairs `usage_count`/`used_by`.
|
|
@@ -185,4 +185,8 @@ Actions: `status` · `create-link` · `complete-return` · `verify` — the admi
|
|
|
185
185
|
|
|
186
186
|
## commerce/seed-store
|
|
187
187
|
|
|
188
|
-
Not action-routed. Body `{ with_sample_data
|
|
188
|
+
Not action-routed. Body `{ store_name?, currency?, with_sample_data?, products?, coupons?, tax_rates? }`. **`store_name` is required** when the `emails` group doesn't exist yet (**400** `store_name_required` otherwise) — pass the app's name **as the platform shows it** — ask the user or read it from the dashboard. `base44/config.jsonc` → `name` is *not* authoritative: it can still say `New App` for an app the platform calls `Canvas`. A backend function can't read either, its environment being only `BASE44_APP_ID`. It lands in `emails.store_name` — one setting serving as both the store name in email subjects and the sender name on every transactional email; a nameless store renders subjects like `[]: New order #1002`, which is why seeding refuses one. Requires admin. Runs a **canary schema check** first — on any incompatibility returns **422** `{ success:false, code:"schema_incompatible", errors:[{ entity, error }] }` and writes nothing. Otherwise seeds defaults idempotently, then the catalog. On an already-seeded store a passed `store_name` fills a **blank** name and never overwrites one the merchant chose. **`currency`** is an ISO code (validated against the shared currencies table, **400** `invalid_payload` on an unknown one) that sets `general.currency` plus the currency's standard `num_decimals`; unlike `store_name` it **always wins** — there is no blank state to distinguish a merchant's USD from the seeded default, so passing it on a re-run updates the store currency.
|
|
189
|
+
|
|
190
|
+
**`products`** is the one-call catalog bootstrap — the worked example and full semantics are in [`../post-installation.md` §2.1](../post-installation.md#21-the-products-payload). Each entry references categories/tags/attributes by **display name** (taxonomy is get-or-created: slugs/codes derived, existing records matched case-insensitively and reused, with the stored casing canonicalized into the product). `attributes: [{ name, options }]` (or `{ <name>: [options] }`) declares the variant axes; `variations: [{ options: { <name>: <option> }, ...overrides }]` lists the stocked combinations — omit it to auto-generate **all** combinations, each inheriting the product-level price/sale fields. A variation with its own `stock_quantity` gets `manage_stock: "yes"`; without one it draws on the parent's pooled stock (`"parent"`). Variation SKUs are synthesized from the parent SKU when absent. Parent `price`/`regular_price`/`on_sale` are rolled up from the cheapest publishable variant, `stock_status` derived, category/tag counts maintained — same helpers as `admin-products` `save`, but **no `product.created` webhooks are dispatched** (bootstrap precedes webhook subscribers; use `admin-products` for webhook-visible creates). Payload problems fail before any write as **400** `invalid_payload` with `errors: [{ path, error }]`; an explicit variation SKU already in use is **409** `duplicate_sku`; a mid-write failure rolls back everything the call created (**500** `catalog_seed_failed`) without touching reused taxonomy. **Re-runs converge**: a product whose `sku` (or, without one, derived slug) already exists is skipped and reported, so retries never duplicate. Limits: ≤100 products, ≤50 variations per product, ≤500 variations per call, ≤50 coupons/tax rates. `coupons`/`tax_rates` are thin passthroughs (coupon `code` lowercased; both skip-if-exists). `with_sample_data: true` seeds the template's demo catalog instead (only when the store has zero products) and cannot be combined with the catalog keys.
|
|
191
|
+
|
|
192
|
+
→ `{ seeded: { settings_groups, gateways, tax_classes, zones, zone_methods }, sample_data: {...} | false, catalog: { categories|tags|attributes|terms: { created, reused }, products_created, products_skipped, variations_created, coupons, tax_rates, products: [{ name, id, slug, sku, variation_count } | { name, skipped: true, reason: "sku_exists"|"slug_exists", existing_id }] } | null, store_name: { value, action: "created" | "filled" | "unchanged" | "kept_existing" }, currency: { value, action: "created" | "updated" | "unchanged" } | null }`.
|
|
@@ -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,
|
|
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,7 +47,7 @@ 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.
|
|
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
|
|
|
@@ -64,7 +64,7 @@ const { data } = (await base44.functions.invoke("commerce/admin-tools", { action
|
|
|
64
64
|
"errors": [{ "entity": "commerce.Product", "error": "..." }] }
|
|
65
65
|
```
|
|
66
66
|
|
|
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.
|
|
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).
|
|
68
68
|
|
|
69
69
|
---
|
|
70
70
|
|
|
@@ -89,4 +89,4 @@ If your work includes a customer-facing shopfront, read **[`SKILL.md` → *If yo
|
|
|
89
89
|
|
|
90
90
|
## 5. Next steps
|
|
91
91
|
|
|
92
|
-
Continue with [`post-installation.md`](./post-installation.md): embedding the admin pages (router mount, admin-role enforcement)
|
|
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.
|