@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
|
@@ -10,10 +10,21 @@ import { createClientFromRequest } from "npm:@base44/sdk";
|
|
|
10
10
|
import { HttpError, requireAdmin } from "../../../shared/commerce/auth.ts";
|
|
11
11
|
import { getSettings } from "../../../shared/commerce/settings.ts";
|
|
12
12
|
import { deriveStockStatus } from "../../../shared/commerce/stock.ts";
|
|
13
|
+
import { isVariable } from "../../../shared/commerce/products.ts";
|
|
13
14
|
import { sendStockEmail } from "../../../shared/commerce/emails.ts";
|
|
14
15
|
import { dispatch } from "../../../shared/commerce/webhooks.ts";
|
|
15
16
|
import { pageSlice, scanAll, textMatch } from "../../../shared/commerce/scan.ts";
|
|
16
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
adjustTermCounts,
|
|
19
|
+
assertUniqueSku,
|
|
20
|
+
bumpCount,
|
|
21
|
+
derivePricing,
|
|
22
|
+
deriveStock,
|
|
23
|
+
ensureUniqueCode,
|
|
24
|
+
ensureUniqueSlug,
|
|
25
|
+
rollUpParent,
|
|
26
|
+
slugify,
|
|
27
|
+
} from "../../../shared/commerce/catalog.ts";
|
|
17
28
|
|
|
18
29
|
const ok = (data: unknown, status = 200) => Response.json({ success: true, data }, { status });
|
|
19
30
|
const fail = (status: number, error: string, code?: string) =>
|
|
@@ -55,87 +66,6 @@ Deno.serve(async (req) => {
|
|
|
55
66
|
}
|
|
56
67
|
});
|
|
57
68
|
|
|
58
|
-
// ── derivations ──────────────────────────────────────────────────────────────
|
|
59
|
-
|
|
60
|
-
function slugify(name: string): string {
|
|
61
|
-
return String(name || "")
|
|
62
|
-
.toLowerCase()
|
|
63
|
-
.trim()
|
|
64
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
65
|
-
.replace(/^-+|-+$/g, "") || "product";
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** price/on_sale from the sale window. Mutates rec. */
|
|
69
|
-
function derivePricing(rec: any): void {
|
|
70
|
-
const now = Date.now();
|
|
71
|
-
const from = rec.date_on_sale_from ? new Date(rec.date_on_sale_from).getTime() : -Infinity;
|
|
72
|
-
const to = rec.date_on_sale_to ? new Date(rec.date_on_sale_to).getTime() : Infinity;
|
|
73
|
-
const saleActive = rec.sale_price != null && rec.sale_price !== "" && now >= from && now <= to;
|
|
74
|
-
rec.on_sale = !!saleActive;
|
|
75
|
-
const effective = saleActive ? rec.sale_price : rec.regular_price;
|
|
76
|
-
if (effective != null && effective !== "") rec.price = round2(Number(effective));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** stock_status derivation when stock is managed. Mutates rec. */
|
|
80
|
-
function deriveStock(rec: any, outThreshold: number, managed: boolean): void {
|
|
81
|
-
if (managed) {
|
|
82
|
-
rec.stock_status = deriveStockStatus(
|
|
83
|
-
Number(rec.stock_quantity ?? 0),
|
|
84
|
-
rec.backorders ?? "no",
|
|
85
|
-
outThreshold,
|
|
86
|
-
);
|
|
87
|
-
} else if (!rec.stock_status) {
|
|
88
|
-
rec.stock_status = "instock";
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ── uniqueness ───────────────────────────────────────────────────────────────
|
|
93
|
-
|
|
94
|
-
async function ensureUniqueSlug(
|
|
95
|
-
sr: any,
|
|
96
|
-
slug: string,
|
|
97
|
-
selfId?: string,
|
|
98
|
-
entity = "commerce.Product",
|
|
99
|
-
scope?: Record<string, any>,
|
|
100
|
-
): Promise<string> {
|
|
101
|
-
let candidate = slug;
|
|
102
|
-
for (let i = 2; i < 100; i++) {
|
|
103
|
-
const hits = (await sr.entities[entity].filter({ ...(scope ?? {}), slug: candidate }, undefined, 2)) ?? [];
|
|
104
|
-
if (!hits.some((p: any) => p.id !== selfId)) return candidate;
|
|
105
|
-
candidate = `${slug}-${i}`;
|
|
106
|
-
}
|
|
107
|
-
return `${slug}-${crypto.randomUUID().slice(0, 6)}`;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** SKU must be unique across products AND variations. Throws duplicate_sku. */
|
|
111
|
-
async function assertUniqueSku(sr: any, sku: string, opts: { productId?: string; variationId?: string }): Promise<void> {
|
|
112
|
-
if (!sku) return;
|
|
113
|
-
const prods = (await sr.entities["commerce.Product"].filter({ sku }, undefined, 2)) ?? [];
|
|
114
|
-
if (prods.some((p: any) => p.id !== opts.productId)) {
|
|
115
|
-
throw new HttpError(409, `SKU "${sku}" is already in use by another product.`, "duplicate_sku");
|
|
116
|
-
}
|
|
117
|
-
const vars = (await sr.entities["commerce.ProductVariation"].filter({ sku }, undefined, 2)) ?? [];
|
|
118
|
-
if (vars.some((v: any) => v.id !== opts.variationId)) {
|
|
119
|
-
throw new HttpError(409, `SKU "${sku}" is already in use by a product variation.`, "duplicate_sku");
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// ── taxonomy counts ──────────────────────────────────────────────────────────
|
|
124
|
-
|
|
125
|
-
async function adjustTermCounts(sr: any, entity: string, prevIds: string[], nextIds: string[]): Promise<void> {
|
|
126
|
-
const added = nextIds.filter((id) => !prevIds.includes(id));
|
|
127
|
-
const removed = prevIds.filter((id) => !nextIds.includes(id));
|
|
128
|
-
for (const id of added) await bumpCount(sr, entity, id, +1);
|
|
129
|
-
for (const id of removed) await bumpCount(sr, entity, id, -1);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async function bumpCount(sr: any, entity: string, id: string, delta: number): Promise<void> {
|
|
133
|
-
try {
|
|
134
|
-
const rec = await sr.entities[entity].get(id);
|
|
135
|
-
if (rec) await sr.entities[entity].update(id, { count: Math.max(0, (rec.count ?? 0) + delta) });
|
|
136
|
-
} catch { /* stale reference — recount-terms repairs */ }
|
|
137
|
-
}
|
|
138
|
-
|
|
139
69
|
// ── save ─────────────────────────────────────────────────────────────────────
|
|
140
70
|
|
|
141
71
|
async function save(sr: any, payload: any): Promise<any> {
|
|
@@ -155,7 +85,7 @@ async function save(sr: any, payload: any): Promise<any> {
|
|
|
155
85
|
rec.slug = await ensureUniqueSlug(sr, slugify(rec.slug), rec.id);
|
|
156
86
|
await assertUniqueSku(sr, rec.sku, { productId: rec.id });
|
|
157
87
|
derivePricing(rec);
|
|
158
|
-
deriveStock(rec, outThreshold, !!rec.manage_stock
|
|
88
|
+
deriveStock(rec, outThreshold, !!rec.manage_stock);
|
|
159
89
|
|
|
160
90
|
const { id: _id, created_date: _cd, updated_date: _ud, created_by: _cb, ...fields } = rec;
|
|
161
91
|
let saved: any;
|
|
@@ -177,10 +107,10 @@ async function save(sr: any, payload: any): Promise<any> {
|
|
|
177
107
|
let savedVariations: any[] | undefined;
|
|
178
108
|
if (Array.isArray(variations)) {
|
|
179
109
|
savedVariations = await diffVariations(sr, saved, variations, outThreshold);
|
|
180
|
-
await
|
|
181
|
-
} else if (saved
|
|
110
|
+
await rollUpParent(sr, saved, savedVariations);
|
|
111
|
+
} else if (isVariable(saved)) {
|
|
182
112
|
const existing = await scanAll(sr.entities["commerce.ProductVariation"], { product_id: saved.id });
|
|
183
|
-
await
|
|
113
|
+
await rollUpParent(sr, saved, existing);
|
|
184
114
|
}
|
|
185
115
|
|
|
186
116
|
await dispatch(sr, prev ? "product.updated" : "product.created", saved);
|
|
@@ -213,19 +143,6 @@ async function diffVariations(sr: any, parent: any, incoming: any[], outThreshol
|
|
|
213
143
|
return out;
|
|
214
144
|
}
|
|
215
145
|
|
|
216
|
-
/** Variable parent shows outofstock only when every purchasable variation is out. */
|
|
217
|
-
async function rollUpParentStock(sr: any, parent: any, variations: any[]): Promise<void> {
|
|
218
|
-
if (parent.type !== "variable" || parent.manage_stock) return;
|
|
219
|
-
const live = variations.filter((v) => (v.status ?? "publish") === "publish");
|
|
220
|
-
const status = live.length && live.every((v) => v.stock_status === "outofstock")
|
|
221
|
-
? "outofstock"
|
|
222
|
-
: "instock";
|
|
223
|
-
if (status !== parent.stock_status) {
|
|
224
|
-
await sr.entities["commerce.Product"].update(parent.id, { stock_status: status });
|
|
225
|
-
parent.stock_status = status;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
146
|
// ── delete / batch / duplicate ───────────────────────────────────────────────
|
|
230
147
|
|
|
231
148
|
async function remove(sr: any, id: string): Promise<any> {
|
|
@@ -355,10 +272,9 @@ async function categoryWithDescendants(sr: any, rootId: string): Promise<Set<str
|
|
|
355
272
|
}
|
|
356
273
|
|
|
357
274
|
async function search(sr: any, payload: any): Promise<any> {
|
|
358
|
-
const { q, category_id,
|
|
275
|
+
const { q, category_id, stock_status, status, sort = "-created_date", limit = 20, skip = 0 } = payload;
|
|
359
276
|
|
|
360
277
|
const query: Record<string, any> = {};
|
|
361
|
-
if (type) query.type = type;
|
|
362
278
|
if (status) query.status = status;
|
|
363
279
|
if (stock_status) query.stock_status = stock_status;
|
|
364
280
|
|
|
@@ -393,10 +309,10 @@ function taxonomyEntity(taxonomy: string): string {
|
|
|
393
309
|
}
|
|
394
310
|
|
|
395
311
|
/**
|
|
396
|
-
* Upsert a category, tag, attribute or attribute
|
|
312
|
+
* Upsert a category, tag, attribute or attribute value. Exists so an agent (or
|
|
397
313
|
* any API caller) can create these — referencing one from a product is useless
|
|
398
314
|
* while the record itself can only be made in the admin UI. An attribute plus
|
|
399
|
-
* its
|
|
315
|
+
* its values is what a product's `attributes[].options` draws on.
|
|
400
316
|
*/
|
|
401
317
|
async function saveTerm(sr: any, payload: any): Promise<any> {
|
|
402
318
|
const entity = taxonomyEntity(payload.taxonomy);
|
|
@@ -407,30 +323,29 @@ async function saveTerm(sr: any, payload: any): Promise<any> {
|
|
|
407
323
|
const fields: Record<string, any> = { name };
|
|
408
324
|
|
|
409
325
|
if (entity === "commerce.ProductAttributeTerm") {
|
|
410
|
-
// A
|
|
411
|
-
// to be unique inside that attribute.
|
|
326
|
+
// A value without its attribute is unreachable.
|
|
412
327
|
const attributeId = String(term.attribute_id ?? "").trim();
|
|
413
|
-
if (!attributeId) throw new HttpError(400, "term.attribute_id is required for an attribute
|
|
328
|
+
if (!attributeId) throw new HttpError(400, "term.attribute_id is required for an attribute value", "invalid_payload");
|
|
414
329
|
if (!(await sr.entities["commerce.ProductAttribute"].get(attributeId))) {
|
|
415
330
|
throw new HttpError(404, `Attribute ${attributeId} not found`, "not_found");
|
|
416
331
|
}
|
|
417
332
|
fields.attribute_id = attributeId;
|
|
418
|
-
fields.
|
|
419
|
-
fields.menu_order = Number(term.menu_order ?? 0) || 0;
|
|
420
|
-
fields.slug = await ensureUniqueSlug(sr, slugify(term.slug || name), term.id, entity, { attribute_id: attributeId });
|
|
333
|
+
fields.order = Number(term.order ?? 0) || 0;
|
|
421
334
|
} else if (entity === "commerce.ProductAttribute") {
|
|
422
|
-
fields.
|
|
423
|
-
fields.
|
|
424
|
-
|
|
425
|
-
fields.slug = await ensureUniqueSlug(sr, slugify(term.slug || name), term.id, entity);
|
|
426
|
-
} else {
|
|
335
|
+
fields.code = await ensureUniqueCode(sr, slugify(term.code || name), term.id);
|
|
336
|
+
fields.order = Number(term.order ?? 0) || 0;
|
|
337
|
+
} else if (entity === "commerce.ProductCategory") {
|
|
427
338
|
fields.description = term.description ?? "";
|
|
428
339
|
fields.slug = await ensureUniqueSlug(sr, slugify(term.slug || name), term.id, entity);
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
340
|
+
fields.parent_id = term.parent_id ?? "";
|
|
341
|
+
if (term.image !== undefined) fields.image = term.image;
|
|
342
|
+
if (term.menu_order !== undefined) fields.menu_order = Number(term.menu_order) || 0;
|
|
343
|
+
} else if (!term.id) {
|
|
344
|
+
// Tags are created by typing a name on the product form, so "create" has to
|
|
345
|
+
// mean get-or-create: a second Best Seller would split the tag in two.
|
|
346
|
+
const existing = (await scanAll(sr.entities[entity], null, "name"))
|
|
347
|
+
.find((t: any) => String(t.name ?? "").toLowerCase() === name.toLowerCase());
|
|
348
|
+
if (existing) return existing;
|
|
434
349
|
}
|
|
435
350
|
|
|
436
351
|
if (term.id) {
|
|
@@ -439,12 +354,47 @@ async function saveTerm(sr: any, payload: any): Promise<any> {
|
|
|
439
354
|
// parent_id === own id would make the tree unwalkable (categoryWithDescendants loops on it).
|
|
440
355
|
if (fields.parent_id === term.id) fields.parent_id = "";
|
|
441
356
|
await sr.entities[entity].update(term.id, fields);
|
|
357
|
+
// Products store an attribute value by *name*, so a rename that stops here
|
|
358
|
+
// would leave every product and variant pointing at a value that is gone.
|
|
359
|
+
if (entity === "commerce.ProductAttributeTerm" && existing.name !== fields.name) {
|
|
360
|
+
await renameAttributeValue(sr, fields.attribute_id, existing.name, fields.name);
|
|
361
|
+
}
|
|
442
362
|
return { ...existing, ...fields, id: term.id };
|
|
443
363
|
}
|
|
444
364
|
const withCount = entity === "commerce.ProductAttribute" ? fields : { ...fields, count: 0 };
|
|
445
365
|
return await sr.entities[entity].create(withCount);
|
|
446
366
|
}
|
|
447
367
|
|
|
368
|
+
/** Rewrite a renamed value across every product and variant that used it. */
|
|
369
|
+
async function renameAttributeValue(sr: any, attributeId: string, from: string, to: string): Promise<void> {
|
|
370
|
+
const products = await scanAll(sr.entities["commerce.Product"]);
|
|
371
|
+
for (const product of products) {
|
|
372
|
+
const rows = product.attributes ?? [];
|
|
373
|
+
if (!rows.some((a: any) => a.attribute_id === attributeId && (a.options ?? []).includes(from))) continue;
|
|
374
|
+
|
|
375
|
+
await sr.entities["commerce.Product"].update(product.id, {
|
|
376
|
+
attributes: rows.map((a: any) =>
|
|
377
|
+
a.attribute_id === attributeId
|
|
378
|
+
? { ...a, options: (a.options ?? []).map((o: string) => (o === from ? to : o)) }
|
|
379
|
+
: a
|
|
380
|
+
),
|
|
381
|
+
default_attributes: (product.default_attributes ?? []).map((d: any) =>
|
|
382
|
+
d.attribute_id === attributeId && d.option === from ? { ...d, option: to } : d
|
|
383
|
+
),
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const variations = await scanAll(sr.entities["commerce.ProductVariation"], { product_id: product.id });
|
|
387
|
+
for (const v of variations) {
|
|
388
|
+
if (!(v.attributes ?? []).some((a: any) => a.attribute_id === attributeId && a.option === from)) continue;
|
|
389
|
+
await sr.entities["commerce.ProductVariation"].update(v.id, {
|
|
390
|
+
attributes: v.attributes.map((a: any) =>
|
|
391
|
+
a.attribute_id === attributeId && a.option === from ? { ...a, option: to } : a
|
|
392
|
+
),
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
448
398
|
/**
|
|
449
399
|
* Delete a term. For a category or tag, products keep the id in
|
|
450
400
|
* category_ids/tag_ids — same as the admin UI, and the storefront skips ids that
|
|
@@ -494,13 +444,15 @@ async function deleteTerm(sr: any, payload: any): Promise<any> {
|
|
|
494
444
|
/** Terms for the taxonomy, so a caller can reuse an existing one before creating a duplicate. */
|
|
495
445
|
async function listTerms(sr: any, payload: any): Promise<any> {
|
|
496
446
|
const entity = taxonomyEntity(payload.taxonomy);
|
|
497
|
-
const sort = entity === "commerce.ProductCategory"
|
|
447
|
+
const sort = entity === "commerce.ProductCategory"
|
|
498
448
|
? "menu_order"
|
|
449
|
+
: entity === "commerce.ProductAttributeTerm" || entity === "commerce.ProductAttribute"
|
|
450
|
+
? "order"
|
|
499
451
|
: "name";
|
|
500
452
|
const query = entity === "commerce.ProductAttributeTerm" && payload.attribute_id
|
|
501
453
|
? { attribute_id: String(payload.attribute_id) }
|
|
502
454
|
: null;
|
|
503
455
|
let rows = await scanAll(sr.entities[entity], query, sort);
|
|
504
|
-
if (payload.q) rows = rows.filter((t: any) => textMatch(t.name, payload.q) || textMatch(t.
|
|
456
|
+
if (payload.q) rows = rows.filter((t: any) => textMatch(t.name, payload.q) || textMatch(t.code, payload.q));
|
|
505
457
|
return pageSlice(rows, Number(payload.limit ?? 100), Number(payload.skip ?? 0));
|
|
506
458
|
}
|
|
@@ -30,7 +30,7 @@ Deno.serve(async (req) => {
|
|
|
30
30
|
case "top-sellers": return ok(await topSellers(sr, payload));
|
|
31
31
|
case "stock": return ok(await stockReport(sr));
|
|
32
32
|
case "orders-totals": return ok(await ordersTotals(sr));
|
|
33
|
-
case "products-totals": return ok(await groupCount(sr.entities["commerce.Product"], "
|
|
33
|
+
case "products-totals": return ok(await groupCount(sr.entities["commerce.Product"], "status"));
|
|
34
34
|
case "customers-totals": return ok(await customersTotals(sr));
|
|
35
35
|
case "coupons-totals": return ok(await groupCount(sr.entities["commerce.Coupon"], "discount_type"));
|
|
36
36
|
case "reviews-totals": return ok(await groupCount(sr.entities["commerce.ProductReview"], "status"));
|
|
@@ -116,9 +116,10 @@ async function status(sr: any): Promise<any> {
|
|
|
116
116
|
settings_groups: groups,
|
|
117
117
|
counts,
|
|
118
118
|
checks: {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
// counts is keyed by the namespaced entity name — `counts.PaymentGateway`
|
|
120
|
+
// is undefined, which silently reported both checks as false.
|
|
121
|
+
has_payment_gateways: (counts["commerce.PaymentGateway"] as number) > 0,
|
|
122
|
+
has_default_zone: (counts["commerce.ShippingZone"] as number) > 0,
|
|
122
123
|
},
|
|
123
124
|
};
|
|
124
125
|
}
|
|
@@ -120,7 +120,6 @@ Deno.serve(async (req: Request) => {
|
|
|
120
120
|
successUrl: payload.success_url,
|
|
121
121
|
cancelUrl: payload.cancel_url,
|
|
122
122
|
returnUrl: payload.return_url,
|
|
123
|
-
storeUrl: settings.general?.store_url,
|
|
124
123
|
returnPath: settings.general?.order_received_path,
|
|
125
124
|
});
|
|
126
125
|
const link = await startOnlinePayment(sr, order, {
|
|
@@ -164,7 +163,6 @@ Deno.serve(async (req: Request) => {
|
|
|
164
163
|
successUrl: payload.success_url,
|
|
165
164
|
cancelUrl: payload.cancel_url,
|
|
166
165
|
returnUrl: payload.return_url,
|
|
167
|
-
storeUrl: settings.general?.store_url,
|
|
168
166
|
returnPath: settings.general?.order_received_path,
|
|
169
167
|
});
|
|
170
168
|
const link = await startOnlinePayment(sr, result.order, {
|
|
@@ -24,17 +24,8 @@ export const SETTINGS_DEFAULTS: Array<{ group_id: string; values: Record<string,
|
|
|
24
24
|
{
|
|
25
25
|
group_id: "general",
|
|
26
26
|
values: {
|
|
27
|
-
store_name: "", // seed-store payload supplies it (the app/store name)
|
|
28
|
-
// Public URL of the shopfront (e.g. https://shop.example.com). Used to
|
|
29
|
-
// build the page a customer returns to after paying; when empty the
|
|
30
|
-
// origin of the calling app is used instead.
|
|
31
|
-
store_url: "",
|
|
32
27
|
// Storefront route customers return to after paying; a mismatch is a 404.
|
|
33
28
|
order_received_path: "/order-received",
|
|
34
|
-
address: { address_1: "", address_2: "", city: "", state: "", postcode: "", country: "US" },
|
|
35
|
-
enable_taxes: true,
|
|
36
|
-
enable_coupons: true,
|
|
37
|
-
calc_discounts_sequentially: false,
|
|
38
29
|
currency: "USD",
|
|
39
30
|
currency_position: "left", // left | right | left_space | right_space
|
|
40
31
|
thousand_sep: ",",
|
|
@@ -70,7 +61,7 @@ export const SETTINGS_DEFAULTS: Array<{ group_id: string; values: Record<string,
|
|
|
70
61
|
group_id: "tax",
|
|
71
62
|
values: {
|
|
72
63
|
prices_include_tax: false,
|
|
73
|
-
tax_based_on: "shipping", // shipping | billing
|
|
64
|
+
tax_based_on: "shipping", // shipping | billing
|
|
74
65
|
shipping_tax_class: "inherit", // inherit | standard | <class slug>
|
|
75
66
|
display_prices_shop: "excl", // incl | excl
|
|
76
67
|
display_prices_cart: "excl",
|
|
@@ -85,7 +76,9 @@ export const SETTINGS_DEFAULTS: Array<{ group_id: string; values: Record<string,
|
|
|
85
76
|
{
|
|
86
77
|
group_id: "emails",
|
|
87
78
|
values: {
|
|
88
|
-
|
|
79
|
+
// Subject line and sender name both. seed-store's payload supplies it;
|
|
80
|
+
// blank means SendEmail gets no from_name and Base44 sends as the app's name.
|
|
81
|
+
store_name: "",
|
|
89
82
|
admin_recipients: [],
|
|
90
83
|
...EMAIL_TYPE_DEFAULTS,
|
|
91
84
|
},
|
|
@@ -94,35 +87,16 @@ export const SETTINGS_DEFAULTS: Array<{ group_id: string; values: Record<string,
|
|
|
94
87
|
|
|
95
88
|
export const GATEWAY_DEFAULTS = [
|
|
96
89
|
{
|
|
97
|
-
slug: "
|
|
98
|
-
title: "
|
|
99
|
-
description: "
|
|
90
|
+
slug: "offline",
|
|
91
|
+
title: "Offline payment",
|
|
92
|
+
description: "Pay outside the store — bank transfer on the details below, or cash on delivery. Please quote your order number.",
|
|
100
93
|
enabled: true,
|
|
101
94
|
order: 0,
|
|
102
|
-
method_title: "
|
|
103
|
-
method_description:
|
|
95
|
+
method_title: "Offline payment",
|
|
96
|
+
method_description:
|
|
97
|
+
"Any payment settled outside the store: bank transfer, cash on delivery, or whatever you arrange with the customer. Orders are set on-hold until you confirm the money arrived.",
|
|
104
98
|
settings: { account_details: [] as any[] },
|
|
105
99
|
},
|
|
106
|
-
{
|
|
107
|
-
slug: "cheque",
|
|
108
|
-
title: "Check payments",
|
|
109
|
-
description: "Please send a check to our store address.",
|
|
110
|
-
enabled: false,
|
|
111
|
-
order: 1,
|
|
112
|
-
method_title: "Check payments",
|
|
113
|
-
method_description: "Take payments by check. Orders are set on-hold until payment clears.",
|
|
114
|
-
settings: {},
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
slug: "cod",
|
|
118
|
-
title: "Cash on delivery",
|
|
119
|
-
description: "Pay with cash upon delivery.",
|
|
120
|
-
enabled: true,
|
|
121
|
-
order: 2,
|
|
122
|
-
method_title: "Cash on delivery",
|
|
123
|
-
method_description: "Have your customers pay with cash (or by other means) upon delivery.",
|
|
124
|
-
settings: { enable_for_methods: [] as string[], enable_for_virtual: false },
|
|
125
|
-
},
|
|
126
100
|
{
|
|
127
101
|
slug: "stripe",
|
|
128
102
|
title: "Credit card",
|
|
@@ -132,10 +106,10 @@ export const GATEWAY_DEFAULTS = [
|
|
|
132
106
|
// storefront gateway list and the admin both check that live — so enabling it
|
|
133
107
|
// here cannot strand a shopper on an unusable option.
|
|
134
108
|
enabled: true,
|
|
135
|
-
order:
|
|
136
|
-
method_title: "Online card payments",
|
|
109
|
+
order: 1,
|
|
110
|
+
method_title: "Online card payments (Stripe by default)",
|
|
137
111
|
method_description:
|
|
138
|
-
"Card payments through the connected payment provider (Stripe out of the box).
|
|
112
|
+
"Card payments through the connected payment provider (Stripe out of the box). Set the provider up in the app's integrations to start taking payments; customers only see this option once it is connected.",
|
|
139
113
|
settings: { connector: "stripe" },
|
|
140
114
|
},
|
|
141
115
|
];
|