@base44/app-plugin-commerce 0.10.5 → 0.10.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/base44/functions/commerce/seed-store/entry.ts +5 -2
- package/base44/functions/commerce/seed-store/seed-catalog.ts +36 -0
- package/base44/shared/commerce/data/currencies.ts +34 -0
- package/package.json +1 -1
- package/skills/commerce/SKILL.md +1 -1
- package/skills/commerce/installation/install.md +11 -8
|
@@ -84,7 +84,10 @@ Deno.serve(async (req) => {
|
|
|
84
84
|
const withSample = !!body.with_sample_data;
|
|
85
85
|
const storeName = String(body.store_name ?? "").trim();
|
|
86
86
|
const currencyCode = String(body.currency ?? "").trim().toUpperCase();
|
|
87
|
-
const currencyInfo = currencyCode
|
|
87
|
+
const currencyInfo = currencyCode
|
|
88
|
+
? CURRENCIES.find((c) => c.code === currencyCode)
|
|
89
|
+
?? (/^[A-Z]{3}$/.test(currencyCode) ? { code: currencyCode, name: currencyCode, symbol: currencyCode, decimals: 2 } : undefined)
|
|
90
|
+
: undefined;
|
|
88
91
|
const WEIGHT_UNITS = ["kg", "g", "lbs", "oz"];
|
|
89
92
|
const DIMENSION_UNITS = ["cm", "m", "mm", "in", "yd"];
|
|
90
93
|
const weightUnit = String(body.weight_unit ?? "").trim().toLowerCase();
|
|
@@ -94,7 +97,7 @@ Deno.serve(async (req) => {
|
|
|
94
97
|
// or any write ──────────────────────────────────────────────────────
|
|
95
98
|
const unitErrors: Array<{ path: string; error: string }> = [];
|
|
96
99
|
if (currencyCode && !currencyInfo) {
|
|
97
|
-
unitErrors.push({ path: "currency", error: `"${currencyCode}" is not
|
|
100
|
+
unitErrors.push({ path: "currency", error: `"${currencyCode}" is not an ISO 4217 code — three letters, e.g. "EUR"` });
|
|
98
101
|
}
|
|
99
102
|
if (weightUnit && !WEIGHT_UNITS.includes(weightUnit)) {
|
|
100
103
|
unitErrors.push({ path: "weight_unit", error: `must be one of: ${WEIGHT_UNITS.join(", ")}` });
|
|
@@ -106,6 +106,12 @@ const ENUMS: Record<string, string[]> = {
|
|
|
106
106
|
tax_status: ["taxable", "none"],
|
|
107
107
|
discount_type: ["percent", "fixed_cart", "fixed_product"],
|
|
108
108
|
};
|
|
109
|
+
// What builds write for the coupon type before they read the enum.
|
|
110
|
+
const DISCOUNT_TYPE_SYNONYMS: Record<string, string> = {
|
|
111
|
+
percentage: "percent", pct: "percent", percent_off: "percent",
|
|
112
|
+
fixed: "fixed_cart", flat: "fixed_cart", fixed_amount: "fixed_cart", amount: "fixed_cart", cart: "fixed_cart",
|
|
113
|
+
per_product: "fixed_product", product: "fixed_product",
|
|
114
|
+
};
|
|
109
115
|
|
|
110
116
|
const PRICE_KEYS = ["regular_price", "sale_price", "stock_quantity", "low_stock_amount", "weight"];
|
|
111
117
|
|
|
@@ -168,6 +174,10 @@ export function normalizeCatalogPayload(body: any): CatalogSpec | null {
|
|
|
168
174
|
const path = `coupons[${i}]`;
|
|
169
175
|
if (!c || typeof c !== "object") return err(path, "must be an object");
|
|
170
176
|
if (!String(c.code ?? "").trim()) err(`${path}.code`, "is required");
|
|
177
|
+
if (typeof c.discount_type === "string") {
|
|
178
|
+
const k = c.discount_type.trim().toLowerCase();
|
|
179
|
+
c.discount_type = DISCOUNT_TYPE_SYNONYMS[k] ?? k;
|
|
180
|
+
}
|
|
171
181
|
if (c.discount_type != null && !ENUMS.discount_type.includes(c.discount_type)) {
|
|
172
182
|
err(`${path}.discount_type`, `must be one of: ${ENUMS.discount_type.join(", ")}`);
|
|
173
183
|
}
|
|
@@ -355,7 +365,13 @@ function normalizeProduct(
|
|
|
355
365
|
const name = String(spec.name ?? "").trim();
|
|
356
366
|
if (!name) err(`${path}.name`, "is required");
|
|
357
367
|
|
|
368
|
+
// `brand` is what a merchant means by a "Brand" spec row; `on_sale` is derived from the sale window.
|
|
369
|
+
if (typeof spec.brand === "string" && spec.brand.trim()) {
|
|
370
|
+
const { brand, ...rest } = spec;
|
|
371
|
+
spec = { ...rest, meta_data: [...(Array.isArray(rest.meta_data) ? rest.meta_data : []), { key: "Brand", value: brand.trim() }] };
|
|
372
|
+
}
|
|
358
373
|
for (const key of Object.keys(spec)) {
|
|
374
|
+
if (key === "on_sale") continue;
|
|
359
375
|
if (!PRODUCT_KEYS.has(key)) err(`${path}.${key}`, "unknown key — not a product field the seeder accepts");
|
|
360
376
|
}
|
|
361
377
|
checkEnumsAndNumbers(spec, path, err);
|
|
@@ -500,6 +516,7 @@ function normalizeProduct(
|
|
|
500
516
|
}
|
|
501
517
|
if (fields.sku !== undefined) fields.sku = sku;
|
|
502
518
|
fields.images = normalizeImages(spec.images, name, `${path}.images`, err);
|
|
519
|
+
if (fields.meta_data !== undefined) fields.meta_data = normalizeMetaData(fields.meta_data, `${path}.meta_data`, err);
|
|
503
520
|
|
|
504
521
|
return {
|
|
505
522
|
fields,
|
|
@@ -542,6 +559,25 @@ function strList(value: any, path: string, err: (p: string, e: string) => void):
|
|
|
542
559
|
}
|
|
543
560
|
|
|
544
561
|
/** `images` accepts plain URLs or { src, name?, alt? }; normalized to the entity shape. */
|
|
562
|
+
// The entity field is string-typed. A number, boolean or list here is a value the seeder can
|
|
563
|
+
// spell out — not a reason to roll the whole catalog back after every other write succeeded.
|
|
564
|
+
function normalizeMetaData(value: any, path: string, err: (p: string, e: string) => void): any[] {
|
|
565
|
+
if (!Array.isArray(value)) {
|
|
566
|
+
err(path, "must be an array of { key, value } rows");
|
|
567
|
+
return [];
|
|
568
|
+
}
|
|
569
|
+
const rows: any[] = [];
|
|
570
|
+
value.forEach((row, i) => {
|
|
571
|
+
if (!row || typeof row !== "object") return err(`${path}[${i}]`, "must be { key, value }");
|
|
572
|
+
const key = String(row.key ?? "").trim();
|
|
573
|
+
if (!key) return err(`${path}[${i}].key`, "is required");
|
|
574
|
+
const v = row.value;
|
|
575
|
+
if (v == null || v === "") return;
|
|
576
|
+
rows.push({ key, value: Array.isArray(v) ? v.map(String).join(", ") : typeof v === "object" ? JSON.stringify(v) : String(v) });
|
|
577
|
+
});
|
|
578
|
+
return rows;
|
|
579
|
+
}
|
|
580
|
+
|
|
545
581
|
function normalizeImages(value: any, productName: string, path: string, err: (p: string, e: string) => void): any[] {
|
|
546
582
|
if (value == null) return [];
|
|
547
583
|
if (!Array.isArray(value)) {
|
|
@@ -43,4 +43,38 @@ export const CURRENCIES: CurrencyInfo[] = [
|
|
|
43
43
|
{ code: "MYR", name: "Malaysian Ringgit", symbol: "RM", decimals: 2 },
|
|
44
44
|
{ code: "IDR", name: "Indonesian Rupiah", symbol: "Rp", decimals: 0 },
|
|
45
45
|
{ code: "VND", name: "Vietnamese Dong", symbol: "₫", decimals: 0 },
|
|
46
|
+
{ code: "NGN", name: "Nigerian Naira", symbol: "₦", decimals: 2 },
|
|
47
|
+
{ code: "KES", name: "Kenyan Shilling", symbol: "KSh", decimals: 2 },
|
|
48
|
+
{ code: "GHS", name: "Ghanaian Cedi", symbol: "GH₵", decimals: 2 },
|
|
49
|
+
{ code: "PKR", name: "Pakistani Rupee", symbol: "₨", decimals: 2 },
|
|
50
|
+
{ code: "BDT", name: "Bangladeshi Taka", symbol: "৳", decimals: 2 },
|
|
51
|
+
{ code: "NPR", name: "Nepalese Rupee", symbol: "रू", decimals: 2 },
|
|
52
|
+
{ code: "LKR", name: "Sri Lankan Rupee", symbol: "Rs", decimals: 2 },
|
|
53
|
+
{ code: "EGP", name: "Egyptian Pound", symbol: "E£", decimals: 2 },
|
|
54
|
+
{ code: "MAD", name: "Moroccan Dirham", symbol: "MAD", decimals: 2 },
|
|
55
|
+
{ code: "DZD", name: "Algerian Dinar", symbol: "DA", decimals: 2 },
|
|
56
|
+
{ code: "TND", name: "Tunisian Dinar", symbol: "DT", decimals: 3 },
|
|
57
|
+
{ code: "SAR", name: "Saudi Riyal", symbol: "﷼", decimals: 2 },
|
|
58
|
+
{ code: "QAR", name: "Qatari Riyal", symbol: "QR", decimals: 2 },
|
|
59
|
+
{ code: "KWD", name: "Kuwaiti Dinar", symbol: "KD", decimals: 3 },
|
|
60
|
+
{ code: "BHD", name: "Bahraini Dinar", symbol: "BD", decimals: 3 },
|
|
61
|
+
{ code: "OMR", name: "Omani Rial", symbol: "OMR", decimals: 3 },
|
|
62
|
+
{ code: "JOD", name: "Jordanian Dinar", symbol: "JD", decimals: 3 },
|
|
63
|
+
{ code: "IQD", name: "Iraqi Dinar", symbol: "IQD", decimals: 3 },
|
|
64
|
+
{ code: "ARS", name: "Argentine Peso", symbol: "$", decimals: 2 },
|
|
65
|
+
{ code: "COP", name: "Colombian Peso", symbol: "$", decimals: 2 },
|
|
66
|
+
{ code: "PEN", name: "Peruvian Sol", symbol: "S/", decimals: 2 },
|
|
67
|
+
{ code: "CLP", name: "Chilean Peso", symbol: "$", decimals: 0 },
|
|
68
|
+
{ code: "UAH", name: "Ukrainian Hryvnia", symbol: "₴", decimals: 2 },
|
|
69
|
+
{ code: "KZT", name: "Kazakhstani Tenge", symbol: "₸", decimals: 2 },
|
|
70
|
+
{ code: "UZS", name: "Uzbekistani Som", symbol: "so'm", decimals: 2 },
|
|
71
|
+
{ code: "KGS", name: "Kyrgyzstani Som", symbol: "с", decimals: 2 },
|
|
72
|
+
{ code: "AZN", name: "Azerbaijani Manat", symbol: "₼", decimals: 2 },
|
|
73
|
+
{ code: "GEL", name: "Georgian Lari", symbol: "₾", decimals: 2 },
|
|
74
|
+
{ code: "ETB", name: "Ethiopian Birr", symbol: "Br", decimals: 2 },
|
|
75
|
+
{ code: "TZS", name: "Tanzanian Shilling", symbol: "TSh", decimals: 2 },
|
|
76
|
+
{ code: "UGX", name: "Ugandan Shilling", symbol: "USh", decimals: 0 },
|
|
77
|
+
{ code: "RWF", name: "Rwandan Franc", symbol: "FRw", decimals: 0 },
|
|
78
|
+
{ code: "XOF", name: "West African CFA Franc", symbol: "CFA", decimals: 0 },
|
|
79
|
+
{ code: "XAF", name: "Central African CFA Franc", symbol: "FCFA", decimals: 0 },
|
|
46
80
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@base44/app-plugin-commerce",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.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/skills/commerce/SKILL.md
CHANGED
|
@@ -114,7 +114,7 @@ Open a file when its work starts — not while planning.
|
|
|
114
114
|
|
|
115
115
|
| Topic | Open when | Size |
|
|
116
116
|
|---|---|---|
|
|
117
|
-
| [`installation/install.md`](./installation/install.md) | installing — all three stages: admin mount, storefront pages, then the seed payload |
|
|
117
|
+
| [`installation/install.md`](./installation/install.md) | installing — all three stages: admin mount, storefront pages, then the seed payload | 53K |
|
|
118
118
|
| [`docs/entities.md`](./docs/entities.md) | any direct entity read/write ("which entity holds X") | 11K |
|
|
119
119
|
| [`references/catalog-rendering.md`](./references/catalog-rendering.md) | field shapes each catalog call returns, variant edge cases | 16K |
|
|
120
120
|
| [`references/shipping-and-tax.md`](./references/shipping-and-tax.md) | zones beyond stage 03's recipe, taxes, day-2 edits | 8K |
|
|
@@ -51,7 +51,7 @@ lines and treat that stage as done — but keep reading this file, not re-fetchi
|
|
|
51
51
|
|
|
52
52
|
Image generation is the slowest step and nothing depends on it until seed time; the storefront doesn't wait on live data either.
|
|
53
53
|
|
|
54
|
-
1. **Start image generation first
|
|
54
|
+
1. **Start image generation first — in the same message as the install call**: one `generate_image` per product, before reading anything.
|
|
55
55
|
⚑ **The result already carries the real `image_url`** alongside a `placeholder_url`
|
|
56
56
|
— use `image_url` and move on. Nothing is pending, there is nothing to poll, and a
|
|
57
57
|
backend function calling `Core.GenerateImage` to "fetch the real URLs" is pure waste:
|
|
@@ -62,8 +62,10 @@ Image generation is the slowest step and nothing depends on it until seed time;
|
|
|
62
62
|
2. **Mount the admin (below) and build the storefront** while images render. Every
|
|
63
63
|
request and response shape the pages build against is written out in
|
|
64
64
|
[stage 02 below](#02--storefront), so they are written from the docs,
|
|
65
|
-
not from live data.
|
|
66
|
-
|
|
65
|
+
not from live data. Write `src/App.jsx` **last**, once every page it imports exists — an
|
|
66
|
+
earlier write warns `unresolved import` and buys a fix cycle. The platform validates the
|
|
67
|
+
build after every write; never run `npx vite build` or `npm run build`.
|
|
68
|
+
3. **Seed the moment the image URLs are back — in the same message as a write batch**, never as a message of its own: one `commerce/seed-store` call ([stage 03 below](#03--store-data)); it runs alongside the writes and takes a few seconds — don't idle on it.
|
|
67
69
|
4. **Converge**: open the finished pages against the live catalog.
|
|
68
70
|
5. **Payments last, if at all** — cards are off by default; [stage 03 below](#03--store-data) decides it.
|
|
69
71
|
|
|
@@ -185,7 +187,7 @@ function StoreLayout() {
|
|
|
185
187
|
|
|
186
188
|
**This file is the whole job** — every shape is in ["What each hook resolves to"](#what-each-hook-resolves-to). Rules marked ⚑ must survive whatever design you build.
|
|
187
189
|
|
|
188
|
-
⚑ **
|
|
190
|
+
⚑ **These are the only importable names — the list is complete, never open the barrels to check it.** `@/commerce/storefront`: StorefrontProvider, useStorefrontState, useStorefront, useStoreInfo, useFormatMoney, useCountries, useCart, useCheckout, CheckoutProvider, useCheckoutContext, useOrderReturn, orderReceivedUrl, ShippingMethodPicker, PaymentMethodPicker, AddressFields, CartUIProvider, useCartUI, REQUIRED_BILLING_FIELDS, missingBillingFields, shippingSlice, isShippingAddressComplete, useProductList, useCategories, useRibbons, useProduct, useAddToCart, useCartLine, CartLine, variantAxes, productPrice, productImages, imageIndex, productRibbons, productSpecs, findSpec, attributesLabel, cartTotalsLines, orderTotalsLines, addressFieldSpec, storefrontErrorCode, storefrontErrorMessage. `@/commerce/storefront-ui`: CartPage, MiniCart, CartButton, CheckoutPage, OrderReceivedPage — `CartButton` lives here, not in `storefront`. Everything else — `listProducts`, `getProduct`, `listCategories`, `listRibbons`, `getProductReviews`, `submitReview`, `getCart`, `applyCoupon`, `chooseShippingMethod`, `completeReturn` — is a **method on the client** `useStorefront()` returns; importing one by name throws `does not provide an export named …` and blanks the app: `const c = useStorefront(); await c.submitReview(…)`. Kit React files are `.jsx` (`src/commerce/admin/index.jsx`); there is no `.js` twin to read.
|
|
189
191
|
|
|
190
192
|
## Design language — once, before any page
|
|
191
193
|
|
|
@@ -366,7 +368,7 @@ Headings, CTAs and empty-state copy come in per component as `brand` (each overr
|
|
|
366
368
|
|
|
367
369
|
Behavior options, all optional (defaults are the professional store): `CartPage`/`CheckoutPage` take `sections` — `notes: false`, and on checkout `phone: "optional"|"required"|"hidden"`, `shipToDifferent: true`, `termsCheckbox: false`, `layout: "two-column"|"single"`. `hrefs` (`checkoutHref`, `cartHref`, `continueHref`, `homeHref`) default to `/checkout`, `/cart`, `/`; pass `productHref={(item) => …}` so line names link to your product route. Full props, slots and recipes: [`../references/storefront-ui.md`](../references/storefront-ui.md).
|
|
368
370
|
|
|
369
|
-
**A store language outside the six
|
|
371
|
+
**A store language outside the six** — no file reads needed: copy `src/commerce/storefront-ui/i18n/locales/en.js` to `<lang>.js` and translate the values (~2–3K chars), then repoint the one `import active` line in `src/commerce/storefront-ui/i18n/index.js` with `find_replace`: `import active from "./locales/en.js";` → `import active from "./locales/<lang>.js";`. The admin has the same line in `src/commerce/admin/i18n/index.js`. Same-language brand props alone don't need this.
|
|
370
372
|
|
|
371
373
|
## Driving the storefront from a browser script?
|
|
372
374
|
|
|
@@ -399,11 +401,12 @@ A fresh install has **no settings and no catalog**. One admin-only, idempotent c
|
|
|
399
401
|
| **Demo data** | `{ store_name, with_sample_data: true }` — 10 generic products; cannot combine with `products` (**400**) |
|
|
400
402
|
| **No products** | `{ store_name }` — defaults only |
|
|
401
403
|
|
|
402
|
-
**`store_name` is required on a first seed** — the app's name as the platform shows it (`base44/config.jsonc` → `name` can be stale; ask if unsure). **`currency`** is
|
|
404
|
+
**`store_name` is required on a first seed** — the app's name as the platform shows it (`base44/config.jsonc` → `name` can be stale; ask if unsure). **`currency`** is any ISO 4217 code (`"EUR"`, `"NGN"`); formatting follows the viewer's locale, nothing else to set. Explicit values always win, first seed and re-runs alike.
|
|
403
405
|
|
|
404
406
|
The working call — `name` is the only required product key; give each product the keys its own catalog entry actually has and leave the rest out. The **full key list** (sale windows, downloads, tax, backorders, upsells…) lives in `api-admin.md` — open it only if the catalog needs one:
|
|
405
407
|
|
|
406
408
|
```js
|
|
409
|
+
// `base44` is already in scope in exec_tool — never import or redeclare it
|
|
407
410
|
try {
|
|
408
411
|
const res = await base44.functions.invoke("commerce/seed-store", {
|
|
409
412
|
store_name: "Aurora Threads",
|
|
@@ -447,7 +450,7 @@ try {
|
|
|
447
450
|
],
|
|
448
451
|
},
|
|
449
452
|
],
|
|
450
|
-
coupons: [{ code: "WELCOME10", discount_type: "percent", amount: 10 }],
|
|
453
|
+
coupons: [{ code: "WELCOME10", discount_type: "percent", amount: 10 }], // percent | fixed_cart | fixed_product
|
|
451
454
|
// redemption is the shipped cart/checkout's job
|
|
452
455
|
// locations: [ … ], // shipping — next section; passing any makes them the store's ONLY ones
|
|
453
456
|
});
|
|
@@ -459,7 +462,7 @@ try {
|
|
|
459
462
|
|
|
460
463
|
**Running this through a code-execution tool? Return `res.data`, never the raw response** — the raw response carries circular objects and fails `Converting circular structure to JSON` *even when the seed succeeded*; a thrown error needs `e.response?.data` for the same reason.
|
|
461
464
|
|
|
462
|
-
Reference taxonomy by **display name** — existing records are matched case-insensitively and reused. The seeder derives slugs, checks SKU uniqueness, prices variations, and **rolls the parent's price up from the cheapest publishable variant** — never set a variant parent's price yourself. Unknown keys are rejected, so typos surface. **Idempotency:** a product whose `sku` (or derived slug) exists is skipped and reported — safe to retry. Bad payloads fail **400** `invalid_payload` with `errors: [{ path, error }]` before anything is written; per-call limits (≤100 products, ≤500 variations, ≤50 locations) and the full key list are in [`../docs/api-admin.md`](../docs/api-admin.md#commerceseed-store).
|
|
465
|
+
Reference taxonomy by **display name** — existing records are matched case-insensitively and reused. The seeder derives slugs, checks SKU uniqueness, prices variations, and **rolls the parent's price up from the cheapest publishable variant** — never set a variant parent's price yourself. Unknown keys are rejected, so typos surface (`on_sale` is derived and ignored; `brand` becomes a `Brand` spec row). **Idempotency:** a product whose `sku` (or derived slug) exists is skipped and reported — safe to retry. Bad payloads fail **400** `invalid_payload` with `errors: [{ path, error }]` before anything is written; per-call limits (≤100 products, ≤500 variations, ≤50 locations) and the full key list are in [`../docs/api-admin.md`](../docs/api-admin.md#commerceseed-store).
|
|
463
466
|
|
|
464
467
|
The response reports everything; these matter downstream:
|
|
465
468
|
|