@base44/app-plugin-commerce 0.1.3 → 0.1.5

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 (86) hide show
  1. package/README.md +11 -11
  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 +73 -121
  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 +50 -166
  18. package/base44/functions/commerce/seed-store/sample-data.ts +28 -66
  19. package/base44/functions/commerce/seed-store/seed-catalog.ts +717 -0
  20. package/base44/functions/commerce/storefront-cart/entry.ts +0 -7
  21. package/base44/functions/commerce/storefront-catalog/entry.ts +12 -21
  22. package/base44/functions/commerce/storefront-checkout/entry.ts +5 -12
  23. package/base44/shared/commerce/catalog.ts +134 -0
  24. package/base44/shared/commerce/coupons.ts +4 -7
  25. package/base44/shared/commerce/email-templates.ts +15 -13
  26. package/base44/shared/commerce/emails.ts +4 -3
  27. package/base44/shared/commerce/payments.ts +8 -20
  28. package/base44/shared/commerce/products.ts +24 -0
  29. package/base44/shared/commerce/settings.ts +2 -3
  30. package/base44/shared/commerce/stock.ts +2 -4
  31. package/base44/shared/commerce/totals.ts +37 -42
  32. package/package.json +1 -1
  33. package/scripts/install.js +23 -7
  34. package/skills/commerce/SKILL.md +9 -7
  35. package/skills/commerce/docs/api-admin.md +20 -14
  36. package/skills/commerce/docs/api-storefront.md +39 -35
  37. package/skills/commerce/installation-guidelines.md +8 -7
  38. package/skills/commerce/post-installation.md +205 -84
  39. package/skills/commerce/references/admin-product-form.md +56 -0
  40. package/skills/commerce/references/emails.md +4 -3
  41. package/skills/commerce/references/guest-access-security.md +2 -2
  42. package/skills/commerce/references/limits-and-performance.md +1 -1
  43. package/skills/commerce/references/online-payments.md +8 -8
  44. package/skills/commerce/references/product-render.md +25 -23
  45. package/skills/commerce/references/reviews.md +20 -0
  46. package/skills/commerce/references/storefront-product-page.md +9 -9
  47. package/skills/commerce/references/webhooks.md +3 -1
  48. package/src/commerce/admin/README.md +4 -4
  49. package/src/commerce/admin/context/BasePathContext.jsx +5 -5
  50. package/src/commerce/admin/context/SettingsContext.jsx +4 -4
  51. package/src/commerce/admin/index.jsx +3 -3
  52. package/src/commerce/admin/layout/Sidebar.jsx +1 -9
  53. package/src/commerce/admin/lib/constants.js +0 -7
  54. package/src/commerce/admin/lib/paths.js +6 -6
  55. package/src/commerce/admin/lib/product-utils.js +18 -15
  56. package/src/commerce/admin/pages/orders/components/AddProductDialog.jsx +8 -8
  57. package/src/commerce/admin/pages/products/ProductEditor.jsx +10 -12
  58. package/src/commerce/admin/pages/products/ProductsList.jsx +3 -15
  59. package/src/commerce/admin/pages/products/components/AttributesSection.jsx +426 -0
  60. package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +33 -69
  61. package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +1 -1
  62. package/src/commerce/admin/pages/products/components/tabs/LinkedTab.jsx +1 -14
  63. package/src/commerce/admin/pages/products/components/tabs/ModifiersTab.jsx +19 -0
  64. package/src/commerce/admin/pages/products/components/tabs/PriceInventoryTab.jsx +728 -0
  65. package/src/commerce/admin/pages/reports/Reports.jsx +3 -3
  66. package/src/commerce/admin/pages/settings/EmailsSettings.jsx +12 -14
  67. package/src/commerce/admin/pages/settings/GeneralSettings.jsx +6 -116
  68. package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +101 -62
  69. package/src/commerce/admin/pages/settings/SettingsLayout.jsx +2 -6
  70. package/src/commerce/admin/pages/settings/TaxSettings.jsx +0 -1
  71. package/src/commerce/admin/pages/status/WebhookEditor.jsx +3 -3
  72. package/src/commerce/admin/pages/status/Webhooks.jsx +2 -2
  73. package/src/commerce/admin/routes.jsx +10 -19
  74. package/src/commerce/utils/index.js +2 -2
  75. package/src/commerce/utils/shipping-promos.js +9 -6
  76. package/src/commerce/utils/variants.js +21 -21
  77. package/src/commerce/admin/pages/products/AttributeTerms.jsx +0 -180
  78. package/src/commerce/admin/pages/products/Attributes.jsx +0 -183
  79. package/src/commerce/admin/pages/products/Tags.jsx +0 -150
  80. package/src/commerce/admin/pages/products/components/tabs/AdvancedTab.jsx +0 -48
  81. package/src/commerce/admin/pages/products/components/tabs/AttributesTab.jsx +0 -208
  82. package/src/commerce/admin/pages/products/components/tabs/ExternalTab.jsx +0 -41
  83. package/src/commerce/admin/pages/products/components/tabs/GeneralTab.jsx +0 -103
  84. package/src/commerce/admin/pages/products/components/tabs/InventoryTab.jsx +0 -93
  85. package/src/commerce/admin/pages/products/components/tabs/ShippingTab.jsx +0 -86
  86. package/src/commerce/admin/pages/products/components/tabs/VariationsTab.jsx +0 -377
