@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.
- package/README.md +11 -11
- package/base44/agents/commerce/StoreAdmin.jsonc +7 -7
- package/base44/entities/commerce.Order.jsonc +1 -1
- package/base44/entities/commerce.PaymentGateway.jsonc +3 -3
- package/base44/entities/commerce.Product.jsonc +9 -45
- package/base44/entities/commerce.ProductAttribute.jsonc +7 -19
- package/base44/entities/commerce.ProductAttributeTerm.jsonc +4 -10
- package/base44/entities/commerce.ProductTag.jsonc +1 -8
- package/base44/entities/commerce.ProductVariation.jsonc +1 -1
- package/base44/entities/commerce.StoreSettings.jsonc +1 -1
- package/base44/functions/commerce/admin-products/entry.ts +73 -121
- package/base44/functions/commerce/admin-refunds/entry.ts +1 -1
- package/base44/functions/commerce/admin-reports/entry.ts +1 -1
- package/base44/functions/commerce/admin-tools/entry.ts +4 -3
- package/base44/functions/commerce/payments/entry.ts +0 -2
- package/base44/functions/commerce/seed-store/defaults.ts +13 -39
- package/base44/functions/commerce/seed-store/entry.ts +50 -166
- package/base44/functions/commerce/seed-store/sample-data.ts +28 -66
- package/base44/functions/commerce/seed-store/seed-catalog.ts +717 -0
- package/base44/functions/commerce/storefront-cart/entry.ts +0 -7
- package/base44/functions/commerce/storefront-catalog/entry.ts +12 -21
- package/base44/functions/commerce/storefront-checkout/entry.ts +5 -12
- package/base44/shared/commerce/catalog.ts +134 -0
- package/base44/shared/commerce/coupons.ts +4 -7
- package/base44/shared/commerce/email-templates.ts +15 -13
- package/base44/shared/commerce/emails.ts +4 -3
- package/base44/shared/commerce/payments.ts +8 -20
- package/base44/shared/commerce/products.ts +24 -0
- package/base44/shared/commerce/settings.ts +2 -3
- package/base44/shared/commerce/stock.ts +2 -4
- package/base44/shared/commerce/totals.ts +37 -42
- package/package.json +1 -1
- package/scripts/install.js +23 -7
- package/skills/commerce/SKILL.md +9 -7
- package/skills/commerce/docs/api-admin.md +20 -14
- package/skills/commerce/docs/api-storefront.md +39 -35
- package/skills/commerce/installation-guidelines.md +8 -7
- package/skills/commerce/post-installation.md +205 -84
- package/skills/commerce/references/admin-product-form.md +56 -0
- package/skills/commerce/references/emails.md +4 -3
- package/skills/commerce/references/guest-access-security.md +2 -2
- package/skills/commerce/references/limits-and-performance.md +1 -1
- package/skills/commerce/references/online-payments.md +8 -8
- package/skills/commerce/references/product-render.md +25 -23
- package/skills/commerce/references/reviews.md +20 -0
- package/skills/commerce/references/storefront-product-page.md +9 -9
- package/skills/commerce/references/webhooks.md +3 -1
- package/src/commerce/admin/README.md +4 -4
- package/src/commerce/admin/context/BasePathContext.jsx +5 -5
- package/src/commerce/admin/context/SettingsContext.jsx +4 -4
- package/src/commerce/admin/index.jsx +3 -3
- package/src/commerce/admin/layout/Sidebar.jsx +1 -9
- package/src/commerce/admin/lib/constants.js +0 -7
- package/src/commerce/admin/lib/paths.js +6 -6
- package/src/commerce/admin/lib/product-utils.js +18 -15
- package/src/commerce/admin/pages/orders/components/AddProductDialog.jsx +8 -8
- package/src/commerce/admin/pages/products/ProductEditor.jsx +10 -12
- package/src/commerce/admin/pages/products/ProductsList.jsx +3 -15
- package/src/commerce/admin/pages/products/components/AttributesSection.jsx +426 -0
- package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +33 -69
- package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +1 -1
- package/src/commerce/admin/pages/products/components/tabs/LinkedTab.jsx +1 -14
- package/src/commerce/admin/pages/products/components/tabs/ModifiersTab.jsx +19 -0
- package/src/commerce/admin/pages/products/components/tabs/PriceInventoryTab.jsx +728 -0
- package/src/commerce/admin/pages/reports/Reports.jsx +3 -3
- package/src/commerce/admin/pages/settings/EmailsSettings.jsx +12 -14
- package/src/commerce/admin/pages/settings/GeneralSettings.jsx +6 -116
- package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +101 -62
- package/src/commerce/admin/pages/settings/SettingsLayout.jsx +2 -6
- package/src/commerce/admin/pages/settings/TaxSettings.jsx +0 -1
- package/src/commerce/admin/pages/status/WebhookEditor.jsx +3 -3
- package/src/commerce/admin/pages/status/Webhooks.jsx +2 -2
- package/src/commerce/admin/routes.jsx +10 -19
- package/src/commerce/utils/index.js +2 -2
- package/src/commerce/utils/shipping-promos.js +9 -6
- package/src/commerce/utils/variants.js +21 -21
- package/src/commerce/admin/pages/products/AttributeTerms.jsx +0 -180
- package/src/commerce/admin/pages/products/Attributes.jsx +0 -183
- package/src/commerce/admin/pages/products/Tags.jsx +0 -150
- package/src/commerce/admin/pages/products/components/tabs/AdvancedTab.jsx +0 -48
- package/src/commerce/admin/pages/products/components/tabs/AttributesTab.jsx +0 -208
- package/src/commerce/admin/pages/products/components/tabs/ExternalTab.jsx +0 -41
- package/src/commerce/admin/pages/products/components/tabs/GeneralTab.jsx +0 -103
- package/src/commerce/admin/pages/products/components/tabs/InventoryTab.jsx +0 -93
- package/src/commerce/admin/pages/products/components/tabs/ShippingTab.jsx +0 -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)
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
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("
|
|
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 === "
|
|
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("
|
|
81
|
-
const
|
|
82
|
-
const current = String(
|
|
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(
|
|
85
|
-
values: { ...(
|
|
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.
|
|
119
|
-
let sampleResult: Record<string,
|
|
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
|
|
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 === "
|
|
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(
|
|
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
|
|
165
|
+
const catalog = [
|
|
154
166
|
{ entity: "commerce.ProductCategory", record: { name: "__canary", slug: "__canary", count: 0 } },
|
|
155
|
-
{ entity: "commerce.
|
|
156
|
-
{ entity: "commerce.
|
|
157
|
-
{ entity: "commerce.
|
|
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
|
|
175
|
+
return needsCatalog ? [...base, ...catalog] : base;
|
|
163
176
|
}
|
|
164
177
|
|
|
165
|
-
async function runCanaries(sr: any,
|
|
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(
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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",
|
|
36
|
-
{ name: "Size",
|
|
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;
|
|
39
|
+
export const SAMPLE_ATTRIBUTE_TERMS: Record<string, Array<{ name: string; order: number }>> = {
|
|
40
40
|
color: [
|
|
41
|
-
{ name: "Red",
|
|
42
|
-
{ name: "Blue",
|
|
43
|
-
{ name: "Black",
|
|
44
|
-
{ name: "Green",
|
|
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",
|
|
48
|
-
{ name: "M",
|
|
49
|
-
{ name: "L",
|
|
50
|
-
{ name: "XL",
|
|
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
|
-
*
|
|
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 —
|
|
83
|
-
* ids at seed time. `variations` uses attribute
|
|
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
|
-
{
|
|
205
|
-
{
|
|
197
|
+
{ code: "color", options: ["Red", "Blue"] },
|
|
198
|
+
{ code: "size", options: ["S", "M", "L"] },
|
|
206
199
|
],
|
|
207
|
-
default_attributes: [{
|
|
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
|
-
{
|
|
231
|
-
{
|
|
222
|
+
{ code: "color", options: ["Black", "Blue", "Green"] },
|
|
223
|
+
{ code: "size", options: ["M", "L", "XL"] },
|
|
232
224
|
],
|
|
233
|
-
default_attributes: [{
|
|
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 —
|
|
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
|
|
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
|
-
{
|
|
250
|
+
{ code: "color", options: ["Black", "Blue", "Green"] },
|
|
260
251
|
],
|
|
261
|
-
default_attributes: [{
|
|
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 & 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,
|