@azlib/cms 0.2.0 → 0.4.0

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/dist/index.mjs CHANGED
@@ -229,6 +229,7 @@ function normalizeConfig(config) {
229
229
  },
230
230
  collections,
231
231
  taxonomies: config.taxonomies ?? [],
232
+ plugins: config.plugins ?? [],
232
233
  admin: {
233
234
  route: config.admin?.route ?? "/admin",
234
235
  enableRegistration: config.admin?.enableRegistration ?? false,
@@ -1245,6 +1246,9 @@ var CMSEngine = class {
1245
1246
  revisions;
1246
1247
  lifecycle;
1247
1248
  collections = /* @__PURE__ */ new Map();
1249
+ plugins = /* @__PURE__ */ new Map();
1250
+ customRoutes = /* @__PURE__ */ new Map();
1251
+ pendingPluginSetups = [];
1248
1252
  constructor(config = {}, storage, hooks) {
1249
1253
  this.config = normalizeConfig(config);
1250
1254
  this.hooks = hooks ?? new HooksManager();
@@ -1257,21 +1261,114 @@ var CMSEngine = class {
1257
1261
  this.lifecycle = new ContentLifecycle(this.storage, this.hooks);
1258
1262
  for (const coll of this.config.collections) this.collections.set(coll.slug, coll);
1259
1263
  if (this.config.taxonomies) for (const tax of this.config.taxonomies) this.taxonomies.registerTaxonomy(tax);
1264
+ if (this.config.plugins) for (const plugin of this.config.plugins) this.use(plugin);
1265
+ }
1266
+ /**
1267
+ * Register a plugin with the CMS engine.
1268
+ */
1269
+ use(plugin) {
1270
+ if (this.plugins.has(plugin.name)) throw new Error(`[CMSEngine] Plugin '${plugin.name}' is already registered.`);
1271
+ this.plugins.set(plugin.name, plugin);
1272
+ if (plugin.collections) for (const coll of plugin.collections) this.registerCollection(coll);
1273
+ if (plugin.taxonomies) for (const tax of plugin.taxonomies) this.taxonomies.registerTaxonomy(tax);
1274
+ if (plugin.extendCollections) for (const [slug, fields] of Object.entries(plugin.extendCollections)) this.extendCollection(slug, fields);
1275
+ if (plugin.setup) {
1276
+ const context = {
1277
+ engine: this,
1278
+ hooks: this.hooks,
1279
+ storage: this.storage,
1280
+ options: this.options,
1281
+ taxonomies: this.taxonomies,
1282
+ media: this.media,
1283
+ rbac: this.rbac,
1284
+ revisions: this.revisions,
1285
+ lifecycle: this.lifecycle,
1286
+ registerCollection: (c) => this.registerCollection(c),
1287
+ extendCollection: (s, f) => this.extendCollection(s, f),
1288
+ registerTaxonomy: (t) => this.taxonomies.registerTaxonomy(t),
1289
+ registerRoute: (m, p, h) => this.registerRoute(m, p, h)
1290
+ };
1291
+ const setupResult = plugin.setup(context);
1292
+ if (setupResult instanceof Promise) this.pendingPluginSetups.push(setupResult);
1293
+ }
1294
+ return this;
1295
+ }
1296
+ /**
1297
+ * Get all registered plugins.
1298
+ */
1299
+ getPlugins() {
1300
+ return Array.from(this.plugins.values());
1301
+ }
1302
+ /**
1303
+ * Get a registered plugin by name.
1304
+ */
1305
+ getPlugin(name) {
1306
+ return this.plugins.get(name);
1307
+ }
1308
+ /**
1309
+ * Check if a plugin is registered.
1310
+ */
1311
+ hasPlugin(name) {
1312
+ return this.plugins.has(name);
1313
+ }
1314
+ /**
1315
+ * Register a collection dynamically.
1316
+ */
1317
+ registerCollection(coll) {
1318
+ this.collections.set(coll.slug, coll);
1319
+ }
1320
+ /**
1321
+ * Inject additional field definitions into an existing collection.
1322
+ */
1323
+ extendCollection(slug, newFields) {
1324
+ const existing = this.collections.get(slug);
1325
+ if (!existing) throw new Error(`[CMSEngine] Cannot extend non-existent collection '${slug}'.`);
1326
+ const existingFieldNames = new Set(existing.fields.map((f) => f.name));
1327
+ const addedFields = newFields.filter((f) => !existingFieldNames.has(f.name));
1328
+ this.collections.set(slug, {
1329
+ ...existing,
1330
+ fields: [...existing.fields, ...addedFields]
1331
+ });
1332
+ }
1333
+ /**
1334
+ * Register a custom Web Standard HTTP route on the engine.
1335
+ */
1336
+ registerRoute(method, path, handler) {
1337
+ const normalizedMethod = method.toUpperCase();
1338
+ let methodMap = this.customRoutes.get(normalizedMethod);
1339
+ if (!methodMap) {
1340
+ methodMap = /* @__PURE__ */ new Map();
1341
+ this.customRoutes.set(normalizedMethod, methodMap);
1342
+ }
1343
+ methodMap.set(path, handler);
1344
+ }
1345
+ /**
1346
+ * Get all custom routes registered across all HTTP methods.
1347
+ */
1348
+ getCustomRoutes() {
1349
+ return this.customRoutes;
1260
1350
  }
1261
1351
  /**
1262
1352
  * Initialize storage and trigger bootstrap hooks.
1263
1353
  */
1264
1354
  async init() {
1265
1355
  await this.storage.init();
1356
+ if (this.pendingPluginSetups.length > 0) {
1357
+ await Promise.all(this.pendingPluginSetups);
1358
+ this.pendingPluginSetups = [];
1359
+ }
1266
1360
  if (this.config.site) {
1267
1361
  if (!await this.options.has("site_name")) await this.options.set("site_name", this.config.site.name);
1268
1362
  }
1363
+ for (const plugin of this.plugins.values()) if (plugin.onInit) await plugin.onInit(this);
1364
+ await this.hooks.doAction("cms.plugins_initialized", this);
1269
1365
  await this.hooks.doAction("cms.init", this);
1270
1366
  }
1271
1367
  /**
1272
- * Close storage connections.
1368
+ * Close storage connections and run plugin teardown hooks.
1273
1369
  */
1274
1370
  async close() {
1371
+ for (const plugin of this.plugins.values()) if (plugin.onClose) await plugin.onClose(this);
1275
1372
  await this.storage.close();
1276
1373
  await this.hooks.doAction("cms.close", this);
1277
1374
  }
@@ -1297,38 +1394,39 @@ var CMSEngine = class {
1297
1394
  return {
1298
1395
  config: collConfig,
1299
1396
  async create(input, authorId = null) {
1300
- const inputData = { ...input.data || {} };
1301
- if (input.title !== void 0) inputData.title = input.title;
1302
- if (input.slug !== void 0) inputData.slug = input.slug;
1303
- if (input.status !== void 0) inputData.status = input.status;
1397
+ const filteredInput = await self.hooks.applyFilters("cms.before_create_input", input, { collection: slug });
1398
+ const inputData = { ...filteredInput.data || {} };
1399
+ if (filteredInput.title !== void 0 && inputData.title === void 0) inputData.title = filteredInput.title;
1400
+ if (filteredInput.slug !== void 0 && inputData.slug === void 0) inputData.slug = filteredInput.slug;
1401
+ if (filteredInput.status !== void 0 && inputData.status === void 0) inputData.status = filteredInput.status;
1304
1402
  const { data: normalizedData, errors } = validateAndNormalizeData(collConfig.fields, inputData);
1305
1403
  if (Object.keys(errors).length > 0) throw new Error(`[CMSEngine] Validation failed for collection '${slug}': ${JSON.stringify(errors)}`);
1306
- const title = input.title ?? normalizedData.title ?? "";
1307
- const finalSlug = await resolveUniqueSlug(input.slug ? slugify(input.slug) : slugify(title) || "item", async (s) => {
1404
+ const title = filteredInput.title ?? normalizedData.title ?? "";
1405
+ const finalSlug = await resolveUniqueSlug(filteredInput.slug ? slugify(filteredInput.slug) : slugify(title) || "item", async (s) => {
1308
1406
  return await self.storage.getContentBySlug(slug, s) !== null;
1309
1407
  });
1310
- const status = input.status ?? (collConfig.draftable ? "draft" : "published");
1408
+ const status = filteredInput.status ?? (collConfig.draftable ? "draft" : "published");
1311
1409
  const nowIso = (/* @__PURE__ */ new Date()).toISOString();
1312
1410
  const item = await self.storage.createContent({
1313
1411
  collection: slug,
1314
1412
  slug: finalSlug,
1315
1413
  title,
1316
1414
  status,
1317
- parentId: collConfig.hierarchical ? input.parentId ?? null : null,
1415
+ parentId: collConfig.hierarchical ? filteredInput.parentId ?? null : null,
1318
1416
  authorId,
1319
1417
  publishedAt: status === "published" ? nowIso : null,
1320
- scheduledAt: status === "scheduled" ? input.scheduledAt ?? null : null,
1418
+ scheduledAt: status === "scheduled" ? filteredInput.scheduledAt ?? null : null,
1321
1419
  data: normalizedData,
1322
- terms: input.terms
1420
+ terms: filteredInput.terms
1323
1421
  });
1324
- if (input.terms) {
1325
- const allTermIds = Object.values(input.terms).flat();
1422
+ if (filteredInput.terms) {
1423
+ const allTermIds = Object.values(filteredInput.terms).flat();
1326
1424
  if (allTermIds.length > 0) await self.taxonomies.assignTerms(item.id, allTermIds);
1327
1425
  }
1328
1426
  if (collConfig.revisions) await self.revisions.createRevision(item, authorId, "Initial creation");
1329
1427
  await self.hooks.doAction("cms.content_created", item);
1330
1428
  await self.hooks.doAction(`cms.${slug}_created`, item);
1331
- return item;
1429
+ return self.hooks.applyFilters("cms.after_create_item", item, { collection: slug });
1332
1430
  },
1333
1431
  async findById(id) {
1334
1432
  return self.storage.getContent(slug, id);
@@ -1337,51 +1435,58 @@ var CMSEngine = class {
1337
1435
  return self.storage.getContentBySlug(slug, contentSlug);
1338
1436
  },
1339
1437
  async find(options = {}) {
1340
- return self.storage.findContent(slug, options);
1438
+ const filteredOptions = await self.hooks.applyFilters("cms.find_options", options, { collection: slug });
1439
+ return self.storage.findContent(slug, filteredOptions);
1341
1440
  },
1342
1441
  async update(id, input, authorId = null, revisionNote) {
1343
1442
  const existing = await self.storage.getContent(slug, id);
1344
1443
  if (!existing) return null;
1444
+ const filteredInput = await self.hooks.applyFilters("cms.before_update_input", input, {
1445
+ collection: slug,
1446
+ id,
1447
+ existing
1448
+ });
1345
1449
  const mergedData = {
1346
1450
  ...existing.data,
1347
- ...input.data || {}
1451
+ ...filteredInput.data || {}
1348
1452
  };
1349
- if (input.title !== void 0) mergedData.title = input.title;
1453
+ if (filteredInput.title !== void 0 && filteredInput.data?.title === void 0) mergedData.title = filteredInput.title;
1350
1454
  else if (existing.title !== void 0 && mergedData.title === void 0) mergedData.title = existing.title;
1351
- if (input.slug !== void 0) mergedData.slug = input.slug;
1455
+ if (filteredInput.slug !== void 0 && filteredInput.data?.slug === void 0) mergedData.slug = filteredInput.slug;
1352
1456
  else if (existing.slug !== void 0 && mergedData.slug === void 0) mergedData.slug = existing.slug;
1353
- if (input.status !== void 0) mergedData.status = input.status;
1457
+ if (filteredInput.status !== void 0 && filteredInput.data?.status === void 0) mergedData.status = filteredInput.status;
1354
1458
  else if (existing.status !== void 0 && mergedData.status === void 0) mergedData.status = existing.status;
1355
1459
  const { data: normalizedData, errors } = validateAndNormalizeData(collConfig.fields, mergedData);
1356
1460
  if (Object.keys(errors).length > 0) throw new Error(`[CMSEngine] Validation failed updating '${slug}': ${JSON.stringify(errors)}`);
1357
1461
  let updatedSlug = existing.slug;
1358
- if (input.slug && input.slug !== existing.slug) updatedSlug = await resolveUniqueSlug(input.slug, async (s) => {
1462
+ if (filteredInput.slug && filteredInput.slug !== existing.slug) updatedSlug = await resolveUniqueSlug(filteredInput.slug, async (s) => {
1359
1463
  const check = await self.storage.getContentBySlug(slug, s);
1360
1464
  return check !== null && check.id !== id;
1361
1465
  }, id);
1362
- const nextStatus = input.status ?? existing.status;
1466
+ const nextStatus = filteredInput.status ?? existing.status;
1363
1467
  const nowIso = (/* @__PURE__ */ new Date()).toISOString();
1364
1468
  let publishedAt = existing.publishedAt;
1365
1469
  if (nextStatus === "published" && !publishedAt) publishedAt = nowIso;
1366
1470
  const updated = await self.storage.updateContent(slug, id, {
1367
- title: input.title !== void 0 ? input.title : existing.title,
1471
+ title: filteredInput.title !== void 0 ? filteredInput.title : existing.title,
1368
1472
  slug: updatedSlug,
1369
1473
  status: nextStatus,
1370
- parentId: collConfig.hierarchical ? input.parentId !== void 0 ? input.parentId : existing.parentId : null,
1371
- scheduledAt: input.scheduledAt !== void 0 ? input.scheduledAt : existing.scheduledAt,
1474
+ parentId: collConfig.hierarchical ? filteredInput.parentId !== void 0 ? filteredInput.parentId : existing.parentId : null,
1475
+ scheduledAt: filteredInput.scheduledAt !== void 0 ? filteredInput.scheduledAt : existing.scheduledAt,
1372
1476
  publishedAt,
1373
1477
  authorId: authorId ?? existing.authorId,
1374
1478
  data: normalizedData,
1375
- terms: input.terms !== void 0 ? input.terms : existing.terms
1479
+ terms: filteredInput.terms !== void 0 ? filteredInput.terms : existing.terms
1376
1480
  });
1377
1481
  if (updated) {
1378
- if (input.terms) {
1379
- const allTermIds = Object.values(input.terms).flat();
1482
+ if (filteredInput.terms) {
1483
+ const allTermIds = Object.values(filteredInput.terms).flat();
1380
1484
  await self.taxonomies.assignTerms(updated.id, allTermIds);
1381
1485
  }
1382
1486
  if (collConfig.revisions) await self.revisions.createRevision(updated, authorId, revisionNote ?? `Updated version ${updated.version}`);
1383
1487
  await self.hooks.doAction("cms.content_updated", updated);
1384
1488
  await self.hooks.doAction(`cms.${slug}_updated`, updated);
1489
+ return self.hooks.applyFilters("cms.after_update_item", updated, { collection: slug });
1385
1490
  }
1386
1491
  return updated;
1387
1492
  },
@@ -1438,7 +1543,33 @@ function createCMSEngine(config, storage, hooks) {
1438
1543
  return new CMSEngine(config, storage, hooks);
1439
1544
  }
1440
1545
  //#endregion
1546
+ //#region src/core/plugin.ts
1547
+ /**
1548
+ * Type-safe helper for authoring reusable CMS plugins and plugin factories.
1549
+ */
1550
+ function definePlugin(factory) {
1551
+ return factory;
1552
+ }
1553
+ //#endregion
1441
1554
  //#region src/api/router.ts
1555
+ function compilePath(path) {
1556
+ const keys = [];
1557
+ const pattern = path.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
1558
+ keys.push(key);
1559
+ return "([^/]+)";
1560
+ });
1561
+ return {
1562
+ regex: new RegExp(`^${pattern}$`),
1563
+ keys
1564
+ };
1565
+ }
1566
+ function matchRoute(entry, pathname) {
1567
+ const match = pathname.match(entry.regex);
1568
+ if (!match) return null;
1569
+ const params = {};
1570
+ for (let i = 0; i < entry.keys.length; i++) params[entry.keys[i]] = decodeURIComponent(match[i + 1]);
1571
+ return params;
1572
+ }
1442
1573
  function jsonResponse(data, status = 200, headers = {}) {
1443
1574
  return new Response(JSON.stringify(data), {
1444
1575
  status,
@@ -1453,10 +1584,25 @@ function jsonResponse(data, status = 200, headers = {}) {
1453
1584
  }
1454
1585
  var CMSRouter = class {
1455
1586
  engine;
1587
+ localRoutes = [];
1456
1588
  constructor(engine) {
1457
1589
  this.engine = engine;
1458
1590
  }
1459
1591
  /**
1592
+ * Register a custom Web Standard route on the router.
1593
+ */
1594
+ registerRoute(method, path, handler) {
1595
+ const { regex, keys } = compilePath(path);
1596
+ this.localRoutes.push({
1597
+ method: method.toUpperCase(),
1598
+ path,
1599
+ regex,
1600
+ keys,
1601
+ handler
1602
+ });
1603
+ return this;
1604
+ }
1605
+ /**
1460
1606
  * Universal Web Standards request handler.
1461
1607
  */
1462
1608
  async handle(request) {
@@ -1484,6 +1630,8 @@ var CMSRouter = class {
1484
1630
  if (pathname.startsWith("/api/taxonomies")) return this.handleTaxonomies(pathname, method, url, request);
1485
1631
  if (pathname.startsWith("/api/media")) return this.handleMedia(pathname, method, url, request);
1486
1632
  if (pathname.startsWith("/api/content")) return this.handleContent(pathname, method, url, request);
1633
+ const customResponse = await this.handleCustomRoute(pathname, method, url, request);
1634
+ if (customResponse) return customResponse;
1487
1635
  return jsonResponse({
1488
1636
  error: "Endpoint not found",
1489
1637
  path: pathname
@@ -1492,6 +1640,29 @@ var CMSRouter = class {
1492
1640
  return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 500);
1493
1641
  }
1494
1642
  }
1643
+ async handleCustomRoute(pathname, method, url, request) {
1644
+ for (const route of this.localRoutes) if (route.method === method) {
1645
+ const params = matchRoute(route, pathname);
1646
+ if (params !== null) {
1647
+ const context = {
1648
+ params,
1649
+ url,
1650
+ engine: this.engine
1651
+ };
1652
+ return await route.handler(request, context);
1653
+ }
1654
+ }
1655
+ const engineRoutes = this.engine.getCustomRoutes().get(method);
1656
+ if (engineRoutes) for (const [pathPattern, handler] of engineRoutes) {
1657
+ const params = matchRoute(compilePath(pathPattern), pathname);
1658
+ if (params !== null) return await handler(request, {
1659
+ params,
1660
+ url,
1661
+ engine: this.engine
1662
+ });
1663
+ }
1664
+ return null;
1665
+ }
1495
1666
  async handleContent(pathname, method, url, request) {
1496
1667
  const parts = pathname.replace(/^\/api\/content\/?/, "").split("/").filter(Boolean);
1497
1668
  const collectionSlug = parts[0];
@@ -1670,6 +1841,12 @@ var CMSClient = class {
1670
1841
  ...options.headers || {}
1671
1842
  };
1672
1843
  }
1844
+ /**
1845
+ * Access underlying CMSEngine instance when in in-process mode.
1846
+ */
1847
+ getEngine() {
1848
+ return this.engine;
1849
+ }
1673
1850
  collection(slug) {
1674
1851
  const self = this;
1675
1852
  if (this.engine) {
@@ -1753,6 +1930,9 @@ var CMSClient = class {
1753
1930
  return defaultValue;
1754
1931
  }
1755
1932
  } };
1933
+ /**
1934
+ * Perform an HTTP request against the CMS API (available in remote mode).
1935
+ */
1756
1936
  async request(endpoint, init) {
1757
1937
  if (!this.baseUrl) throw new Error(`[CMSClient] baseUrl must be provided when connecting to a remote CMS API.`);
1758
1938
  const url = `${this.baseUrl}${endpoint}`;
@@ -1775,6 +1955,1285 @@ function createCmsClient(options) {
1775
1955
  return new CMSClient(options);
1776
1956
  }
1777
1957
  //#endregion
1778
- export { CMSClient, CMSEngine, CMSRouter, ContentLifecycle, DEFAULT_COLLECTIONS, DEFAULT_ROLE_CAPABILITIES, HooksManager, MediaManager, MemoryStorageAdapter, OptionsManager, RBACManager, RevisionManager, TaxonomyManager, VALID_STATUS_TRANSITIONS, collection, createCMSEngine, createCMSRouter, createCmsClient, defaultHooks, defineConfig, fields, normalizeConfig, resolveUniqueSlug, slugify, validateAndNormalizeData };
1958
+ //#region src/plugins/ecommerce/schemas.ts
1959
+ /**
1960
+ * Creates the collection configuration for Products.
1961
+ */
1962
+ function createProductCollection(options = {}) {
1963
+ const slug = options.productCollectionSlug ?? "products";
1964
+ const catSlug = options.categoriesTaxonomySlug ?? "product_categories";
1965
+ const tagSlug = options.tagsTaxonomySlug ?? "product_tags";
1966
+ const brandSlug = options.brandsTaxonomySlug ?? "product_brands";
1967
+ const defaultCurrency = options.defaultCurrency ?? "USD";
1968
+ return collection({
1969
+ slug,
1970
+ label: "Products",
1971
+ singularLabel: "Product",
1972
+ description: "Catalog products with pricing, inventory, images, and variants",
1973
+ timestamps: true,
1974
+ revisions: true,
1975
+ draftable: true,
1976
+ taxonomies: [
1977
+ catSlug,
1978
+ tagSlug,
1979
+ brandSlug
1980
+ ],
1981
+ defaultSort: {
1982
+ field: "createdAt",
1983
+ direction: "desc"
1984
+ },
1985
+ fields: [
1986
+ fields.text({
1987
+ name: "title",
1988
+ label: "Product Title",
1989
+ required: true
1990
+ }),
1991
+ fields.slug({
1992
+ from: "title",
1993
+ unique: true
1994
+ }),
1995
+ fields.text({
1996
+ name: "sku",
1997
+ label: "SKU",
1998
+ description: "Stock Keeping Unit identifier"
1999
+ }),
2000
+ fields.number({
2001
+ name: "price",
2002
+ label: "Price",
2003
+ required: true,
2004
+ min: 0
2005
+ }),
2006
+ fields.number({
2007
+ name: "compareAtPrice",
2008
+ label: "Compare At Price",
2009
+ description: "Original retail price for showing strike-through discounts",
2010
+ min: 0
2011
+ }),
2012
+ fields.number({
2013
+ name: "costPrice",
2014
+ label: "Cost Price",
2015
+ description: "Wholesale or production cost for margin tracking",
2016
+ min: 0
2017
+ }),
2018
+ fields.text({
2019
+ name: "currency",
2020
+ label: "Currency",
2021
+ defaultValue: defaultCurrency
2022
+ }),
2023
+ fields.number({
2024
+ name: "stock",
2025
+ label: "Inventory Quantity",
2026
+ defaultValue: 0,
2027
+ min: 0
2028
+ }),
2029
+ fields.boolean({
2030
+ name: "trackInventory",
2031
+ label: "Track Inventory",
2032
+ defaultValue: options.inventoryManagement ?? true
2033
+ }),
2034
+ fields.select({
2035
+ name: "status",
2036
+ label: "Status",
2037
+ options: [
2038
+ "draft",
2039
+ "published",
2040
+ "out_of_stock",
2041
+ "archived"
2042
+ ],
2043
+ defaultValue: "draft"
2044
+ }),
2045
+ fields.richText({
2046
+ name: "description",
2047
+ label: "Full Description"
2048
+ }),
2049
+ fields.text({
2050
+ name: "shortDescription",
2051
+ label: "Short Description"
2052
+ }),
2053
+ fields.image({
2054
+ name: "featuredImage",
2055
+ label: "Featured Image"
2056
+ }),
2057
+ fields.json({
2058
+ name: "gallery",
2059
+ label: "Image Gallery",
2060
+ defaultValue: []
2061
+ }),
2062
+ fields.json({
2063
+ name: "variants",
2064
+ label: "Product Variants",
2065
+ defaultValue: []
2066
+ }),
2067
+ fields.json({
2068
+ name: "attributes",
2069
+ label: "Specifications / Attributes",
2070
+ defaultValue: {}
2071
+ }),
2072
+ fields.number({
2073
+ name: "weight",
2074
+ label: "Weight",
2075
+ min: 0
2076
+ })
2077
+ ]
2078
+ });
2079
+ }
2080
+ /**
2081
+ * Creates the collection configuration for Discounts / Coupons.
2082
+ */
2083
+ function createDiscountCollection(options = {}) {
2084
+ return collection({
2085
+ slug: options.discountCollectionSlug ?? "discounts",
2086
+ label: "Discounts",
2087
+ singularLabel: "Discount",
2088
+ description: "Promotional discount codes and coupons",
2089
+ timestamps: true,
2090
+ revisions: true,
2091
+ draftable: false,
2092
+ defaultSort: {
2093
+ field: "createdAt",
2094
+ direction: "desc"
2095
+ },
2096
+ fields: [
2097
+ fields.text({
2098
+ name: "title",
2099
+ label: "Discount Name",
2100
+ required: true
2101
+ }),
2102
+ fields.text({
2103
+ name: "code",
2104
+ label: "Coupon Code",
2105
+ required: true,
2106
+ unique: true
2107
+ }),
2108
+ fields.select({
2109
+ name: "discountType",
2110
+ label: "Discount Type",
2111
+ options: [
2112
+ "percentage",
2113
+ "fixed_amount",
2114
+ "free_shipping"
2115
+ ],
2116
+ defaultValue: "percentage"
2117
+ }),
2118
+ fields.number({
2119
+ name: "value",
2120
+ label: "Discount Value",
2121
+ required: true,
2122
+ min: 0
2123
+ }),
2124
+ fields.number({
2125
+ name: "minOrderAmount",
2126
+ label: "Minimum Order Amount",
2127
+ min: 0
2128
+ }),
2129
+ fields.number({
2130
+ name: "maxDiscountAmount",
2131
+ label: "Maximum Discount Cap",
2132
+ min: 0
2133
+ }),
2134
+ fields.number({
2135
+ name: "maxUses",
2136
+ label: "Maximum Uses",
2137
+ min: 1
2138
+ }),
2139
+ fields.number({
2140
+ name: "usedCount",
2141
+ label: "Times Used",
2142
+ defaultValue: 0,
2143
+ min: 0
2144
+ }),
2145
+ fields.date({
2146
+ name: "startDate",
2147
+ label: "Start Date"
2148
+ }),
2149
+ fields.date({
2150
+ name: "endDate",
2151
+ label: "Expiration Date"
2152
+ }),
2153
+ fields.select({
2154
+ name: "status",
2155
+ label: "Status",
2156
+ options: [
2157
+ "active",
2158
+ "disabled",
2159
+ "expired"
2160
+ ],
2161
+ defaultValue: "active"
2162
+ }),
2163
+ fields.json({
2164
+ name: "appliesToProductIds",
2165
+ label: "Specific Product IDs",
2166
+ defaultValue: []
2167
+ }),
2168
+ fields.json({
2169
+ name: "appliesToCategoryIds",
2170
+ label: "Specific Category Term IDs",
2171
+ defaultValue: []
2172
+ })
2173
+ ]
2174
+ });
2175
+ }
2176
+ /**
2177
+ * Creates the collection configuration for Orders.
2178
+ */
2179
+ function createOrderCollection(options = {}) {
2180
+ const slug = options.orderCollectionSlug ?? "orders";
2181
+ const defaultCurrency = options.defaultCurrency ?? "USD";
2182
+ return collection({
2183
+ slug,
2184
+ label: "Orders",
2185
+ singularLabel: "Order",
2186
+ description: "Customer orders, status lifecycle, and line items",
2187
+ timestamps: true,
2188
+ revisions: true,
2189
+ draftable: false,
2190
+ defaultSort: {
2191
+ field: "createdAt",
2192
+ direction: "desc"
2193
+ },
2194
+ fields: [
2195
+ fields.text({
2196
+ name: "orderNumber",
2197
+ label: "Order Number",
2198
+ required: true,
2199
+ unique: true
2200
+ }),
2201
+ fields.text({
2202
+ name: "customerEmail",
2203
+ label: "Customer Email",
2204
+ required: true
2205
+ }),
2206
+ fields.text({
2207
+ name: "customerName",
2208
+ label: "Customer Name"
2209
+ }),
2210
+ fields.select({
2211
+ name: "status",
2212
+ label: "Status",
2213
+ options: [
2214
+ "pending",
2215
+ "paid",
2216
+ "processing",
2217
+ "shipped",
2218
+ "delivered",
2219
+ "cancelled",
2220
+ "refunded"
2221
+ ],
2222
+ defaultValue: "pending"
2223
+ }),
2224
+ fields.text({
2225
+ name: "currency",
2226
+ label: "Currency",
2227
+ defaultValue: defaultCurrency
2228
+ }),
2229
+ fields.json({
2230
+ name: "items",
2231
+ label: "Line Items",
2232
+ defaultValue: []
2233
+ }),
2234
+ fields.number({
2235
+ name: "subtotal",
2236
+ label: "Subtotal",
2237
+ defaultValue: 0,
2238
+ min: 0
2239
+ }),
2240
+ fields.number({
2241
+ name: "discountTotal",
2242
+ label: "Discount Total",
2243
+ defaultValue: 0,
2244
+ min: 0
2245
+ }),
2246
+ fields.text({
2247
+ name: "discountCode",
2248
+ label: "Discount Code"
2249
+ }),
2250
+ fields.number({
2251
+ name: "shippingTotal",
2252
+ label: "Shipping Total",
2253
+ defaultValue: 0,
2254
+ min: 0
2255
+ }),
2256
+ fields.number({
2257
+ name: "taxTotal",
2258
+ label: "Tax Total",
2259
+ defaultValue: 0,
2260
+ min: 0
2261
+ }),
2262
+ fields.number({
2263
+ name: "total",
2264
+ label: "Grand Total",
2265
+ defaultValue: 0,
2266
+ min: 0
2267
+ }),
2268
+ fields.json({
2269
+ name: "shippingAddress",
2270
+ label: "Shipping Address"
2271
+ }),
2272
+ fields.json({
2273
+ name: "billingAddress",
2274
+ label: "Billing Address"
2275
+ }),
2276
+ fields.text({
2277
+ name: "paymentMethod",
2278
+ label: "Payment Method"
2279
+ }),
2280
+ fields.text({
2281
+ name: "notes",
2282
+ label: "Notes"
2283
+ })
2284
+ ]
2285
+ });
2286
+ }
2287
+ /**
2288
+ * Creates the standard e-commerce taxonomies: product categories, tags, and brands.
2289
+ */
2290
+ function createEcommerceTaxonomies(options = {}) {
2291
+ const productSlug = options.productCollectionSlug ?? "products";
2292
+ const catSlug = options.categoriesTaxonomySlug ?? "product_categories";
2293
+ const tagSlug = options.tagsTaxonomySlug ?? "product_tags";
2294
+ const brandSlug = options.brandsTaxonomySlug ?? "product_brands";
2295
+ return [
2296
+ {
2297
+ slug: catSlug,
2298
+ label: "Product Categories",
2299
+ singularLabel: "Product Category",
2300
+ hierarchical: true,
2301
+ postTypes: [productSlug],
2302
+ description: "Hierarchical categories and catalog collections for organizing products"
2303
+ },
2304
+ {
2305
+ slug: tagSlug,
2306
+ label: "Product Tags",
2307
+ singularLabel: "Product Tag",
2308
+ hierarchical: false,
2309
+ postTypes: [productSlug],
2310
+ description: "Flat keyword tags for filtering products"
2311
+ },
2312
+ {
2313
+ slug: brandSlug,
2314
+ label: "Brands",
2315
+ singularLabel: "Brand",
2316
+ hierarchical: false,
2317
+ postTypes: [productSlug],
2318
+ description: "Product manufacturers or brand labels"
2319
+ }
2320
+ ];
2321
+ }
2322
+ //#endregion
2323
+ //#region src/plugins/ecommerce/service.ts
2324
+ var EcommerceService = class {
2325
+ engine;
2326
+ options;
2327
+ productSlug;
2328
+ discountSlug;
2329
+ orderSlug;
2330
+ categoriesTaxonomy;
2331
+ tagsTaxonomy;
2332
+ brandsTaxonomy;
2333
+ defaultCurrency;
2334
+ inventoryManagement;
2335
+ constructor(engine, options = {}) {
2336
+ this.engine = engine;
2337
+ this.options = options;
2338
+ this.productSlug = options.productCollectionSlug ?? "products";
2339
+ this.discountSlug = options.discountCollectionSlug ?? "discounts";
2340
+ this.orderSlug = options.orderCollectionSlug ?? "orders";
2341
+ this.categoriesTaxonomy = options.categoriesTaxonomySlug ?? "product_categories";
2342
+ this.tagsTaxonomy = options.tagsTaxonomySlug ?? "product_tags";
2343
+ this.brandsTaxonomy = options.brandsTaxonomySlug ?? "product_brands";
2344
+ this.defaultCurrency = options.defaultCurrency ?? "USD";
2345
+ this.inventoryManagement = options.inventoryManagement ?? true;
2346
+ }
2347
+ get productsCollection() {
2348
+ return this.engine.collection(this.productSlug);
2349
+ }
2350
+ get discountsCollection() {
2351
+ return this.engine.collection(this.discountSlug);
2352
+ }
2353
+ get ordersCollection() {
2354
+ return this.engine.collection(this.orderSlug);
2355
+ }
2356
+ /**
2357
+ * Create a new product in the catalog.
2358
+ */
2359
+ async createProduct(input, authorId) {
2360
+ const productData = {
2361
+ price: input.price,
2362
+ compareAtPrice: input.compareAtPrice,
2363
+ costPrice: input.costPrice,
2364
+ sku: input.sku,
2365
+ currency: input.currency ?? this.defaultCurrency,
2366
+ stock: input.stock ?? 0,
2367
+ trackInventory: input.trackInventory ?? this.inventoryManagement,
2368
+ status: input.status ?? "draft",
2369
+ description: input.description,
2370
+ shortDescription: input.shortDescription,
2371
+ featuredImage: input.featuredImage,
2372
+ gallery: input.gallery ?? [],
2373
+ variants: input.variants ?? [],
2374
+ attributes: input.attributes ?? {},
2375
+ weight: input.weight
2376
+ };
2377
+ const product = await this.productsCollection.create({
2378
+ title: input.title,
2379
+ slug: input.slug,
2380
+ status: input.status === "published" ? "published" : "draft",
2381
+ data: productData
2382
+ }, authorId);
2383
+ if (input.categoryIds && input.categoryIds.length > 0) await this.engine.taxonomies.assignTerms(product.id, input.categoryIds);
2384
+ if (input.tagIds && input.tagIds.length > 0) await this.engine.taxonomies.assignTerms(product.id, input.tagIds);
2385
+ if (input.brandIds && input.brandIds.length > 0) await this.engine.taxonomies.assignTerms(product.id, input.brandIds);
2386
+ await this.engine.hooks.doAction("ecommerce.product_created", product);
2387
+ return product;
2388
+ }
2389
+ /**
2390
+ * Update an existing product.
2391
+ */
2392
+ async updateProduct(id, input, authorId) {
2393
+ const existing = await this.getProduct(id);
2394
+ if (!existing) return null;
2395
+ const updatedData = {
2396
+ ...existing.data,
2397
+ ...input.price !== void 0 ? { price: input.price } : {},
2398
+ ...input.compareAtPrice !== void 0 ? { compareAtPrice: input.compareAtPrice } : {},
2399
+ ...input.costPrice !== void 0 ? { costPrice: input.costPrice } : {},
2400
+ ...input.sku !== void 0 ? { sku: input.sku } : {},
2401
+ ...input.currency !== void 0 ? { currency: input.currency } : {},
2402
+ ...input.stock !== void 0 ? { stock: input.stock } : {},
2403
+ ...input.trackInventory !== void 0 ? { trackInventory: input.trackInventory } : {},
2404
+ ...input.status !== void 0 ? { status: input.status } : {},
2405
+ ...input.description !== void 0 ? { description: input.description } : {},
2406
+ ...input.shortDescription !== void 0 ? { shortDescription: input.shortDescription } : {},
2407
+ ...input.featuredImage !== void 0 ? { featuredImage: input.featuredImage } : {},
2408
+ ...input.gallery !== void 0 ? { gallery: input.gallery } : {},
2409
+ ...input.variants !== void 0 ? { variants: input.variants } : {},
2410
+ ...input.attributes !== void 0 ? { attributes: input.attributes } : {},
2411
+ ...input.weight !== void 0 ? { weight: input.weight } : {}
2412
+ };
2413
+ const updated = await this.productsCollection.update(id, {
2414
+ ...input.title ? { title: input.title } : {},
2415
+ ...input.slug ? { slug: input.slug } : {},
2416
+ ...input.status === "published" ? { status: "published" } : input.status ? { status: "draft" } : {},
2417
+ data: updatedData
2418
+ }, authorId);
2419
+ if (updated) {
2420
+ if (input.categoryIds) await this.engine.taxonomies.assignTerms(id, input.categoryIds);
2421
+ if (input.tagIds) await this.engine.taxonomies.assignTerms(id, input.tagIds);
2422
+ if (input.brandIds) await this.engine.taxonomies.assignTerms(id, input.brandIds);
2423
+ await this.engine.hooks.doAction("ecommerce.product_updated", updated);
2424
+ }
2425
+ return updated;
2426
+ }
2427
+ /**
2428
+ * Get a product by ID.
2429
+ */
2430
+ async getProduct(id) {
2431
+ return this.productsCollection.findById(id);
2432
+ }
2433
+ /**
2434
+ * Get a product by its URL slug.
2435
+ */
2436
+ async getProductBySlug(slug) {
2437
+ return this.productsCollection.findBySlug(slug);
2438
+ }
2439
+ /**
2440
+ * Delete a product by ID.
2441
+ */
2442
+ async deleteProduct(id) {
2443
+ const deleted = await this.productsCollection.delete(id);
2444
+ if (deleted) await this.engine.hooks.doAction("ecommerce.product_deleted", id);
2445
+ return deleted;
2446
+ }
2447
+ /**
2448
+ * List and filter catalog products.
2449
+ */
2450
+ async listProducts(query = {}) {
2451
+ let termIds;
2452
+ if (query.categorySlug) {
2453
+ const term = await this.engine.taxonomies.getTermBySlug(this.categoriesTaxonomy, query.categorySlug);
2454
+ if (term) termIds = [term.id];
2455
+ else return {
2456
+ items: [],
2457
+ total: 0,
2458
+ limit: query.limit ?? 20,
2459
+ offset: query.offset ?? 0,
2460
+ hasMore: false
2461
+ };
2462
+ } else if (query.categoryId) termIds = [query.categoryId];
2463
+ if (query.tagSlug) {
2464
+ const term = await this.engine.taxonomies.getTermBySlug(this.tagsTaxonomy, query.tagSlug);
2465
+ if (term) termIds = termIds ? [...termIds, term.id] : [term.id];
2466
+ }
2467
+ if (query.brandSlug) {
2468
+ const term = await this.engine.taxonomies.getTermBySlug(this.brandsTaxonomy, query.brandSlug);
2469
+ if (term) termIds = termIds ? [...termIds, term.id] : [term.id];
2470
+ }
2471
+ const contentStatus = query.status === "draft" ? "draft" : query.status === "published" ? "published" : void 0;
2472
+ const result = await this.productsCollection.find({
2473
+ search: query.search,
2474
+ termIds,
2475
+ status: contentStatus ?? "published",
2476
+ limit: query.limit ?? 20,
2477
+ offset: query.offset ?? 0,
2478
+ orderBy: query.orderBy === "price" || query.orderBy === "stock" ? void 0 : query.orderBy,
2479
+ orderDirection: query.orderDirection ?? "desc"
2480
+ });
2481
+ let items = result.items;
2482
+ if (query.status) {
2483
+ const statuses = Array.isArray(query.status) ? query.status : [query.status];
2484
+ items = items.filter((p) => statuses.includes(p.data.status));
2485
+ }
2486
+ if (query.minPrice !== void 0) items = items.filter((p) => p.data.price >= query.minPrice);
2487
+ if (query.maxPrice !== void 0) items = items.filter((p) => p.data.price <= query.maxPrice);
2488
+ if (query.inStock) items = items.filter((p) => !p.data.trackInventory || p.data.stock > 0);
2489
+ if (query.orderBy === "price") items.sort((a, b) => query.orderDirection === "asc" ? a.data.price - b.data.price : b.data.price - a.data.price);
2490
+ else if (query.orderBy === "stock") items.sort((a, b) => query.orderDirection === "asc" ? a.data.stock - b.data.stock : b.data.stock - a.data.stock);
2491
+ return {
2492
+ items,
2493
+ total: items.length,
2494
+ limit: result.limit,
2495
+ offset: result.offset,
2496
+ hasMore: result.offset + items.length < result.total
2497
+ };
2498
+ }
2499
+ /**
2500
+ * Upload and link a product image to its gallery and featured slot.
2501
+ */
2502
+ async uploadProductImage(productId, file, authorId) {
2503
+ const product = await this.getProduct(productId);
2504
+ if (!product) throw new Error(`[EcommerceService] Product '${productId}' not found.`);
2505
+ const media = await this.engine.media.upload({
2506
+ filename: file.filename,
2507
+ mimeType: file.mimeType,
2508
+ sizeBytes: file.sizeBytes,
2509
+ url: file.url,
2510
+ altText: file.altText ?? product.title,
2511
+ caption: file.caption,
2512
+ width: file.width,
2513
+ height: file.height
2514
+ }, authorId);
2515
+ const gallery = [...product.data.gallery || []];
2516
+ gallery.push({
2517
+ id: media.id,
2518
+ url: media.url,
2519
+ altText: media.altText,
2520
+ caption: media.caption,
2521
+ width: media.width,
2522
+ height: media.height
2523
+ });
2524
+ const isFeatured = file.isFeatured ?? !product.data.featuredImage;
2525
+ const updated = await this.productsCollection.update(productId, { data: {
2526
+ ...product.data,
2527
+ gallery,
2528
+ ...isFeatured ? { featuredImage: media.url } : {}
2529
+ } });
2530
+ await this.engine.hooks.doAction("ecommerce.product_image_added", {
2531
+ product: updated ?? product,
2532
+ media
2533
+ });
2534
+ return {
2535
+ media,
2536
+ product: updated ?? product
2537
+ };
2538
+ }
2539
+ /**
2540
+ * Adjust inventory stock for a product or variant.
2541
+ */
2542
+ async adjustStock(productId, delta, variantId) {
2543
+ const product = await this.getProduct(productId);
2544
+ if (!product) return null;
2545
+ if (!product.data.trackInventory) return product;
2546
+ let newStock = product.data.stock;
2547
+ const variants = [...product.data.variants || []];
2548
+ if (variantId) {
2549
+ const idx = variants.findIndex((v) => v.id === variantId);
2550
+ if (idx !== -1) {
2551
+ const currentVariantStock = variants[idx].stock ?? 0;
2552
+ const updatedVariantStock = Math.max(0, currentVariantStock + delta);
2553
+ variants[idx] = {
2554
+ ...variants[idx],
2555
+ stock: updatedVariantStock
2556
+ };
2557
+ }
2558
+ } else newStock = Math.max(0, product.data.stock + delta);
2559
+ const newStatus = newStock === 0 && product.data.status === "published" ? "out_of_stock" : product.data.status === "out_of_stock" && newStock > 0 ? "published" : product.data.status;
2560
+ const updated = await this.productsCollection.update(productId, { data: {
2561
+ ...product.data,
2562
+ stock: newStock,
2563
+ variants,
2564
+ status: newStatus
2565
+ } });
2566
+ await this.engine.hooks.doAction("ecommerce.stock_changed", {
2567
+ product: updated ?? product,
2568
+ delta,
2569
+ variantId,
2570
+ oldStock: product.data.stock,
2571
+ newStock
2572
+ });
2573
+ return updated;
2574
+ }
2575
+ /**
2576
+ * Create a catalog category in the hierarchical category taxonomy.
2577
+ */
2578
+ async createCategory(input) {
2579
+ return this.engine.taxonomies.createTerm(this.categoriesTaxonomy, input);
2580
+ }
2581
+ /**
2582
+ * Get all catalog categories.
2583
+ */
2584
+ async getCategories(options) {
2585
+ return this.engine.taxonomies.getTerms(this.categoriesTaxonomy, options);
2586
+ }
2587
+ /**
2588
+ * Get full hierarchical catalog category tree.
2589
+ */
2590
+ async getCategoryTree() {
2591
+ return this.engine.taxonomies.getTermTree(this.categoriesTaxonomy);
2592
+ }
2593
+ /**
2594
+ * Assign category IDs to a product.
2595
+ */
2596
+ async assignProductCategory(productId, categoryIds) {
2597
+ const ids = Array.isArray(categoryIds) ? categoryIds : [categoryIds];
2598
+ await this.engine.taxonomies.assignTerms(productId, ids);
2599
+ }
2600
+ /**
2601
+ * Get assigned categories for a product.
2602
+ */
2603
+ async getProductCategories(productId) {
2604
+ return this.engine.taxonomies.getContentTerms(productId, this.categoriesTaxonomy);
2605
+ }
2606
+ /**
2607
+ * Create a promotional coupon / discount code.
2608
+ */
2609
+ async createDiscount(input, authorId) {
2610
+ const normalizedCode = input.code.trim().toUpperCase();
2611
+ const discountData = {
2612
+ code: normalizedCode,
2613
+ discountType: input.discountType,
2614
+ value: input.value,
2615
+ minOrderAmount: input.minOrderAmount,
2616
+ maxDiscountAmount: input.maxDiscountAmount,
2617
+ maxUses: input.maxUses,
2618
+ usedCount: 0,
2619
+ startDate: input.startDate,
2620
+ endDate: input.endDate,
2621
+ status: input.status ?? "active",
2622
+ appliesToProductIds: input.appliesToProductIds ?? [],
2623
+ appliesToCategoryIds: input.appliesToCategoryIds ?? []
2624
+ };
2625
+ const discount = await this.discountsCollection.create({
2626
+ title: input.title,
2627
+ slug: normalizedCode.toLowerCase(),
2628
+ status: "published",
2629
+ data: discountData
2630
+ }, authorId);
2631
+ await this.engine.hooks.doAction("ecommerce.discount_created", discount);
2632
+ return discount;
2633
+ }
2634
+ /**
2635
+ * Find a discount code.
2636
+ */
2637
+ async getDiscountByCode(code) {
2638
+ const normalized = code.trim().toUpperCase();
2639
+ return (await this.discountsCollection.find({ limit: 100 })).items.find((d) => d.data.code?.toUpperCase() === normalized) ?? null;
2640
+ }
2641
+ /**
2642
+ * Validate a discount coupon against cart items and order subtotal.
2643
+ */
2644
+ async validateDiscount(code, cartSubtotal, productIds = []) {
2645
+ const normalized = code.trim().toUpperCase();
2646
+ const discount = await this.getDiscountByCode(normalized);
2647
+ if (!discount) return {
2648
+ valid: false,
2649
+ code: normalized,
2650
+ discountAmount: 0,
2651
+ message: `Discount code '${code}' not found.`
2652
+ };
2653
+ const { data } = discount;
2654
+ if (data.status !== "active") return {
2655
+ valid: false,
2656
+ code: normalized,
2657
+ discountAmount: 0,
2658
+ message: "Discount code is currently inactive."
2659
+ };
2660
+ const now = Date.now();
2661
+ if (data.startDate && new Date(data.startDate).getTime() > now) return {
2662
+ valid: false,
2663
+ code: normalized,
2664
+ discountAmount: 0,
2665
+ message: "Discount code is not yet active."
2666
+ };
2667
+ if (data.endDate && new Date(data.endDate).getTime() < now) return {
2668
+ valid: false,
2669
+ code: normalized,
2670
+ discountAmount: 0,
2671
+ message: "Discount code has expired."
2672
+ };
2673
+ if (data.maxUses !== void 0 && data.usedCount >= data.maxUses) return {
2674
+ valid: false,
2675
+ code: normalized,
2676
+ discountAmount: 0,
2677
+ message: "Discount code usage limit reached."
2678
+ };
2679
+ if (data.minOrderAmount !== void 0 && cartSubtotal < data.minOrderAmount) return {
2680
+ valid: false,
2681
+ code: normalized,
2682
+ discountAmount: 0,
2683
+ message: `Minimum order amount of ${data.minOrderAmount} required for this coupon.`
2684
+ };
2685
+ if (data.appliesToProductIds && data.appliesToProductIds.length > 0 && productIds.length > 0) {
2686
+ if (!productIds.some((id) => data.appliesToProductIds?.includes(id))) return {
2687
+ valid: false,
2688
+ code: normalized,
2689
+ discountAmount: 0,
2690
+ message: "Coupon is not applicable to any products in your cart."
2691
+ };
2692
+ }
2693
+ let discountAmount = 0;
2694
+ if (data.discountType === "percentage") {
2695
+ discountAmount = cartSubtotal * data.value / 100;
2696
+ if (data.maxDiscountAmount) discountAmount = Math.min(discountAmount, data.maxDiscountAmount);
2697
+ } else if (data.discountType === "fixed_amount") discountAmount = Math.min(data.value, cartSubtotal);
2698
+ else if (data.discountType === "free_shipping") discountAmount = 0;
2699
+ discountAmount = await this.engine.hooks.applyFilters("ecommerce.apply_discount", discountAmount, {
2700
+ discount,
2701
+ cartSubtotal
2702
+ });
2703
+ return {
2704
+ valid: true,
2705
+ code: normalized,
2706
+ discountAmount: Math.round(discountAmount * 100) / 100,
2707
+ discountType: data.discountType,
2708
+ discount
2709
+ };
2710
+ }
2711
+ /**
2712
+ * Calculate cart subtotals, apply discounts, shipping, and taxes.
2713
+ */
2714
+ async calculateCart(input) {
2715
+ const lineItems = [];
2716
+ let subtotal = 0;
2717
+ const productIds = [];
2718
+ for (const item of input.items) {
2719
+ const product = await this.getProduct(item.productId);
2720
+ if (!product) throw new Error(`[EcommerceService] Product '${item.productId}' not found.`);
2721
+ productIds.push(product.id);
2722
+ let price = product.data.price;
2723
+ let sku = product.data.sku;
2724
+ let title = product.title ?? "Product";
2725
+ let availableStock = product.data.stock;
2726
+ let image = product.data.featuredImage;
2727
+ if (item.variantId && product.data.variants) {
2728
+ const variant = product.data.variants.find((v) => v.id === item.variantId);
2729
+ if (variant) {
2730
+ price = variant.price ?? price;
2731
+ sku = variant.sku ?? sku;
2732
+ title = `${title} (${variant.title})`;
2733
+ availableStock = variant.stock ?? availableStock;
2734
+ image = variant.image ?? image;
2735
+ }
2736
+ }
2737
+ if (this.inventoryManagement && product.data.trackInventory && availableStock < item.quantity) throw new Error(`[EcommerceService] Insufficient stock for '${title}'. Available: ${availableStock}, Requested: ${item.quantity}.`);
2738
+ const itemSubtotal = Math.round(price * item.quantity * 100) / 100;
2739
+ subtotal += itemSubtotal;
2740
+ lineItems.push({
2741
+ productId: product.id,
2742
+ variantId: item.variantId,
2743
+ title,
2744
+ sku,
2745
+ price,
2746
+ quantity: item.quantity,
2747
+ subtotal: itemSubtotal,
2748
+ image
2749
+ });
2750
+ }
2751
+ subtotal = Math.round(subtotal * 100) / 100;
2752
+ let discountTotal = 0;
2753
+ let discountType;
2754
+ if (input.discountCode) {
2755
+ const validation = await this.validateDiscount(input.discountCode, subtotal, productIds);
2756
+ if (validation.valid) {
2757
+ discountTotal = validation.discountAmount;
2758
+ discountType = validation.discountType;
2759
+ }
2760
+ }
2761
+ let shippingTotal = input.shippingCost ?? this.options.defaultShippingCost ?? 0;
2762
+ if (discountType === "free_shipping") shippingTotal = 0;
2763
+ const taxRate = input.taxRate ?? this.options.defaultTaxRate ?? 0;
2764
+ const taxableAmount = Math.max(0, subtotal - discountTotal);
2765
+ const taxTotal = Math.round(taxableAmount * taxRate * 100) / 100;
2766
+ const total = Math.round((taxableAmount + shippingTotal + taxTotal) * 100) / 100;
2767
+ const result = {
2768
+ items: lineItems,
2769
+ subtotal,
2770
+ discountTotal,
2771
+ discountCode: input.discountCode,
2772
+ shippingTotal,
2773
+ taxTotal,
2774
+ total,
2775
+ currency: this.defaultCurrency
2776
+ };
2777
+ return this.engine.hooks.applyFilters("ecommerce.calculate_totals", result, input);
2778
+ }
2779
+ /**
2780
+ * Place a new order with cart validation, inventory deduction, and coupon counter updates.
2781
+ */
2782
+ async createOrder(input, authorId) {
2783
+ const calc = await this.calculateCart({
2784
+ items: input.items,
2785
+ discountCode: input.discountCode,
2786
+ shippingCost: input.shippingCost,
2787
+ taxRate: input.taxRate
2788
+ });
2789
+ const orderNumber = `ORD-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "")}-${Math.random().toString(36).substring(2, 6).toUpperCase()}`;
2790
+ if (this.inventoryManagement) for (const item of input.items) await this.adjustStock(item.productId, -item.quantity, item.variantId);
2791
+ if (input.discountCode) {
2792
+ const discount = await this.getDiscountByCode(input.discountCode);
2793
+ if (discount) await this.discountsCollection.update(discount.id, { data: {
2794
+ ...discount.data,
2795
+ usedCount: (discount.data.usedCount || 0) + 1
2796
+ } });
2797
+ }
2798
+ const orderData = {
2799
+ orderNumber,
2800
+ customerEmail: input.customerEmail,
2801
+ customerName: input.customerName,
2802
+ status: "pending",
2803
+ currency: calc.currency,
2804
+ items: calc.items,
2805
+ subtotal: calc.subtotal,
2806
+ discountTotal: calc.discountTotal,
2807
+ discountCode: calc.discountCode,
2808
+ shippingTotal: calc.shippingTotal,
2809
+ taxTotal: calc.taxTotal,
2810
+ total: calc.total,
2811
+ shippingAddress: input.shippingAddress,
2812
+ billingAddress: input.billingAddress,
2813
+ paymentMethod: input.paymentMethod,
2814
+ notes: input.notes
2815
+ };
2816
+ const order = await this.ordersCollection.create({
2817
+ title: `Order #${orderNumber}`,
2818
+ slug: orderNumber.toLowerCase(),
2819
+ status: "published",
2820
+ data: orderData
2821
+ }, authorId);
2822
+ await this.engine.hooks.doAction("ecommerce.order_created", order);
2823
+ return order;
2824
+ }
2825
+ /**
2826
+ * Get order by ID.
2827
+ */
2828
+ async getOrder(id) {
2829
+ return this.ordersCollection.findById(id);
2830
+ }
2831
+ /**
2832
+ * Get order by order number.
2833
+ */
2834
+ async getOrderByNumber(orderNumber) {
2835
+ return (await this.ordersCollection.find({ limit: 100 })).items.find((o) => o.data.orderNumber?.toUpperCase() === orderNumber.toUpperCase()) ?? null;
2836
+ }
2837
+ /**
2838
+ * Update the status of an order (e.g. pending -> paid -> shipped).
2839
+ */
2840
+ async updateOrderStatus(id, status, note) {
2841
+ const existing = await this.getOrder(id);
2842
+ if (!existing) return null;
2843
+ const oldStatus = existing.data.status;
2844
+ const updated = await this.ordersCollection.update(id, { data: {
2845
+ ...existing.data,
2846
+ status
2847
+ } }, void 0, note);
2848
+ if (updated) await this.engine.hooks.doAction("ecommerce.order_status_changed", {
2849
+ order: updated,
2850
+ oldStatus,
2851
+ newStatus: status,
2852
+ note
2853
+ });
2854
+ return updated;
2855
+ }
2856
+ };
2857
+ //#endregion
2858
+ //#region src/plugins/ecommerce/routes.ts
2859
+ function json(data, status = 200) {
2860
+ return new Response(JSON.stringify(data), {
2861
+ status,
2862
+ headers: {
2863
+ "Content-Type": "application/json",
2864
+ "Access-Control-Allow-Origin": "*"
2865
+ }
2866
+ });
2867
+ }
2868
+ function badRequest(message) {
2869
+ return json({ error: message }, 400);
2870
+ }
2871
+ function notFound(message) {
2872
+ return json({ error: message }, 404);
2873
+ }
2874
+ function registerEcommerceRoutes(ctx, service, options = {}) {
2875
+ const prefix = (options.apiPrefix ?? "/api/ecommerce").replace(/\/+$/, "");
2876
+ ctx.registerRoute("GET", `${prefix}/products`, async (_req, { url }) => {
2877
+ try {
2878
+ const categorySlug = url.searchParams.get("category") ?? void 0;
2879
+ const categoryId = url.searchParams.get("categoryId") ?? void 0;
2880
+ const tagSlug = url.searchParams.get("tag") ?? void 0;
2881
+ const brandSlug = url.searchParams.get("brand") ?? void 0;
2882
+ const search = url.searchParams.get("search") ?? void 0;
2883
+ const statusParam = url.searchParams.get("status");
2884
+ const inStock = url.searchParams.get("inStock") === "true";
2885
+ const minPriceStr = url.searchParams.get("minPrice");
2886
+ const maxPriceStr = url.searchParams.get("maxPrice");
2887
+ const minPrice = minPriceStr ? parseFloat(minPriceStr) : void 0;
2888
+ const maxPrice = maxPriceStr ? parseFloat(maxPriceStr) : void 0;
2889
+ const orderBy = url.searchParams.get("orderBy");
2890
+ const orderDirection = url.searchParams.get("orderDirection") ?? "desc";
2891
+ const limitStr = url.searchParams.get("limit");
2892
+ const offsetStr = url.searchParams.get("offset");
2893
+ const limit = limitStr ? parseInt(limitStr, 10) : 20;
2894
+ const offset = offsetStr ? parseInt(offsetStr, 10) : 0;
2895
+ return json(await service.listProducts({
2896
+ categorySlug,
2897
+ categoryId,
2898
+ tagSlug,
2899
+ brandSlug,
2900
+ search,
2901
+ status: statusParam,
2902
+ inStock,
2903
+ minPrice,
2904
+ maxPrice,
2905
+ orderBy,
2906
+ orderDirection,
2907
+ limit,
2908
+ offset
2909
+ }));
2910
+ } catch (err) {
2911
+ return badRequest(err instanceof Error ? err.message : String(err));
2912
+ }
2913
+ });
2914
+ ctx.registerRoute("GET", `${prefix}/products/:id`, async (_req, { params, url }) => {
2915
+ const id = params.id;
2916
+ const by = url.searchParams.get("by");
2917
+ let product = null;
2918
+ if (by === "slug") product = await service.getProductBySlug(id);
2919
+ else {
2920
+ product = await service.getProduct(id);
2921
+ if (!product) product = await service.getProductBySlug(id);
2922
+ }
2923
+ if (!product) return notFound(`Product '${id}' not found`);
2924
+ return json(product);
2925
+ });
2926
+ ctx.registerRoute("POST", `${prefix}/products`, async (req) => {
2927
+ try {
2928
+ const body = await req.json();
2929
+ if (!body.title) return badRequest("Product 'title' is required.");
2930
+ if (body.price === void 0 || body.price < 0) return badRequest("Valid product 'price' is required.");
2931
+ return json(await service.createProduct(body), 201);
2932
+ } catch (err) {
2933
+ return badRequest(err instanceof Error ? err.message : String(err));
2934
+ }
2935
+ });
2936
+ ctx.registerRoute("PUT", `${prefix}/products/:id`, async (req, { params }) => {
2937
+ try {
2938
+ const body = await req.json();
2939
+ const updated = await service.updateProduct(params.id, body);
2940
+ if (!updated) return notFound(`Product '${params.id}' not found.`);
2941
+ return json(updated);
2942
+ } catch (err) {
2943
+ return badRequest(err instanceof Error ? err.message : String(err));
2944
+ }
2945
+ });
2946
+ ctx.registerRoute("DELETE", `${prefix}/products/:id`, async (_req, { params }) => {
2947
+ if (!await service.deleteProduct(params.id)) return notFound(`Product '${params.id}' not found.`);
2948
+ return json({
2949
+ success: true,
2950
+ id: params.id
2951
+ });
2952
+ });
2953
+ ctx.registerRoute("POST", `${prefix}/products/:id/images`, async (req, { params }) => {
2954
+ try {
2955
+ const body = await req.json();
2956
+ if (!body.filename || !body.mimeType) return badRequest("'filename' and 'mimeType' are required.");
2957
+ return json(await service.uploadProductImage(params.id, {
2958
+ filename: body.filename,
2959
+ mimeType: body.mimeType,
2960
+ sizeBytes: body.sizeBytes ?? 0,
2961
+ url: body.url,
2962
+ altText: body.altText,
2963
+ caption: body.caption,
2964
+ width: body.width,
2965
+ height: body.height,
2966
+ isFeatured: body.isFeatured
2967
+ }), 201);
2968
+ } catch (err) {
2969
+ return badRequest(err instanceof Error ? err.message : String(err));
2970
+ }
2971
+ });
2972
+ ctx.registerRoute("GET", `${prefix}/categories`, async (_req, { url }) => {
2973
+ try {
2974
+ if (url.searchParams.get("tree") === "true") return json(await service.getCategoryTree());
2975
+ const parentId = url.searchParams.get("parentId");
2976
+ return json(await service.getCategories({ parentId: parentId === "null" ? null : parentId ?? void 0 }));
2977
+ } catch (err) {
2978
+ return badRequest(err instanceof Error ? err.message : String(err));
2979
+ }
2980
+ });
2981
+ ctx.registerRoute("POST", `${prefix}/categories`, async (req) => {
2982
+ try {
2983
+ const body = await req.json();
2984
+ if (!body.name) return badRequest("Category 'name' is required.");
2985
+ return json(await service.createCategory(body), 201);
2986
+ } catch (err) {
2987
+ return badRequest(err instanceof Error ? err.message : String(err));
2988
+ }
2989
+ });
2990
+ if (options.enableDiscounts !== false) {
2991
+ ctx.registerRoute("POST", `${prefix}/discounts`, async (req) => {
2992
+ try {
2993
+ const body = await req.json();
2994
+ if (!body.title || !body.code) return badRequest("'title' and 'code' are required.");
2995
+ if (body.value === void 0 || body.value < 0) return badRequest("Valid discount 'value' is required.");
2996
+ return json(await service.createDiscount(body), 201);
2997
+ } catch (err) {
2998
+ return badRequest(err instanceof Error ? err.message : String(err));
2999
+ }
3000
+ });
3001
+ ctx.registerRoute("POST", `${prefix}/discounts/validate`, async (req) => {
3002
+ try {
3003
+ const body = await req.json();
3004
+ if (!body.code) return badRequest("Discount 'code' is required.");
3005
+ const subtotal = Number(body.subtotal ?? 0);
3006
+ const productIds = Array.isArray(body.productIds) ? body.productIds : [];
3007
+ return json(await service.validateDiscount(body.code, subtotal, productIds));
3008
+ } catch (err) {
3009
+ return badRequest(err instanceof Error ? err.message : String(err));
3010
+ }
3011
+ });
3012
+ }
3013
+ ctx.registerRoute("POST", `${prefix}/cart/calculate`, async (req) => {
3014
+ try {
3015
+ const body = await req.json();
3016
+ if (!body.items || !Array.isArray(body.items) || body.items.length === 0) return badRequest("'items' array is required and must not be empty.");
3017
+ return json(await service.calculateCart(body));
3018
+ } catch (err) {
3019
+ return badRequest(err instanceof Error ? err.message : String(err));
3020
+ }
3021
+ });
3022
+ if (options.enableOrders !== false) {
3023
+ ctx.registerRoute("POST", `${prefix}/orders`, async (req) => {
3024
+ try {
3025
+ const body = await req.json();
3026
+ if (!body.customerEmail) return badRequest("'customerEmail' is required.");
3027
+ if (!body.items || !Array.isArray(body.items) || body.items.length === 0) return badRequest("'items' array is required.");
3028
+ return json(await service.createOrder(body), 201);
3029
+ } catch (err) {
3030
+ return badRequest(err instanceof Error ? err.message : String(err));
3031
+ }
3032
+ });
3033
+ ctx.registerRoute("GET", `${prefix}/orders/:id`, async (_req, { params, url }) => {
3034
+ const id = params.id;
3035
+ const by = url.searchParams.get("by");
3036
+ let order = null;
3037
+ if (by === "number") order = await service.getOrderByNumber(id);
3038
+ else {
3039
+ order = await service.getOrder(id);
3040
+ if (!order) order = await service.getOrderByNumber(id);
3041
+ }
3042
+ if (!order) return notFound(`Order '${id}' not found.`);
3043
+ return json(order);
3044
+ });
3045
+ ctx.registerRoute("PATCH", `${prefix}/orders/:id/status`, async (req, { params }) => {
3046
+ try {
3047
+ const body = await req.json();
3048
+ if (!body.status) return badRequest("New 'status' is required.");
3049
+ const updated = await service.updateOrderStatus(params.id, body.status, body.note);
3050
+ if (!updated) return notFound(`Order '${params.id}' not found.`);
3051
+ return json(updated);
3052
+ } catch (err) {
3053
+ return badRequest(err instanceof Error ? err.message : String(err));
3054
+ }
3055
+ });
3056
+ }
3057
+ }
3058
+ //#endregion
3059
+ //#region src/plugins/ecommerce/client.ts
3060
+ var EcommerceClient = class {
3061
+ client;
3062
+ options;
3063
+ service;
3064
+ prefix;
3065
+ constructor(client, options = {}) {
3066
+ this.client = client;
3067
+ this.options = options;
3068
+ this.prefix = (options.apiPrefix ?? "/api/ecommerce").replace(/\/+$/, "");
3069
+ const engine = client.getEngine();
3070
+ if (engine) this.service = new EcommerceService(engine, options);
3071
+ }
3072
+ products = {
3073
+ find: async (query = {}) => {
3074
+ if (this.service) return this.service.listProducts(query);
3075
+ const params = new URLSearchParams();
3076
+ if (query.categorySlug) params.set("category", query.categorySlug);
3077
+ if (query.categoryId) params.set("categoryId", query.categoryId);
3078
+ if (query.tagSlug) params.set("tag", query.tagSlug);
3079
+ if (query.brandSlug) params.set("brand", query.brandSlug);
3080
+ if (query.search) params.set("search", query.search);
3081
+ if (query.inStock) params.set("inStock", "true");
3082
+ if (query.minPrice !== void 0) params.set("minPrice", String(query.minPrice));
3083
+ if (query.maxPrice !== void 0) params.set("maxPrice", String(query.maxPrice));
3084
+ if (query.orderBy) params.set("orderBy", query.orderBy);
3085
+ if (query.orderDirection) params.set("orderDirection", query.orderDirection);
3086
+ if (query.limit !== void 0) params.set("limit", String(query.limit));
3087
+ if (query.offset !== void 0) params.set("offset", String(query.offset));
3088
+ const q = params.toString() ? `?${params.toString()}` : "";
3089
+ return this.client.request(`${this.prefix}/products${q}`);
3090
+ },
3091
+ get: async (idOrSlug, by) => {
3092
+ if (this.service) return by === "slug" ? this.service.getProductBySlug(idOrSlug) : this.service.getProduct(idOrSlug);
3093
+ const query = by ? `?by=${by}` : "";
3094
+ return this.client.request(`${this.prefix}/products/${encodeURIComponent(idOrSlug)}${query}`);
3095
+ },
3096
+ create: async (data) => {
3097
+ if (this.service) return this.service.createProduct(data);
3098
+ return this.client.request(`${this.prefix}/products`, {
3099
+ method: "POST",
3100
+ body: JSON.stringify(data)
3101
+ });
3102
+ },
3103
+ update: async (id, data) => {
3104
+ if (this.service) return this.service.updateProduct(id, data);
3105
+ return this.client.request(`${this.prefix}/products/${encodeURIComponent(id)}`, {
3106
+ method: "PUT",
3107
+ body: JSON.stringify(data)
3108
+ });
3109
+ },
3110
+ delete: async (id) => {
3111
+ if (this.service) return this.service.deleteProduct(id);
3112
+ const res = await this.client.request(`${this.prefix}/products/${encodeURIComponent(id)}`, { method: "DELETE" });
3113
+ return Boolean(res?.success);
3114
+ },
3115
+ uploadImage: async (productId, file) => {
3116
+ if (this.service) return this.service.uploadProductImage(productId, file);
3117
+ return this.client.request(`${this.prefix}/products/${encodeURIComponent(productId)}/images`, {
3118
+ method: "POST",
3119
+ body: JSON.stringify(file)
3120
+ });
3121
+ }
3122
+ };
3123
+ categories = {
3124
+ list: async (options) => {
3125
+ if (this.service) return this.service.getCategories(options);
3126
+ const q = options?.parentId !== void 0 ? `?parentId=${options.parentId}` : "";
3127
+ return this.client.request(`${this.prefix}/categories${q}`);
3128
+ },
3129
+ tree: async () => {
3130
+ if (this.service) return this.service.getCategoryTree();
3131
+ return this.client.request(`${this.prefix}/categories?tree=true`);
3132
+ },
3133
+ create: async (input) => {
3134
+ if (this.service) return this.service.createCategory(input);
3135
+ return this.client.request(`${this.prefix}/categories`, {
3136
+ method: "POST",
3137
+ body: JSON.stringify(input)
3138
+ });
3139
+ }
3140
+ };
3141
+ discounts = {
3142
+ validate: async (code, subtotal, productIds) => {
3143
+ if (this.service) return this.service.validateDiscount(code, subtotal, productIds);
3144
+ return this.client.request(`${this.prefix}/discounts/validate`, {
3145
+ method: "POST",
3146
+ body: JSON.stringify({
3147
+ code,
3148
+ subtotal,
3149
+ productIds
3150
+ })
3151
+ });
3152
+ },
3153
+ create: async (input) => {
3154
+ if (this.service) return this.service.createDiscount(input);
3155
+ return this.client.request(`${this.prefix}/discounts`, {
3156
+ method: "POST",
3157
+ body: JSON.stringify(input)
3158
+ });
3159
+ }
3160
+ };
3161
+ cart = { calculate: async (input) => {
3162
+ if (this.service) return this.service.calculateCart(input);
3163
+ return this.client.request(`${this.prefix}/cart/calculate`, {
3164
+ method: "POST",
3165
+ body: JSON.stringify(input)
3166
+ });
3167
+ } };
3168
+ orders = {
3169
+ create: async (input) => {
3170
+ if (this.service) return this.service.createOrder(input);
3171
+ return this.client.request(`${this.prefix}/orders`, {
3172
+ method: "POST",
3173
+ body: JSON.stringify(input)
3174
+ });
3175
+ },
3176
+ get: async (idOrNumber, by) => {
3177
+ if (this.service) return by === "number" ? this.service.getOrderByNumber(idOrNumber) : this.service.getOrder(idOrNumber);
3178
+ const q = by ? `?by=${by}` : "";
3179
+ return this.client.request(`${this.prefix}/orders/${encodeURIComponent(idOrNumber)}${q}`);
3180
+ },
3181
+ updateStatus: async (id, status, note) => {
3182
+ if (this.service) return this.service.updateOrderStatus(id, status, note);
3183
+ return this.client.request(`${this.prefix}/orders/${encodeURIComponent(id)}/status`, {
3184
+ method: "PATCH",
3185
+ body: JSON.stringify({
3186
+ status,
3187
+ note
3188
+ })
3189
+ });
3190
+ }
3191
+ };
3192
+ };
3193
+ /**
3194
+ * Get or create an EcommerceClient adapter for a CMSClient.
3195
+ */
3196
+ function getEcommerceClient(client, options) {
3197
+ return new EcommerceClient(client, options);
3198
+ }
3199
+ //#endregion
3200
+ //#region src/plugins/ecommerce/index.ts
3201
+ /**
3202
+ * @azlib/cms - Built-in E-commerce Plugin
3203
+ */
3204
+ /**
3205
+ * Built-in E-commerce plugin factory for @azlib/cms.
3206
+ * Equips the CMS engine with product catalogs, hierarchical categories,
3207
+ * image uploading, discount coupons, cart calculation, and order tracking.
3208
+ */
3209
+ const ecommercePlugin = definePlugin((options) => {
3210
+ const opts = options || {};
3211
+ const collections = [createProductCollection(opts)];
3212
+ if (opts.enableDiscounts !== false) collections.push(createDiscountCollection(opts));
3213
+ if (opts.enableOrders !== false) collections.push(createOrderCollection(opts));
3214
+ return {
3215
+ name: "ecommerce",
3216
+ version: "1.0.0",
3217
+ description: "Built-in E-commerce shopping, products, catalogs, discounts, and orders plugin",
3218
+ collections,
3219
+ taxonomies: createEcommerceTaxonomies(opts),
3220
+ setup(ctx) {
3221
+ const service = new EcommerceService(ctx.engine, opts);
3222
+ ctx.engine.__ecommerceService = service;
3223
+ registerEcommerceRoutes(ctx, service, opts);
3224
+ }
3225
+ };
3226
+ });
3227
+ /**
3228
+ * Retrieve the active EcommerceService instance associated with a CMSEngine.
3229
+ */
3230
+ function getEcommerceService(engine, options) {
3231
+ if (engine.__ecommerceService) return engine.__ecommerceService;
3232
+ const service = new EcommerceService(engine, options);
3233
+ engine.__ecommerceService = service;
3234
+ return service;
3235
+ }
3236
+ //#endregion
3237
+ export { CMSClient, CMSEngine, CMSRouter, ContentLifecycle, DEFAULT_COLLECTIONS, DEFAULT_ROLE_CAPABILITIES, EcommerceClient, EcommerceService, HooksManager, MediaManager, MemoryStorageAdapter, OptionsManager, RBACManager, RevisionManager, TaxonomyManager, VALID_STATUS_TRANSITIONS, collection, createCMSEngine, createCMSRouter, createCmsClient, createDiscountCollection, createEcommerceTaxonomies, createOrderCollection, createProductCollection, defaultHooks, defineConfig, definePlugin, ecommercePlugin, fields, getEcommerceClient, getEcommerceService, normalizeConfig, resolveUniqueSlug, slugify, validateAndNormalizeData };
1779
3238
 
1780
3239
  //# sourceMappingURL=index.mjs.map