@@ -1,22 +1,25 @@
1
1
  /**
2
2
  * commerce/seed-store — idempotent store initialization.
3
3
  *
4
- * Flow: (1) canary schema validation (probe create+delete per entity; abort
5
- * with 422 schema_incompatible and write nothing on any failure), (2) seed
6
- * required defaults (settings groups, gateways, tax classes, fallback zone),
7
- * (3) optional sample catalog (~12 products across every product type, with
8
- * images and descriptions, plus three variable products with variants) — only
9
- * when with_sample_data=true and the store has zero products, with best-effort
10
- * rollback on mid-failure.
4
+ * Flow: (1) validate any catalog payload (pure — 400 invalid_payload with
5
+ * per-field errors before anything runs), (2) canary schema validation (probe
6
+ * create+delete per entity; abort with 422 schema_incompatible and write
7
+ * nothing on any failure), (3) seed required defaults (settings groups,
8
+ * gateways, tax classes, fallback zone), (4) the catalog: either the caller's
9
+ * `products`/`coupons`/`tax_rates` (taxonomy get-or-created by name,
10
+ * variations generated from `attributes` when not given, per-product skip when
11
+ * the sku/slug already exists) or the generic demo catalog
12
+ * (with_sample_data=true, only when the store has zero products) — with
13
+ * best-effort rollback on mid-failure. See seed-catalog.ts for the pipeline.
11
14
  *
12
- * Body: { with_sample_data?: boolean, store_name?: string }
15
+ * Body: { store_name?, with_sample_data?, products?, coupons?, tax_rates? }
16
+ * — with_sample_data cannot be combined with the catalog keys.
13
17
  *
14
18
  * `store_name` is required on a first seed: a function's env is only
15
19
  * BASE44_APP_ID, so it cannot read the app's name, and subjects need one.
16
20
  */
17
21
  import { createClientFromRequest } from "npm:@base44/sdk";
18
22
  import { HttpError, requireAdmin } from "../../../shared/commerce/auth.ts";
