@base44/app-plugin-commerce 0.1.3 → 0.1.4

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 (83) hide show
  1. package/README.md +8 -8
  2. package/base44/agents/commerce/StoreAdmin.jsonc +7 -7
  3. package/base44/entities/commerce.Order.jsonc +1 -1
  4. package/base44/entities/commerce.PaymentGateway.jsonc +3 -3
  5. package/base44/entities/commerce.Product.jsonc +9 -45
  6. package/base44/entities/commerce.ProductAttribute.jsonc +7 -19
  7. package/base44/entities/commerce.ProductAttributeTerm.jsonc +4 -10
  8. package/base44/entities/commerce.ProductTag.jsonc +1 -8
  9. package/base44/entities/commerce.ProductVariation.jsonc +1 -1
  10. package/base44/entities/commerce.StoreSettings.jsonc +1 -1
  11. package/base44/functions/commerce/admin-products/entry.ts +107 -35
  12. package/base44/functions/commerce/admin-refunds/entry.ts +1 -1
  13. package/base44/functions/commerce/admin-reports/entry.ts +1 -1
  14. package/base44/functions/commerce/admin-tools/entry.ts +4 -3
  15. package/base44/functions/commerce/payments/entry.ts +0 -2
  16. package/base44/functions/commerce/seed-store/defaults.ts +13 -39
  17. package/base44/functions/commerce/seed-store/entry.ts +47 -35
  18. package/base44/functions/commerce/seed-store/sample-data.ts +28 -66
  19. package/base44/functions/commerce/storefront-cart/entry.ts +0 -7
  20. package/base44/functions/commerce/storefront-catalog/entry.ts +12 -21
  21. package/base44/functions/commerce/storefront-checkout/entry.ts +5 -12
  22. package/base44/shared/commerce/coupons.ts +4 -7
  23. package/base44/shared/commerce/email-templates.ts +15 -13
  24. package/base44/shared/commerce/emails.ts +4 -3
  25. package/base44/shared/commerce/payments.ts +8 -20
  26. package/base44/shared/commerce/products.ts +24 -0
  27. package/base44/shared/commerce/settings.ts +2 -3
  28. package/base44/shared/commerce/stock.ts +2 -4
  29. package/base44/shared/commerce/totals.ts +37 -42
  30. package/package.json +1 -1
  31. package/scripts/install.js +21 -4
  32. package/skills/commerce/SKILL.md +6 -5
  33. package/skills/commerce/docs/api-admin.md +16 -14
  34. package/skills/commerce/docs/api-storefront.md +39 -35
  35. package/skills/commerce/installation-guidelines.md +4 -3
  36. package/skills/commerce/post-installation.md +22 -25
  37. package/skills/commerce/references/admin-product-form.md +56 -0
  38. package/skills/commerce/references/emails.md +4 -3
  39. package/skills/commerce/references/guest-access-security.md +2 -2
  40. package/skills/commerce/references/limits-and-performance.md +1 -1
  41. package/skills/commerce/references/online-payments.md +8 -8
  42. package/skills/commerce/references/product-render.md +25 -23
  43. package/skills/commerce/references/storefront-product-page.md +9 -9
  44. package/skills/commerce/references/webhooks.md +3 -1
  45. package/src/commerce/admin/README.md +4 -4
  46. package/src/commerce/admin/context/BasePathContext.jsx +5 -5
  47. package/src/commerce/admin/context/SettingsContext.jsx +4 -4
  48. package/src/commerce/admin/index.jsx +3 -3
  49. package/src/commerce/admin/layout/Sidebar.jsx +1 -9
  50. package/src/commerce/admin/lib/constants.js +0 -7
  51. package/src/commerce/admin/lib/paths.js +6 -6
  52. package/src/commerce/admin/lib/product-utils.js +18 -15
  53. package/src/commerce/admin/pages/orders/components/AddProductDialog.jsx +8 -8
  54. package/src/commerce/admin/pages/products/ProductEditor.jsx +10 -12
  55. package/src/commerce/admin/pages/products/ProductsList.jsx +3 -15
  56. package/src/commerce/admin/pages/products/components/AttributesSection.jsx +426 -0
  57. package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +33 -69
  58. package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +1 -1
  59. package/src/commerce/admin/pages/products/components/tabs/LinkedTab.jsx +1 -14
  60. package/src/commerce/admin/pages/products/components/tabs/ModifiersTab.jsx +19 -0
  61. package/src/commerce/admin/pages/products/components/tabs/PriceInventoryTab.jsx +728 -0
  62. package/src/commerce/admin/pages/reports/Reports.jsx +3 -3
  63. package/src/commerce/admin/pages/settings/EmailsSettings.jsx +12 -14
  64. package/src/commerce/admin/pages/settings/GeneralSettings.jsx +6 -116
  65. package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +101 -62
  66. package/src/commerce/admin/pages/settings/SettingsLayout.jsx +2 -6
  67. package/src/commerce/admin/pages/settings/TaxSettings.jsx +0 -1
  68. package/src/commerce/admin/pages/status/WebhookEditor.jsx +3 -3
  69. package/src/commerce/admin/pages/status/Webhooks.jsx +2 -2
  70. package/src/commerce/admin/routes.jsx +10 -19
  71. package/src/commerce/utils/index.js +2 -2
  72. package/src/commerce/utils/shipping-promos.js +9 -6
  73. package/src/commerce/utils/variants.js +21 -21
  74. package/src/commerce/admin/pages/products/AttributeTerms.jsx +0 -180
  75. package/src/commerce/admin/pages/products/Attributes.jsx +0 -183
  76. package/src/commerce/admin/pages/products/Tags.jsx +0 -150
  77. package/src/commerce/admin/pages/products/components/tabs/AdvancedTab.jsx +0 -48
  78. package/src/commerce/admin/pages/products/components/tabs/AttributesTab.jsx +0 -208
  79. package/src/commerce/admin/pages/products/components/tabs/ExternalTab.jsx +0 -41
  80. package/src/commerce/admin/pages/products/components/tabs/GeneralTab.jsx +0 -103
  81. package/src/commerce/admin/pages/products/components/tabs/InventoryTab.jsx +0 -93
  82. package/src/commerce/admin/pages/products/components/tabs/ShippingTab.jsx +0 -86
  83. package/src/commerce/admin/pages/products/components/tabs/VariationsTab.jsx +0 -377
