@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.
@@ -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 ? CURRENCIES.find((c) => c.code === currencyCode) : undefined;
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 a known ISO currency code` });
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.5",
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",
@@ -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 | 52K |
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**every product image, before anything else.
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
- 3. **Seed the moment the image URLs are back** — one `commerce/seed-store` call ([stage 03 below](#03--store-data)). Its writes run in parallel, so the call usually takes a few seconds — but don't idle on it either.
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
- ⚑ **Only some of what lives there is importable.** Hooks and helpers (`useProduct`, `useCart`, `productImages`, …) are named exports; reviews exist **only on the client** `useStorefront()` returns `submitReview`, `getProductReviews`. Importing one by name throws `does not provide an export named …` and blanks the whole app: `const c = useStorefront(); await c.submitReview(…)`.
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**: copy `src/commerce/storefront-ui/i18n/locales/en.js` to `<lang>.js`, translate the values (~2–3K chars), repoint the one `import active` line in `src/commerce/storefront-ui/i18n/index.js`. Same-language brand props alone don't need this.
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 an ISO code (`"EUR"`); formatting follows the viewer's locale, nothing else to set. Explicit values always win, first seed and re-runs alike.
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