19
- import { round2 } from "../../../shared/commerce/money.ts";
20
23
  import {
21
24
  GATEWAY_DEFAULTS,
22
25
  REST_OF_WORLD_EXAMPLE_METHOD,
@@ -24,14 +27,7 @@ import {
24
27
  SETTINGS_DEFAULTS,
25
28
  TAX_CLASS_DEFAULTS,
26
29
  } from "./defaults.ts";
27
- import {
28
- SAMPLE_ATTRIBUTE_TERMS,
29
- SAMPLE_ATTRIBUTES,
30
- SAMPLE_CATEGORIES,
31
- SAMPLE_COUPONS,
32
- SAMPLE_PRODUCTS,
33
- SAMPLE_TAX_RATES,
34
- } from "./sample-data.ts";
30
+ import { CatalogPayloadError, normalizeCatalogPayload, sampleCatalog, seedCatalog } from "./seed-catalog.ts";
35
31
 
36
32
  const ok = (data: unknown, status = 200) => Response.json({ success: true, data }, { status });
37
33
  const fail = (status: number, error: string, code?: string, extra?: Record<string, unknown>) =>
@@ -46,8 +42,18 @@ Deno.serve(async (req) => {
46
42
  const withSample = !!body.with_sample_data;
47
43
  const storeName = String(body.store_name ?? "").trim();
48
44
 
45
+ // ── 0. normalize the catalog payload — pure, so bad input fails before
46
+ // canaries or any write ─────────────────────────────────────────────
47
+ let catalogSpec = null;
48
+ try {
49
+ catalogSpec = normalizeCatalogPayload(body);
50
+ } catch (e) {
51
+ if (e instanceof CatalogPayloadError) return fail(400, e.message, "invalid_payload", { errors: e.errors });
52
+ throw e;
53
+ }
54
+
49
55
  // ── 1. canary schema validation — abort before writing anything real ────
50
- const canaryErrors = await runCanaries(sr, withSample);
56
+ const canaryErrors = await runCanaries(sr, withSample || !!catalogSpec);
51
57
  if (canaryErrors.length) {
52
58
  return fail(422,
53
59
  "Entity schemas have been modified and are incompatible with the seeder.",
@@ -62,12 +68,12 @@ Deno.serve(async (req) => {
62
68
  const existingGroups = new Set(existingSettings.map((s: any) => s.group_id));
63
69
  // A nameless store renders subjects like "[]: New order #1002", so refuse to
64
70
  // create one rather than seeding a blank the caller never sees.
65
- if (!existingGroups.has("general") && !storeName) {
71
+ if (!existingGroups.has("emails") && !storeName) {
66
72
  return fail(400, "store_name is required — pass the app's name as the platform shows it.", "store_name_required");
67
73
  }
68
74
  for (const group of SETTINGS_DEFAULTS) {
69
75
  if (existingGroups.has(group.group_id)) continue;
70
- const values = group.group_id === "general"
76
+ const values = group.group_id === "emails"
71
77
  ? { ...group.values, store_name: storeName }
72
78
  : group.values;
73
79
  await sr.entities["commerce.StoreSettings"].create({ ...group, values });
@@ -77,12 +83,12 @@ Deno.serve(async (req) => {
77
83
  // A passed store_name must never be silently dropped: fill a blank one on an
78
84
  // already-seeded store, but never overwrite a name the merchant chose.
79
85
  let storeNameAction = storeName ? "created" : "unchanged";
80
- if (storeName && existingGroups.has("general")) {
81
- const general = existingSettings.find((r: any) => r.group_id === "general");
82
- const current = String(general?.values?.store_name ?? "").trim();
86
+ if (storeName && existingGroups.has("emails")) {
87
+ const emails = existingSettings.find((r: any) => r.group_id === "emails");
88
+ const current = String(emails?.values?.store_name ?? "").trim();
83
89
  if (!current) {
84
- await sr.entities["commerce.StoreSettings"].update(general.id, {
85
- values: { ...(general.values ?? {}), store_name: storeName },
90
+ await sr.entities["commerce.StoreSettings"].update(emails.id, {
91
+ values: { ...(emails.values ?? {}), store_name: storeName },
86
92
  });
87
93
  storeNameAction = "filled";
88
94
  } else {
@@ -115,21 +121,27 @@ Deno.serve(async (req) => {
115
121
  seeded.zone_methods++;
116
122
  }
117
123
 
118
- // ── 3. optional sample catalog ───────────────────────────────────────────
119
- let sampleResult: Record<string, number> | null = null;
124
+ // ── 3. the catalog: caller-supplied, or the demo sample ──────────────────
125
+ let sampleResult: Record<string, any> | null = null;
120
126
  if (withSample) {
121
127
  const anyProduct = (await sr.entities["commerce.Product"].list(undefined, 1)) ?? [];
122
128
  if (!anyProduct.length) {
123
- sampleResult = await seedSampleData(sr);
129
+ sampleResult = await seedCatalog(sr, sampleCatalog(), { skipExisting: false, errorCode: "sample_seed_failed" });
124
130
  }
125
131
  }
126
132
 
133
+ let catalogResult: Record<string, any> | null = null;
134
+ if (catalogSpec) {
135
+ catalogResult = await seedCatalog(sr, catalogSpec, { skipExisting: true, errorCode: "catalog_seed_failed" });
136
+ }
137
+
127
138
  const currentName = String(
128
- (existingSettings.find((r: any) => r.group_id === "general")?.values?.store_name ?? storeName) || storeName,
139
+ (existingSettings.find((r: any) => r.group_id === "emails")?.values?.store_name ?? storeName) || storeName,
129
140
  );
130
141
  return ok({
131
142
  seeded,
132
143
  sample_data: sampleResult ?? false,
144
+ catalog: catalogResult,
133
145
  store_name: { value: storeNameAction === "kept_existing" ? currentName : storeName, action: storeNameAction },
134
146
  });
135
147
  } catch (e) {
@@ -142,7 +154,7 @@ Deno.serve(async (req) => {
142
154
  // ── canaries ─────────────────────────────────────────────────────────────────
143
155
 
144
156
  /** Minimal-valid probe records per entity the seeder writes. */
145
- function canarySpecs(withSample: boolean): Array<{ entity: string; record: Record<string, any> }> {
157
+ function canarySpecs(needsCatalog: boolean): Array<{ entity: string; record: Record<string, any> }> {
146
158
  const base = [
147
159
  { entity: "commerce.StoreSettings", record: { group_id: "general", values: { __canary: true } } },
148
160
  { entity: "commerce.PaymentGateway", record: { slug: "__canary", title: "Canary", enabled: false, order: 999, settings: {} } },
@@ -150,21 +162,22 @@ function canarySpecs(withSample: boolean): Array<{ entity: string; record: Recor
150
162
  { entity: "commerce.ShippingZone", record: { name: "__canary", order: 998, locations: [] } },
151
163
  { entity: "commerce.ShippingZoneMethod", record: { zone_id: "__canary", method_id: "flat_rate", title: "Canary", enabled: false, order: 0, settings: { cost: 0 } } },
152
164
  ];
153
- const sample = [
165
+ const catalog = [
154
166
  { entity: "commerce.ProductCategory", record: { name: "__canary", slug: "__canary", count: 0 } },
155
- { entity: "commerce.ProductAttribute", record: { name: "__canary", slug: "__canary", type: "select" } },
156
- { entity: "commerce.ProductAttributeTerm", record: { attribute_id: "__canary", name: "__canary", slug: "__canary" } },
157
- { entity: "commerce.Product", record: { name: "__canary", type: "simple", status: "draft", regular_price: 1, price: 1 } },
167
+ { entity: "commerce.ProductTag", record: { name: "__canary", count: 0 } },
168
+ { entity: "commerce.ProductAttribute", record: { name: "__canary", code: "__canary", order: 0 } },
169
+ { entity: "commerce.ProductAttributeTerm", record: { attribute_id: "__canary", name: "__canary", order: 0 } },
170
+ { entity: "commerce.Product", record: { name: "__canary", status: "draft", regular_price: 1, price: 1 } },
158
171
  { entity: "commerce.ProductVariation", record: { product_id: "__canary", attributes: [], status: "draft" } },
159
172
  { entity: "commerce.Coupon", record: { code: "__canary", discount_type: "percent", amount: 1, usage_count: 0, used_by: [] } },
160
173
  { entity: "commerce.TaxRate", record: { country: "ZZ", rate: 1, name: "__canary", priority: 1, compound: false, shipping: true, tax_class: "standard" } },
161
174
  ];
162
- return withSample ? [...base, ...sample] : base;
175
+ return needsCatalog ? [...base, ...catalog] : base;
163
176
  }
164
177
 
165
- async function runCanaries(sr: any, withSample: boolean): Promise<Array<{ entity: string; error: string }>> {
178
+ async function runCanaries(sr: any, needsCatalog: boolean): Promise<Array<{ entity: string; error: string }>> {
166
179
  const errors: Array<{ entity: string; error: string }> = [];
167
- for (const spec of canarySpecs(withSample)) {
180
+ for (const spec of canarySpecs(needsCatalog)) {
168
181
  let created: any = null;
169
182
  try {
170
183
  created = await sr.entities[spec.entity].create(spec.record);
@@ -179,132 +192,3 @@ async function runCanaries(sr: any, withSample: boolean): Promise<Array<{ entity
179
192
  return errors;
180
193
  }
181
194
 
182
- // ── sample catalog ───────────────────────────────────────────────────────────
183
-
184
- /** price/on_sale/stock_status derivation for seeded products (mirrors commerce/admin-products). */
185
- function deriveSeedProduct(p: any): any {
186
- const out = { ...p };
187
- out.on_sale = out.sale_price != null;
188
- const effective = out.on_sale ? out.sale_price : out.regular_price;
189
- if (effective != null) out.price = round2(Number(effective));
190
- if (out.manage_stock) {
191
- const qty = Number(out.stock_quantity ?? 0);
192
- out.stock_status = qty > 0 ? "instock" : (out.backorders && out.backorders !== "no" ? "onbackorder" : "outofstock");
193
- } else if (!out.stock_status) {
194
- out.stock_status = "instock";
195
- }
196
- return out;
197
- }
198
-
199
- async function seedSampleData(sr: any): Promise<Record<string, number>> {
200
- // rollback ledger — delete in reverse order on failure
201
- const created: Array<{ entity: string; id: string }> = [];
202
- const track = async (entity: string, record: Record<string, any>) => {
203
- const rec = await sr.entities[entity].create(record);
204
- created.push({ entity, id: rec.id });
205
- return rec;
206
- };
207
-
208
- try {
209
- // categories
210
- const categoryBySlug: Record<string, any> = {};
211
- for (const cat of SAMPLE_CATEGORIES) {
212
- categoryBySlug[cat.slug] = await track("commerce.ProductCategory", cat);
213
- }
214
-
215
- // attributes + terms
216
- const attributeBySlug: Record<string, any> = {};
217
- for (const attr of SAMPLE_ATTRIBUTES) {
218
- const rec = await track("commerce.ProductAttribute", attr);
219
- attributeBySlug[attr.slug] = rec;
220
- for (const term of SAMPLE_ATTRIBUTE_TERMS[attr.slug] ?? []) {
221
- await track("commerce.ProductAttributeTerm", { ...term, attribute_id: rec.id, count: 0 });
222
- }
223
- }
224
-
225
- // products (two passes: grouped references resolved after simple products exist)
226
- const productByKey: Record<string, any> = {};
227
- let variationCount = 0;
228
- const specs = [...SAMPLE_PRODUCTS].sort((a, b) => (a.type === "grouped" ? 1 : 0) - (b.type === "grouped" ? 1 : 0));
229
-
230
- for (const spec of specs) {
231
- const { key, categories, attributes, default_attributes, variations, grouped_keys, options: _o, ...fields } = spec;
232
- const record: any = deriveSeedProduct({
233
- ...fields,
234
- slug: fields.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""),
235
- category_ids: (categories ?? []).map((slug: string) => categoryBySlug[slug]?.id).filter(Boolean),
236
- tag_ids: [],
237
- images: fields.images ?? [],
238
- meta_data: [],
239
- total_sales: 0,
240
- });
241
- if (attributes) {
242
- record.attributes = attributes.map((a: any, i: number) => ({
243
- attribute_id: attributeBySlug[a.slug]?.id ?? "",
244
- name: attributeBySlug[a.slug]?.name ?? a.slug,
245
- position: i,
246
- visible: a.visible !== false,
247
- variation: !!a.variation,
248
- options: a.options ?? [],
249
- }));
250
- }
251
- if (default_attributes) {
252
- record.default_attributes = default_attributes.map((d: any) => ({
253
- attribute_id: attributeBySlug[d.slug]?.id ?? "",
254
- name: attributeBySlug[d.slug]?.name ?? d.slug,
255
- option: d.option,
256
- }));
257
- }
258
- if (grouped_keys) {
259
- record.grouped_products = grouped_keys.map((k: string) => productByKey[k]?.id).filter(Boolean);
260
- }
261
- const product = await track("commerce.Product", record);
262
- productByKey[key] = product;
263
-
264
- for (const v of variations ?? []) {
265
- const attrs = Object.entries(v.options).map(([slug, option]) => ({
266
- attribute_id: attributeBySlug[slug]?.id ?? "",
267
- name: attributeBySlug[slug]?.name ?? slug,
268
- option,
269
- }));
270
- await track("commerce.ProductVariation", deriveSeedProduct({
271
- product_id: product.id,
272
- attributes: attrs,
273
- status: "publish",
274
- sku: `${record.sku}-${Object.values(v.options).join("-").toUpperCase()}`,
275
- regular_price: v.regular_price,
276
- ...(v.sale_price != null ? { sale_price: v.sale_price } : {}),
277
- manage_stock: "yes",
278
- stock_quantity: v.stock_quantity,
279
- backorders: v.backorders ?? "no",
280
- ...(v.image ? { image: v.image } : {}),
281
- }));
282
- variationCount++;
283
- }
284
- }
285
-
286
- // taxonomy counts for the seeded catalog
287
- for (const cat of Object.values(categoryBySlug) as any[]) {
288
- const count = specs.filter((s) => (s.categories ?? []).some((slug: string) => categoryBySlug[slug]?.id === cat.id)).length;
289
- await sr.entities["commerce.ProductCategory"].update(cat.id, { count });
290
- }
291
-
292
- for (const coupon of SAMPLE_COUPONS) await track("commerce.Coupon", coupon);
293
- for (const rate of SAMPLE_TAX_RATES) await track("commerce.TaxRate", rate);
294
-
295
- return {
296
- categories: SAMPLE_CATEGORIES.length,
297
- attributes: SAMPLE_ATTRIBUTES.length,
298
- products: SAMPLE_PRODUCTS.length,
299
- variations: variationCount,
300
- coupons: SAMPLE_COUPONS.length,
301
- tax_rates: SAMPLE_TAX_RATES.length,
302
- };
303
- } catch (e) {
304
- // best-effort rollback, newest first
305
- for (const { entity, id } of created.reverse()) {
306
- try { await sr.entities[entity].delete(id); } catch { /* leave orphans; commerce/admin-tools can clean */ }
307
- }
308
- throw new HttpError(500, `Sample data seeding failed and was rolled back: ${(e as Error).message}`, "sample_seed_failed");
309
- }
310
- }
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Optional demo catalog: 3 categories, 2 global attributes (+terms),
3
- * ~12 products covering every product type (including 3 variable products
4
- * with variants), 2 coupons, 2 tax rates. Every product ships with a
2
+ * Optional demo catalog: 3 categories, 2 shared attributes (+values),
3
+ * 10 products (3 of which carry attributes and ship with variants),
4
+ * 2 coupons, 2 tax rates. Every product ships with a
5
5
  * description, short description and a small image gallery so the storefront
6
6
  * and admin look populated out of the box.
7
7
  * Only installed when with_sample_data=true AND the store has no products.
@@ -32,29 +32,29 @@ export const SAMPLE_CATEGORIES = [
32
32
  ];
33
33
 
34
34
  export const SAMPLE_ATTRIBUTES = [
35
- { name: "Color", slug: "color", type: "select", order_by: "menu_order", has_archives: false },
36
- { name: "Size", slug: "size", type: "select", order_by: "menu_order", has_archives: false },
35
+ { name: "Color", code: "color", order: 0 },
36
+ { name: "Size", code: "size", order: 1 },
37
37
  ];
38
38
 
39
- export const SAMPLE_ATTRIBUTE_TERMS: Record<string, Array<{ name: string; slug: string; menu_order: number }>> = {
39
+ export const SAMPLE_ATTRIBUTE_TERMS: Record<string, Array<{ name: string; order: number }>> = {
40
40
  color: [
41
- { name: "Red", slug: "red", menu_order: 0 },
42
- { name: "Blue", slug: "blue", menu_order: 1 },
43
- { name: "Black", slug: "black", menu_order: 2 },
44
- { name: "Green", slug: "green", menu_order: 3 },
41
+ { name: "Red", order: 0 },
42
+ { name: "Blue", order: 1 },
43
+ { name: "Black", order: 2 },
44
+ { name: "Green", order: 3 },
45
45
  ],
46
46
  size: [
47
- { name: "S", slug: "s", menu_order: 0 },
48
- { name: "M", slug: "m", menu_order: 1 },
49
- { name: "L", slug: "l", menu_order: 2 },
50
- { name: "XL", slug: "xl", menu_order: 3 },
47
+ { name: "S", order: 0 },
48
+ { name: "M", order: 1 },
49
+ { name: "L", order: 2 },
50
+ { name: "XL", order: 3 },
51
51
  ],
52
52
  };
53
53
 
54
54
  /**
55
55
  * Per-colour variation photos, keyed by product: a colour swatch is only useful
56
56
  * if it shows the right *garment* in that colour, so these can't be shared
57
- * across product types. Products with no colour-accurate photo available (the
57
+ * between products. Products with no colour-accurate photo available (the
58
58
  * tote) are deliberately absent — their variations fall back to the product
59
59
  * gallery, which beats showing an unrelated item.
60
60
  */
@@ -79,15 +79,14 @@ const colorImage = (name: string, color: string) => {
79
79
  };
80
80
 
81
81
  /**
82
- * Product specs — category/attribute references are by slug and resolved to
83
- * ids at seed time. `variations` uses attribute slug→option pairs and may carry
84
- * a per-variation `image`.
82
+ * Product specs — categories are referenced by slug and attributes by `code`,
83
+ * both resolved to ids at seed time. `variations` uses attribute code→option
84
+ * pairs and may carry a per-variation `image`.
85
85
  */
86
86
  export const SAMPLE_PRODUCTS: any[] = [
87
87
  {
88
88
  key: "tshirt",
89
89
  name: "Classic T-Shirt",
90
- type: "simple",
91
90
  status: "publish",
92
91
  categories: ["clothing"],
93
92
  regular_price: 19.99,
@@ -105,7 +104,6 @@ export const SAMPLE_PRODUCTS: any[] = [
105
104
  {
106
105
  key: "hoodie",
107
106
  name: "Zip Hoodie",
108
- type: "simple",
109
107
  status: "publish",
110
108
  categories: ["clothing"],
111
109
  regular_price: 44.99,
@@ -123,7 +121,6 @@ export const SAMPLE_PRODUCTS: any[] = [
123
121
  {
124
122
  key: "cap",
125
123
  name: "Logo Cap",
126
- type: "simple",
127
124
  status: "publish",
128
125
  categories: ["accessories"],
129
126
  regular_price: 14.99,
@@ -140,7 +137,6 @@ export const SAMPLE_PRODUCTS: any[] = [
140
137
  {
141
138
  key: "stickers",
142
139
  name: "Sticker Pack",
143
- type: "simple",
144
140
  status: "publish",
145
141
  categories: ["accessories"],
146
142
  regular_price: 4.99,
@@ -156,7 +152,6 @@ export const SAMPLE_PRODUCTS: any[] = [
156
152
  {
157
153
  key: "album",
158
154
  name: "Demo Album (Digital Download)",
159
- type: "simple",
160
155
  status: "publish",
161
156
  categories: ["downloads"],
162
157
  regular_price: 9.99,
@@ -175,7 +170,6 @@ export const SAMPLE_PRODUCTS: any[] = [
175
170
  {
176
171
  key: "poster",
177
172
  name: "Art Print Poster",
178
- type: "simple",
179
173
  status: "publish",
180
174
  categories: ["accessories"],
181
175
  regular_price: 12.99,
@@ -191,7 +185,6 @@ export const SAMPLE_PRODUCTS: any[] = [
191
185
  {
192
186
  key: "vneck",
193
187
  name: "V-Neck T-Shirt",
194
- type: "variable",
195
188
  status: "publish",
196
189
  categories: ["clothing"],
197
190
  sku: "DEMO-VNECK",
@@ -201,10 +194,10 @@ export const SAMPLE_PRODUCTS: any[] = [
201
194
  "<p>A fitted v-neck tee in a lightweight jersey knit, available across multiple colors and sizes.</p>" +
202
195
  "<ul><li>Slim, tailored fit</li><li>Lightweight 150 gsm jersey</li><li>Six color/size combinations</li></ul>",
203
196
  attributes: [
204
- { slug: "color", visible: true, variation: true, options: ["Red", "Blue"] },
205
- { slug: "size", visible: true, variation: true, options: ["S", "M", "L"] },
197
+ { code: "color", options: ["Red", "Blue"] },
198
+ { code: "size", options: ["S", "M", "L"] },
206
199
  ],
207
- default_attributes: [{ slug: "color", option: "Red" }, { slug: "size", option: "M" }],
200
+ default_attributes: [{ code: "color", option: "Red" }, { code: "size", option: "M" }],
208
201
  variations: [
209
202
  { options: { color: "Red", size: "S" }, regular_price: 21.99, stock_quantity: 10, image: colorImage("V-Neck T-Shirt", "Red") },
210
203
  { options: { color: "Red", size: "M" }, regular_price: 21.99, stock_quantity: 10, image: colorImage("V-Neck T-Shirt", "Red") },
@@ -217,7 +210,6 @@ export const SAMPLE_PRODUCTS: any[] = [
217
210
  {
218
211
  key: "pullover",
219
212
  name: "Colorblock Pullover Hoodie",
220
- type: "variable",
221
213
  status: "publish",
222
214
  categories: ["clothing"],
223
215
  sku: "DEMO-PULLOVER",
@@ -227,10 +219,10 @@ export const SAMPLE_PRODUCTS: any[] = [
227
219
  "<p>A heavyweight brushed-back pullover hoodie with a double-layer hood and ribbed cuffs. Some combinations are on sale.</p>" +
228
220
  "<ul><li>380 gsm brushed-back fleece</li><li>Double-layer drawcord hood</li><li>Nine color/size combinations</li></ul>",
229
221
  attributes: [
230
- { slug: "color", visible: true, variation: true, options: ["Black", "Blue", "Green"] },
231
- { slug: "size", visible: true, variation: true, options: ["M", "L", "XL"] },
222
+ { code: "color", options: ["Black", "Blue", "Green"] },
223
+ { code: "size", options: ["M", "L", "XL"] },
232
224
  ],
233
- default_attributes: [{ slug: "color", option: "Black" }, { slug: "size", option: "L" }],
225
+ default_attributes: [{ code: "color", option: "Black" }, { code: "size", option: "L" }],
234
226
  variations: [
235
227
  { options: { color: "Black", size: "M" }, regular_price: 49.99, stock_quantity: 12, image: colorImage("Colorblock Pullover Hoodie", "Black") },
236
228
  { options: { color: "Black", size: "L" }, regular_price: 49.99, stock_quantity: 12, image: colorImage("Colorblock Pullover Hoodie", "Black") },
@@ -246,57 +238,27 @@ export const SAMPLE_PRODUCTS: any[] = [
246
238
  {
247
239
  key: "tote",
248
240
  name: "Canvas Tote Bag",
249
- type: "variable",
250
241
  status: "publish",
251
242
  categories: ["accessories"],
252
243
  sku: "DEMO-TOTE",
253
244
  images: gallery("Canvas Tote Bag", ["photo-1544816155-12df9643f363"]),
254
- short_description: "Sturdy canvas tote — a single-attribute variable product in three colors.",
245
+ short_description: "Sturdy canvas tote — one attribute, three colors.",
255
246
  description:
256
- "<p>A roomy, sturdy cotton-canvas tote with reinforced handles. Demonstrates a variable product driven by a single attribute (color).</p>" +
247
+ "<p>A roomy, sturdy cotton-canvas tote with reinforced handles. Demonstrates variants driven by a single attribute (color).</p>" +
257
248
  "<ul><li>12 oz heavyweight cotton canvas</li><li>Reinforced 28 cm handles</li><li>Interior slip pocket</li></ul>",
258
249
  attributes: [
259
- { slug: "color", visible: true, variation: true, options: ["Black", "Blue", "Green"] },
250
+ { code: "color", options: ["Black", "Blue", "Green"] },
260
251
  ],
261
- default_attributes: [{ slug: "color", option: "Black" }],
252
+ default_attributes: [{ code: "color", option: "Black" }],
262
253
  variations: [
263
254
  { options: { color: "Black" }, regular_price: 18.99, stock_quantity: 25, image: colorImage("Canvas Tote Bag", "Black") },
264
255
  { options: { color: "Blue" }, regular_price: 18.99, stock_quantity: 20, image: colorImage("Canvas Tote Bag", "Blue") },
265
256
  { options: { color: "Green" }, regular_price: 18.99, stock_quantity: 0, backorders: "notify", image: colorImage("Canvas Tote Bag", "Green") },
266
257
  ],
267
258
  },
268
- {
269
- key: "mug",
270
- name: "Partner Mug (External)",
271
- type: "external",
272
- status: "publish",
273
- categories: ["accessories"],
274
- regular_price: 11.99,
275
- sku: "DEMO-MUG",
276
- external_url: "https://example.com/partner/mug",
277
- button_text: "Buy at partner store",
278
- images: gallery("Partner Mug", ["photo-1514228742587-6b1558fcca3d", "photo-1516390118834-21602d501886"]),
279
- short_description: "Sold on our partner's site — an external/affiliate product.",
280
- description:
281
- "<p>A 350 ml ceramic mug sold through our partner store. An example of an external/affiliate product whose buy button links off-site.</p>" +
282
- "<ul><li>350 ml ceramic body</li><li>Dishwasher &amp; microwave safe</li><li>Fulfilled by our partner</li></ul>",
283
- },
284
- {
285
- key: "bundle",
286
- name: "Starter Bundle",
287
- type: "grouped",
288
- status: "publish",
289
- categories: ["clothing", "accessories"],
290
- grouped_keys: ["tshirt", "cap", "stickers"], // resolved to grouped_products ids at seed time
291
- images: gallery("Starter Bundle", ["photo-1479064555552-3ef4979f8908"]),
292
- short_description: "Tee, cap and sticker pack together in one grouped listing.",
293
- description:
294
- "<p>A grouped product that bundles the Classic T-Shirt, Logo Cap and Sticker Pack into a single listing. Customers pick quantities for each item.</p>",
295
- },
296
259
  {
297
260
  key: "beanie",
298
261
  name: "Wool Beanie",
299
- type: "simple",
300
262
  status: "publish",
301
263
  categories: ["accessories"],
302
264
  regular_price: 16.99,