@@ -155,8 +155,8 @@ function SetupScreen({ onDone }) {
155
155
  onChange={(e) => setStoreName(e.target.value)}
156
156
  />
157
157
  <p className="text-xs text-muted-foreground">
158
- Shown to customers and used as the sender name on every transactional email.
159
- Editable later in Settings → General.
158
+ Used in the subject and as the sender name on every transactional email.
159
+ Editable later in Settings → Emails.
160
160
  </p>
161
161
  </div>
162
162
  {hasProducts === false && (
@@ -171,8 +171,8 @@ function SetupScreen({ onDone }) {
171
171
  Include sample data
172
172
  </Label>
173
173
  <p className="text-xs text-muted-foreground">
174
- Adds demo categories, products (simple, variable, downloadable,
175
- external, grouped) and coupons so you can explore the admin.
174
+ Adds demo categories, 10 products (three with size/colour variants, one
175
+ downloadable) and coupons so you can explore the admin.
176
176
  </p>
177
177
  </div>
178
178
  </div>
@@ -10,15 +10,15 @@ import { BasePathProvider } from "./context/BasePathContext";
10
10
  * The store admin application.
11
11
  *
12
12
  * Mount inside your app's router:
13
- * <Route path="/admin/*" element={<AdminApp />} />
13
+ * <Route path="/store-admin/*" element={<AdminApp />} />
14
14
  *
15
- * If mounted somewhere other than /admin, pass the prefix:
15
+ * If mounted somewhere other than /store-admin, pass the prefix:
16
16
  * <Route path="/backoffice/*" element={<AdminApp basePath="/backoffice" />} />
17
17
  *
18
18
  * Requires an authenticated user with role "admin" (enforced by AuthGuard,
19
19
  * and independently by entity RLS + requireAdmin() in backend functions).
20
20
  */
21
- export default function AdminApp({ basePath = "/admin" }) {
21
+ export default function AdminApp({ basePath = "/store-admin" }) {
22
22
  return (
23
23
  <BasePathProvider value={basePath}>
24
24
  <Toaster richColors position="top-right" />
@@ -10,7 +10,6 @@ import {
10
10
  ShoppingCart,
11
11
  Sparkles,
12
12
  Users,
13
- Webhook,
14
13
  } from "lucide-react";
15
14
  import { call } from "../lib/api";
16
15
  import { useAdminHref } from "../context/BasePathContext";
@@ -30,8 +29,6 @@ const NAV = [
30
29
  items: [
31
30
  { label: "All Products", path: "products", end: true },
32
31
  { label: "Categories", path: "products/categories" },
33
- { label: "Tags", path: "products/tags" },
34
- { label: "Attributes", path: "products/attributes" },
35
32
  { label: "Reviews", path: "products/reviews" },
36
33
  ],
37
34
  },
@@ -43,11 +40,6 @@ const NAV = [
43
40
  {
44
41
  items: [{ label: "Settings", path: "settings", icon: Settings }],
45
42
  },
46
- {
47
- title: "Status",
48
- icon: Webhook,
49
- items: [{ label: "Webhooks", path: "webhooks" }],
50
- },
51
43
  ];
52
44
 
53
45
  /** Left navigation. `onNavigate` closes the mobile sheet; `onOpenBot` opens the StoreAdmin bot panel. */
@@ -78,7 +70,7 @@ export default function Sidebar({ onNavigate, onOpenBot }) {
78
70
  <div className="flex h-full flex-col">
79
71
  <div className="flex h-14 items-center gap-2 border-b px-4">
80
72
  <Package className="h-5 w-5 text-primary" />
81
- <span className="font-semibold">Store Admin</span>
73
+ <span className="font-semibold">Store Management</span>
82
74
  </div>
83
75
  <nav className="flex-1 space-y-4 overflow-y-auto p-3">
84
76
  {NAV.map((group, gi) => (
@@ -17,13 +17,6 @@ export const PRODUCT_STATUSES = [
17
17
  { value: "publish", label: "Published", color: "bg-green-100 text-green-800 border-green-200" },
18
18
  ];
19
19
 
20
- export const PRODUCT_TYPES = [
21
- { value: "simple", label: "Simple product" },
22
- { value: "grouped", label: "Grouped product" },
23
- { value: "external", label: "External/Affiliate product" },
24
- { value: "variable", label: "Variable product" },
25
- ];
26
-
27
20
  export const CATALOG_VISIBILITIES = [
28
21
  { value: "visible", label: "Shop and search results" },
29
22
  { value: "catalog", label: "Shop only" },
@@ -1,16 +1,16 @@
1
1
  /**
2
2
  * Mount-path handling for the admin app.
3
3
  *
4
- * The admin is mounted with a splat route — `<Route path="/admin/*">` — and both
4
+ * The admin is mounted with a splat route — `<Route path="/store-admin/*">` — and both
5
5
  * halves of that pattern leak into places they shouldn't: the `basePath` prop
6
- * gets the pattern pasted in verbatim, and the literal URL `/admin/*` gets
6
+ * gets the pattern pasted in verbatim, and the literal URL `/store-admin/*` gets
7
7
  * opened (pattern copied into the address bar, or a nav link built from the
8
8
  * route table). Neither is a real page, so both are normalized here rather than
9
9
  * dead-ending the admin.
10
10
  */
11
11
 
12
- /** Strip a trailing route splat and slashes: "/admin/*" → "/admin". */
13
- export function normalizeBasePath(value, fallback = "/admin") {
12
+ /** Strip a trailing route splat and slashes: "/store-admin/*" → "/store-admin". */
13
+ export function normalizeBasePath(value, fallback = "/store-admin") {
14
14
  const clean = String(value ?? "")
15
15
  .replace(/\/+\*+$/, "")
16
16
  .replace(/\/+$/, "");
@@ -21,8 +21,8 @@ export function normalizeBasePath(value, fallback = "/admin") {
21
21
  * Is this leftover path (the `*` param of the catch-all route) nothing but
22
22
  * route-*pattern* segments — `*`, its percent-encoded form, or `:param`?
23
23
  *
24
- * `/admin/*` → true (send them to the dashboard)
25
- * `/admin/ordrs` → false (a genuine 404)
24
+ * `/store-admin/*` → true (send them to the dashboard)
25
+ * `/store-admin/ordrs` → false (a genuine 404)
26
26
  */
27
27
  export function isMountPatternPath(rest) {
28
28
  const segments = String(rest ?? "")
@@ -9,20 +9,20 @@ export function slugify(name) {
9
9
  .replace(/^-+|-+$/g, "");
10
10
  }
11
11
 
12
- export const isSimple = (p) => p?.type === "simple";
13
- export const isGrouped = (p) => p?.type === "grouped";
14
- export const isExternal = (p) => p?.type === "external";
15
- export const isVariable = (p) => p?.type === "variable";
12
+ /**
13
+ * A product sells variants because it carries attributes — every entry in
14
+ * `attributes[]` is a variant axis, so attaching one is the whole declaration.
15
+ * Descriptive properties are `meta_data` (modifiers), not attributes.
16
+ */
17
+ export const isVariable = (p) => (p?.attributes ?? []).length > 0;
16
18
 
17
19
  /**
18
- * Cartesian product of all attributes flagged `variation: true`.
20
+ * Cartesian product of every attribute that has options.
19
21
  * Returns [{attributes: [{attribute_id, name, option}]}] — one entry per combo.
20
- * Returns [] when no variation attributes with options exist.
22
+ * Returns [] when no attribute has options yet.
21
23
  */
22
24
  export function generateVariationCombos(attributes) {
23
- const varAttrs = (attributes || []).filter(
24
- (a) => a.variation && (a.options || []).length > 0
25
- );
25
+ const varAttrs = (attributes || []).filter((a) => (a.options || []).length > 0);
26
26
  if (!varAttrs.length) return [];
27
27
  const combos = varAttrs.reduce(
28
28
  (acc, attr) =>
@@ -44,12 +44,15 @@ export function variationLabel(variation) {
44
44
  .join(" / ") || "Any";
45
45
  }
46
46
 
47
+ /** Order-independent identity of a variation's attribute combination. */
48
+ export function comboKey(variation) {
49
+ return (variation?.attributes || [])
50
+ .map((x) => `${(x.name || "").toLowerCase()}=${(x.option || "").toLowerCase()}`)
51
+ .sort()
52
+ .join("|");
53
+ }
54
+
47
55
  /** True when two variations have the same attribute combo. */
48
56
  export function sameCombo(a, b) {
49
- const key = (v) =>
50
- (v?.attributes || [])
51
- .map((x) => `${(x.name || "").toLowerCase()}=${(x.option || "").toLowerCase()}`)
52
- .sort()
53
- .join("|");
54
- return key(a) === key(b);
57
+ return comboKey(a) === comboKey(b);
55
58
  }
@@ -20,11 +20,11 @@ import { Loader2 } from "lucide-react";
20
20
 
21
21
  import { call, base44 } from "../../../lib/api";
22
22
  import SearchSelect from "../../../components/SearchSelect";
23
- import { variationLabel } from "../../../lib/product-utils";
23
+ import { isVariable, variationLabel } from "../../../lib/product-utils";
24
24
 
25
25
  /**
26
26
  * Two-step product picker for the order editor:
27
- * product search → (variable only) variation select → quantity.
27
+ * product search → (products with variants only) variant select → quantity.
28
28
  *
29
29
  * Props: { open, onOpenChange, onAdd({product, variation|null, quantity}) }
30
30
  */
@@ -50,7 +50,7 @@ export default function AddProductDialog({ open, onOpenChange, onAdd }) {
50
50
  return rows.map((p) => ({
51
51
  value: p.id,
52
52
  label: p.name,
53
- meta: [p.sku && `SKU: ${p.sku}`, p.type].filter(Boolean).join(" · "),
53
+ meta: p.sku ? `SKU: ${p.sku}` : undefined,
54
54
  product: p,
55
55
  }));
56
56
  };
@@ -59,7 +59,7 @@ export default function AddProductDialog({ open, onOpenChange, onAdd }) {
59
59
  setPicked(opt);
60
60
  setVariations(null);
61
61
  setVariationId("");
62
- if (opt?.product?.type === "variable") {
62
+ if (isVariable(opt?.product)) {
63
63
  setLoadingVariations(true);
64
64
  try {
65
65
  const rows = await base44.entities["commerce.ProductVariation"].filter({ product_id: opt.value }, "menu_order", 200);
@@ -70,11 +70,11 @@ export default function AddProductDialog({ open, onOpenChange, onAdd }) {
70
70
  }
71
71
  };
72
72
 
73
- const isVariable = picked?.product?.type === "variable";
74
- const canAdd = picked && quantity > 0 && (!isVariable || variationId);
73
+ const hasVariants = isVariable(picked?.product);
74
+ const canAdd = picked && quantity > 0 && (!hasVariants || variationId);
75
75
 
76
76
  const submit = () => {
77
- const variation = isVariable ? (variations || []).find((v) => v.id === variationId) : null;
77
+ const variation = hasVariants ? (variations || []).find((v) => v.id === variationId) : null;
78
78
  onAdd({ product: picked.product, variation, quantity });
79
79
  onOpenChange(false);
80
80
  };
@@ -97,7 +97,7 @@ export default function AddProductDialog({ open, onOpenChange, onAdd }) {
97
97
  />
98
98
  </div>
99
99
 
100
- {isVariable && (
100
+ {hasVariants && (
101
101
  <div className="grid gap-1.5">
102
102
  <Label>Variation</Label>
103
103
  {loadingVariations ? (
@@ -11,7 +11,7 @@ import { useAdminHref } from "../../context/BasePathContext";
11
11
  import PageHeader from "../../components/PageHeader";
12
12
  import MediaUploader from "../../components/MediaUploader";
13
13
  import RichTextarea from "../../components/RichTextarea";
14
- import { slugify, isVariable } from "../../lib/product-utils";
14
+ import { slugify } from "../../lib/product-utils";
15
15
  import ProductDataPanel from "./components/ProductDataPanel";
16
16
  import TaxonomyPanel from "./components/TaxonomyPanel";
17
17
  import PublishBox from "./components/PublishBox";
@@ -19,7 +19,6 @@ import PublishBox from "./components/PublishBox";
19
19
  const NEW_PRODUCT = {
20
20
  name: "",
21
21
  slug: "",
22
- type: "simple",
23
22
  status: "draft",
24
23
  featured: false,
25
24
  catalog_visibility: "visible",
@@ -35,8 +34,6 @@ const NEW_PRODUCT = {
35
34
  downloads: [],
36
35
  download_limit: -1,
37
36
  download_expiry: -1,
38
- external_url: "",
39
- button_text: "",
40
37
  tax_status: "taxable",
41
38
  tax_class: "standard",
42
39
  manage_stock: false,
@@ -48,17 +45,13 @@ const NEW_PRODUCT = {
48
45
  weight: null,
49
46
  dimensions: { length: null, width: null, height: null },
50
47
  shipping_class_id: "",
51
- reviews_allowed: true,
52
48
  upsell_ids: [],
53
49
  cross_sell_ids: [],
54
- grouped_products: [],
55
- purchase_note: "",
56
50
  category_ids: [],
57
51
  tag_ids: [],
58
52
  images: [],
59
53
  attributes: [],
60
54
  default_attributes: [],
61
- menu_order: 0,
62
55
  meta_data: [],
63
56
  };
64
57
 
@@ -83,7 +76,10 @@ export default function ProductEditor() {
83
76
  () => base44.entities["commerce.ProductTag"].list(undefined, 1000),
84
77
  []
85
78
  );
86
- const { data: attributes } = useAsync(() => base44.entities["commerce.ProductAttribute"].list(undefined, 500), []);
79
+ const { data: attributes, refetch: refreshAttributes } = useAsync(
80
+ () => base44.entities["commerce.ProductAttribute"].list("order", 500),
81
+ []
82
+ );
87
83
  const { data: shippingClasses } = useAsync(() => base44.entities["commerce.ShippingClass"].list(undefined, 500), []);
88
84
  const { data: taxClasses } = useAsync(() => base44.entities["commerce.TaxClass"].list(undefined, 100), []);
89
85
 
@@ -127,8 +123,7 @@ export default function ProductEditor() {
127
123
  }
128
124
  setSaving(true);
129
125
  try {
130
- const payload = { product: { ...product, name: product.name.trim() } };
131
- if (isVariable(product)) payload.variations = variations;
126
+ const payload = { product: { ...product, name: product.name.trim() }, variations };
132
127
  const data = await call("admin-products", "save", payload);
133
128
  const savedId = data?.product?.id || data?.id || id;
134
129
  toast.success(isNew ? "Product created" : "Product saved");
@@ -162,7 +157,10 @@ export default function ProductEditor() {
162
157
  );
163
158
  }
164
159
 
165
- const refs = { categories, tags, attributes, shippingClasses, taxClasses, refreshCategories, refreshTags };
160
+ const refs = {
161
+ categories, tags, attributes, shippingClasses, taxClasses,
162
+ refreshCategories, refreshTags, refreshAttributes,
163
+ };
166
164
 
167
165
  return (
168
166
  <div className="space-y-4">
@@ -32,7 +32,7 @@ import SearchSelect from "../../components/SearchSelect";
32
32
  import StatusBadge from "../../components/StatusBadge";
33
33
  import MoneyInput from "../../components/MoneyInput";
34
34
  import ConfirmDialog from "../../components/ConfirmDialog";
35
- import { PRODUCT_TYPES, PRODUCT_STATUSES, STOCK_STATUSES, statusMeta } from "../../lib/constants";
35
+ import { PRODUCT_STATUSES, STOCK_STATUSES, statusMeta } from "../../lib/constants";
36
36
  import { formatDate } from "../../lib/format";
37
37
 
38
38
  const ALL = "all";
@@ -123,7 +123,6 @@ export default function ProductsList() {
123
123
 
124
124
  const [q, setQ] = useState("");
125
125
  const [category, setCategory] = useState(null);
126
- const [type, setType] = useState(ALL);
127
126
  const [stock, setStock] = useState(ALL);
128
127
  const [selected, setSelected] = useState([]);
129
128
  const [quickEdit, setQuickEdit] = useState(null);
@@ -145,7 +144,6 @@ export default function ProductsList() {
145
144
  const data = await call("admin-products", "search", {
146
145
  q: debouncedQ || undefined,
147
146
  category_id: category?.value || undefined,
148
- type: type === ALL ? undefined : type,
149
147
  stock_status: stock === ALL ? undefined : stock,
150
148
  sort,
151
149
  limit,
@@ -154,14 +152,13 @@ export default function ProductsList() {
154
152
  return data?.rows || data || [];
155
153
  }
156
154
  const query = {};
157
- if (type !== ALL) query.type = type;
158
155
  if (stock !== ALL) query.stock_status = stock;
159
156
  return base44.entities["commerce.Product"].filter(query, sort, limit, skip);
160
157
  },
161
- [usingSearch, debouncedQ, category, type, stock]
158
+ [usingSearch, debouncedQ, category, stock]
162
159
  );
163
160
 
164
- const list = usePagedList(fetcher, { deps: [debouncedQ, category?.value, type, stock] });
161
+ const list = usePagedList(fetcher, { deps: [debouncedQ, category?.value, stock] });
165
162
 
166
163
  const toggleFeatured = async (row) => {
167
164
  await call("admin-products", "save", { product: { id: row.id, featured: !row.featured } });
@@ -322,15 +319,6 @@ export default function ProductsList() {
322
319
  .map((c) => ({ value: c.id, label: c.name }))
323
320
  }
324
321
  />
325
- <Select value={type} onValueChange={setType}>
326
- <SelectTrigger className="w-44"><SelectValue /></SelectTrigger>
327
- <SelectContent>
328
- <SelectItem value={ALL}>All types</SelectItem>
329
- {PRODUCT_TYPES.map((t) => (
330
- <SelectItem key={t.value} value={t.value}>{t.label}</SelectItem>
331
- ))}
332
- </SelectContent>
333
- </Select>
334
322
  <Select value={stock} onValueChange={setStock}>
335
323
  <SelectTrigger className="w-40"><SelectValue /></SelectTrigger>
336
324
  <SelectContent>