@azlib/cms 0.3.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.cjs CHANGED
@@ -1397,9 +1397,9 @@ var CMSEngine = class {
1397
1397
  async create(input, authorId = null) {
1398
1398
  const filteredInput = await self.hooks.applyFilters("cms.before_create_input", input, { collection: slug });
1399
1399
  const inputData = { ...filteredInput.data || {} };
1400
- if (filteredInput.title !== void 0) inputData.title = filteredInput.title;
1401
- if (filteredInput.slug !== void 0) inputData.slug = filteredInput.slug;
1402
- if (filteredInput.status !== void 0) inputData.status = filteredInput.status;
1400
+ if (filteredInput.title !== void 0 && inputData.title === void 0) inputData.title = filteredInput.title;
1401
+ if (filteredInput.slug !== void 0 && inputData.slug === void 0) inputData.slug = filteredInput.slug;
1402
+ if (filteredInput.status !== void 0 && inputData.status === void 0) inputData.status = filteredInput.status;
1403
1403
  const { data: normalizedData, errors } = validateAndNormalizeData(collConfig.fields, inputData);
1404
1404
  if (Object.keys(errors).length > 0) throw new Error(`[CMSEngine] Validation failed for collection '${slug}': ${JSON.stringify(errors)}`);
1405
1405
  const title = filteredInput.title ?? normalizedData.title ?? "";
@@ -1451,11 +1451,11 @@ var CMSEngine = class {
1451
1451
  ...existing.data,
1452
1452
  ...filteredInput.data || {}
1453
1453
  };
1454
- if (filteredInput.title !== void 0) mergedData.title = filteredInput.title;
1454
+ if (filteredInput.title !== void 0 && filteredInput.data?.title === void 0) mergedData.title = filteredInput.title;
1455
1455
  else if (existing.title !== void 0 && mergedData.title === void 0) mergedData.title = existing.title;
1456
- if (filteredInput.slug !== void 0) mergedData.slug = filteredInput.slug;
1456
+ if (filteredInput.slug !== void 0 && filteredInput.data?.slug === void 0) mergedData.slug = filteredInput.slug;
1457
1457
  else if (existing.slug !== void 0 && mergedData.slug === void 0) mergedData.slug = existing.slug;
1458
- if (filteredInput.status !== void 0) mergedData.status = filteredInput.status;
1458
+ if (filteredInput.status !== void 0 && filteredInput.data?.status === void 0) mergedData.status = filteredInput.status;
1459
1459
  else if (existing.status !== void 0 && mergedData.status === void 0) mergedData.status = existing.status;
1460
1460
  const { data: normalizedData, errors } = validateAndNormalizeData(collConfig.fields, mergedData);
1461
1461
  if (Object.keys(errors).length > 0) throw new Error(`[CMSEngine] Validation failed updating '${slug}': ${JSON.stringify(errors)}`);
@@ -1842,6 +1842,12 @@ var CMSClient = class {
1842
1842
  ...options.headers || {}
1843
1843
  };
1844
1844
  }
1845
+ /**
1846
+ * Access underlying CMSEngine instance when in in-process mode.
1847
+ */
1848
+ getEngine() {
1849
+ return this.engine;
1850
+ }
1845
1851
  collection(slug) {
1846
1852
  const self = this;
1847
1853
  if (this.engine) {
@@ -1925,6 +1931,9 @@ var CMSClient = class {
1925
1931
  return defaultValue;
1926
1932
  }
1927
1933
  } };
1934
+ /**
1935
+ * Perform an HTTP request against the CMS API (available in remote mode).
1936
+ */
1928
1937
  async request(endpoint, init) {
1929
1938
  if (!this.baseUrl) throw new Error(`[CMSClient] baseUrl must be provided when connecting to a remote CMS API.`);
1930
1939
  const url = `${this.baseUrl}${endpoint}`;
@@ -1947,12 +1956,1293 @@ function createCmsClient(options) {
1947
1956
  return new CMSClient(options);
1948
1957
  }
1949
1958
  //#endregion
1959
+ //#region src/plugins/ecommerce/schemas.ts
1960
+ /**
1961
+ * Creates the collection configuration for Products.
1962
+ */
1963
+ function createProductCollection(options = {}) {
1964
+ const slug = options.productCollectionSlug ?? "products";
1965
+ const catSlug = options.categoriesTaxonomySlug ?? "product_categories";
1966
+ const tagSlug = options.tagsTaxonomySlug ?? "product_tags";
1967
+ const brandSlug = options.brandsTaxonomySlug ?? "product_brands";
1968
+ const defaultCurrency = options.defaultCurrency ?? "USD";
1969
+ return collection({
1970
+ slug,
1971
+ label: "Products",
1972
+ singularLabel: "Product",
1973
+ description: "Catalog products with pricing, inventory, images, and variants",
1974
+ timestamps: true,
1975
+ revisions: true,
1976
+ draftable: true,
1977
+ taxonomies: [
1978
+ catSlug,
1979
+ tagSlug,
1980
+ brandSlug
1981
+ ],
1982
+ defaultSort: {
1983
+ field: "createdAt",
1984
+ direction: "desc"
1985
+ },
1986
+ fields: [
1987
+ fields.text({
1988
+ name: "title",
1989
+ label: "Product Title",
1990
+ required: true
1991
+ }),
1992
+ fields.slug({
1993
+ from: "title",
1994
+ unique: true
1995
+ }),
1996
+ fields.text({
1997
+ name: "sku",
1998
+ label: "SKU",
1999
+ description: "Stock Keeping Unit identifier"
2000
+ }),
2001
+ fields.number({
2002
+ name: "price",
2003
+ label: "Price",
2004
+ required: true,
2005
+ min: 0
2006
+ }),
2007
+ fields.number({
2008
+ name: "compareAtPrice",
2009
+ label: "Compare At Price",
2010
+ description: "Original retail price for showing strike-through discounts",
2011
+ min: 0
2012
+ }),
2013
+ fields.number({
2014
+ name: "costPrice",
2015
+ label: "Cost Price",
2016
+ description: "Wholesale or production cost for margin tracking",
2017
+ min: 0
2018
+ }),
2019
+ fields.text({
2020
+ name: "currency",
2021
+ label: "Currency",
2022
+ defaultValue: defaultCurrency
2023
+ }),
2024
+ fields.number({
2025
+ name: "stock",
2026
+ label: "Inventory Quantity",
2027
+ defaultValue: 0,
2028
+ min: 0
2029
+ }),
2030
+ fields.boolean({
2031
+ name: "trackInventory",
2032
+ label: "Track Inventory",
2033
+ defaultValue: options.inventoryManagement ?? true
2034
+ }),
2035
+ fields.select({
2036
+ name: "status",
2037
+ label: "Status",
2038
+ options: [
2039
+ "draft",
2040
+ "published",
2041
+ "out_of_stock",
2042
+ "archived"
2043
+ ],
2044
+ defaultValue: "draft"
2045
+ }),
2046
+ fields.richText({
2047
+ name: "description",
2048
+ label: "Full Description"
2049
+ }),
2050
+ fields.text({
2051
+ name: "shortDescription",
2052
+ label: "Short Description"
2053
+ }),
2054
+ fields.image({
2055
+ name: "featuredImage",
2056
+ label: "Featured Image"
2057
+ }),
2058
+ fields.json({
2059
+ name: "gallery",
2060
+ label: "Image Gallery",
2061
+ defaultValue: []
2062
+ }),
2063
+ fields.json({
2064
+ name: "variants",
2065
+ label: "Product Variants",
2066
+ defaultValue: []
2067
+ }),
2068
+ fields.json({
2069
+ name: "attributes",
2070
+ label: "Specifications / Attributes",
2071
+ defaultValue: {}
2072
+ }),
2073
+ fields.number({
2074
+ name: "weight",
2075
+ label: "Weight",
2076
+ min: 0
2077
+ })
2078
+ ]
2079
+ });
2080
+ }
2081
+ /**
2082
+ * Creates the collection configuration for Discounts / Coupons.
2083
+ */
2084
+ function createDiscountCollection(options = {}) {
2085
+ return collection({
2086
+ slug: options.discountCollectionSlug ?? "discounts",
2087
+ label: "Discounts",
2088
+ singularLabel: "Discount",
2089
+ description: "Promotional discount codes and coupons",
2090
+ timestamps: true,
2091
+ revisions: true,
2092
+ draftable: false,
2093
+ defaultSort: {
2094
+ field: "createdAt",
2095
+ direction: "desc"
2096
+ },
2097
+ fields: [
2098
+ fields.text({
2099
+ name: "title",
2100
+ label: "Discount Name",
2101
+ required: true
2102
+ }),
2103
+ fields.text({
2104
+ name: "code",
2105
+ label: "Coupon Code",
2106
+ required: true,
2107
+ unique: true
2108
+ }),
2109
+ fields.select({
2110
+ name: "discountType",
2111
+ label: "Discount Type",
2112
+ options: [
2113
+ "percentage",
2114
+ "fixed_amount",
2115
+ "free_shipping"
2116
+ ],
2117
+ defaultValue: "percentage"
2118
+ }),
2119
+ fields.number({
2120
+ name: "value",
2121
+ label: "Discount Value",
2122
+ required: true,
2123
+ min: 0
2124
+ }),
2125
+ fields.number({
2126
+ name: "minOrderAmount",
2127
+ label: "Minimum Order Amount",
2128
+ min: 0
2129
+ }),
2130
+ fields.number({
2131
+ name: "maxDiscountAmount",
2132
+ label: "Maximum Discount Cap",
2133
+ min: 0
2134
+ }),
2135
+ fields.number({
2136
+ name: "maxUses",
2137
+ label: "Maximum Uses",
2138
+ min: 1
2139
+ }),
2140
+ fields.number({
2141
+ name: "usedCount",
2142
+ label: "Times Used",
2143
+ defaultValue: 0,
2144
+ min: 0
2145
+ }),
2146
+ fields.date({
2147
+ name: "startDate",
2148
+ label: "Start Date"
2149
+ }),
2150
+ fields.date({
2151
+ name: "endDate",
2152
+ label: "Expiration Date"
2153
+ }),
2154
+ fields.select({
2155
+ name: "status",
2156
+ label: "Status",
2157
+ options: [
2158
+ "active",
2159
+ "disabled",
2160
+ "expired"
2161
+ ],
2162
+ defaultValue: "active"
2163
+ }),
2164
+ fields.json({
2165
+ name: "appliesToProductIds",
2166
+ label: "Specific Product IDs",
2167
+ defaultValue: []
2168
+ }),
2169
+ fields.json({
2170
+ name: "appliesToCategoryIds",
2171
+ label: "Specific Category Term IDs",
2172
+ defaultValue: []
2173
+ })
2174
+ ]
2175
+ });
2176
+ }
2177
+ /**
2178
+ * Creates the collection configuration for Orders.
2179
+ */
2180
+ function createOrderCollection(options = {}) {
2181
+ const slug = options.orderCollectionSlug ?? "orders";
2182
+ const defaultCurrency = options.defaultCurrency ?? "USD";
2183
+ return collection({
2184
+ slug,
2185
+ label: "Orders",
2186
+ singularLabel: "Order",
2187
+ description: "Customer orders, status lifecycle, and line items",
2188
+ timestamps: true,
2189
+ revisions: true,
2190
+ draftable: false,
2191
+ defaultSort: {
2192
+ field: "createdAt",
2193
+ direction: "desc"
2194
+ },
2195
+ fields: [
2196
+ fields.text({
2197
+ name: "orderNumber",
2198
+ label: "Order Number",
2199
+ required: true,
2200
+ unique: true
2201
+ }),
2202
+ fields.text({
2203
+ name: "customerEmail",
2204
+ label: "Customer Email",
2205
+ required: true
2206
+ }),
2207
+ fields.text({
2208
+ name: "customerName",
2209
+ label: "Customer Name"
2210
+ }),
2211
+ fields.select({
2212
+ name: "status",
2213
+ label: "Status",
2214
+ options: [
2215
+ "pending",
2216
+ "paid",
2217
+ "processing",
2218
+ "shipped",
2219
+ "delivered",
2220
+ "cancelled",
2221
+ "refunded"
2222
+ ],
2223
+ defaultValue: "pending"
2224
+ }),
2225
+ fields.text({
2226
+ name: "currency",
2227
+ label: "Currency",
2228
+ defaultValue: defaultCurrency
2229
+ }),
2230
+ fields.json({
2231
+ name: "items",
2232
+ label: "Line Items",
2233
+ defaultValue: []
2234
+ }),
2235
+ fields.number({
2236
+ name: "subtotal",
2237
+ label: "Subtotal",
2238
+ defaultValue: 0,
2239
+ min: 0
2240
+ }),
2241
+ fields.number({
2242
+ name: "discountTotal",
2243
+ label: "Discount Total",
2244
+ defaultValue: 0,
2245
+ min: 0
2246
+ }),
2247
+ fields.text({
2248
+ name: "discountCode",
2249
+ label: "Discount Code"
2250
+ }),
2251
+ fields.number({
2252
+ name: "shippingTotal",
2253
+ label: "Shipping Total",
2254
+ defaultValue: 0,
2255
+ min: 0
2256
+ }),
2257
+ fields.number({
2258
+ name: "taxTotal",
2259
+ label: "Tax Total",
2260
+ defaultValue: 0,
2261
+ min: 0
2262
+ }),
2263
+ fields.number({
2264
+ name: "total",
2265
+ label: "Grand Total",
2266
+ defaultValue: 0,
2267
+ min: 0
2268
+ }),
2269
+ fields.json({
2270
+ name: "shippingAddress",
2271
+ label: "Shipping Address"
2272
+ }),
2273
+ fields.json({
2274
+ name: "billingAddress",
2275
+ label: "Billing Address"
2276
+ }),
2277
+ fields.text({
2278
+ name: "paymentMethod",
2279
+ label: "Payment Method"
2280
+ }),
2281
+ fields.text({
2282
+ name: "notes",
2283
+ label: "Notes"
2284
+ })
2285
+ ]
2286
+ });
2287
+ }
2288
+ /**
2289
+ * Creates the standard e-commerce taxonomies: product categories, tags, and brands.
2290
+ */
2291
+ function createEcommerceTaxonomies(options = {}) {
2292
+ const productSlug = options.productCollectionSlug ?? "products";
2293
+ const catSlug = options.categoriesTaxonomySlug ?? "product_categories";
2294
+ const tagSlug = options.tagsTaxonomySlug ?? "product_tags";
2295
+ const brandSlug = options.brandsTaxonomySlug ?? "product_brands";
2296
+ return [
2297
+ {
2298
+ slug: catSlug,
2299
+ label: "Product Categories",
2300
+ singularLabel: "Product Category",
2301
+ hierarchical: true,
2302
+ postTypes: [productSlug],
2303
+ description: "Hierarchical categories and catalog collections for organizing products"
2304
+ },
2305
+ {
2306
+ slug: tagSlug,
2307
+ label: "Product Tags",
2308
+ singularLabel: "Product Tag",
2309
+ hierarchical: false,
2310
+ postTypes: [productSlug],
2311
+ description: "Flat keyword tags for filtering products"
2312
+ },
2313
+ {
2314
+ slug: brandSlug,
2315
+ label: "Brands",
2316
+ singularLabel: "Brand",
2317
+ hierarchical: false,
2318
+ postTypes: [productSlug],
2319
+ description: "Product manufacturers or brand labels"
2320
+ }
2321
+ ];
2322
+ }
2323
+ //#endregion
2324
+ //#region src/plugins/ecommerce/service.ts
2325
+ var EcommerceService = class {
2326
+ engine;
2327
+ options;
2328
+ productSlug;
2329
+ discountSlug;
2330
+ orderSlug;
2331
+ categoriesTaxonomy;
2332
+ tagsTaxonomy;
2333
+ brandsTaxonomy;
2334
+ defaultCurrency;
2335
+ inventoryManagement;
2336
+ constructor(engine, options = {}) {
2337
+ this.engine = engine;
2338
+ this.options = options;
2339
+ this.productSlug = options.productCollectionSlug ?? "products";
2340
+ this.discountSlug = options.discountCollectionSlug ?? "discounts";
2341
+ this.orderSlug = options.orderCollectionSlug ?? "orders";
2342
+ this.categoriesTaxonomy = options.categoriesTaxonomySlug ?? "product_categories";
2343
+ this.tagsTaxonomy = options.tagsTaxonomySlug ?? "product_tags";
2344
+ this.brandsTaxonomy = options.brandsTaxonomySlug ?? "product_brands";
2345
+ this.defaultCurrency = options.defaultCurrency ?? "USD";
2346
+ this.inventoryManagement = options.inventoryManagement ?? true;
2347
+ }
2348
+ get productsCollection() {
2349
+ return this.engine.collection(this.productSlug);
2350
+ }
2351
+ get discountsCollection() {
2352
+ return this.engine.collection(this.discountSlug);
2353
+ }
2354
+ get ordersCollection() {
2355
+ return this.engine.collection(this.orderSlug);
2356
+ }
2357
+ /**
2358
+ * Create a new product in the catalog.
2359
+ */
2360
+ async createProduct(input, authorId) {
2361
+ const productData = {
2362
+ price: input.price,
2363
+ compareAtPrice: input.compareAtPrice,
2364
+ costPrice: input.costPrice,
2365
+ sku: input.sku,
2366
+ currency: input.currency ?? this.defaultCurrency,
2367
+ stock: input.stock ?? 0,
2368
+ trackInventory: input.trackInventory ?? this.inventoryManagement,
2369
+ status: input.status ?? "draft",
2370
+ description: input.description,
2371
+ shortDescription: input.shortDescription,
2372
+ featuredImage: input.featuredImage,
2373
+ gallery: input.gallery ?? [],
2374
+ variants: input.variants ?? [],
2375
+ attributes: input.attributes ?? {},
2376
+ weight: input.weight
2377
+ };
2378
+ const product = await this.productsCollection.create({
2379
+ title: input.title,
2380
+ slug: input.slug,
2381
+ status: input.status === "published" ? "published" : "draft",
2382
+ data: productData
2383
+ }, authorId);
2384
+ if (input.categoryIds && input.categoryIds.length > 0) await this.engine.taxonomies.assignTerms(product.id, input.categoryIds);
2385
+ if (input.tagIds && input.tagIds.length > 0) await this.engine.taxonomies.assignTerms(product.id, input.tagIds);
2386
+ if (input.brandIds && input.brandIds.length > 0) await this.engine.taxonomies.assignTerms(product.id, input.brandIds);
2387
+ await this.engine.hooks.doAction("ecommerce.product_created", product);
2388
+ return product;
2389
+ }
2390
+ /**
2391
+ * Update an existing product.
2392
+ */
2393
+ async updateProduct(id, input, authorId) {
2394
+ const existing = await this.getProduct(id);
2395
+ if (!existing) return null;
2396
+ const updatedData = {
2397
+ ...existing.data,
2398
+ ...input.price !== void 0 ? { price: input.price } : {},
2399
+ ...input.compareAtPrice !== void 0 ? { compareAtPrice: input.compareAtPrice } : {},
2400
+ ...input.costPrice !== void 0 ? { costPrice: input.costPrice } : {},
2401
+ ...input.sku !== void 0 ? { sku: input.sku } : {},
2402
+ ...input.currency !== void 0 ? { currency: input.currency } : {},
2403
+ ...input.stock !== void 0 ? { stock: input.stock } : {},
2404
+ ...input.trackInventory !== void 0 ? { trackInventory: input.trackInventory } : {},
2405
+ ...input.status !== void 0 ? { status: input.status } : {},
2406
+ ...input.description !== void 0 ? { description: input.description } : {},
2407
+ ...input.shortDescription !== void 0 ? { shortDescription: input.shortDescription } : {},
2408
+ ...input.featuredImage !== void 0 ? { featuredImage: input.featuredImage } : {},
2409
+ ...input.gallery !== void 0 ? { gallery: input.gallery } : {},
2410
+ ...input.variants !== void 0 ? { variants: input.variants } : {},
2411
+ ...input.attributes !== void 0 ? { attributes: input.attributes } : {},
2412
+ ...input.weight !== void 0 ? { weight: input.weight } : {}
2413
+ };
2414
+ const updated = await this.productsCollection.update(id, {
2415
+ ...input.title ? { title: input.title } : {},
2416
+ ...input.slug ? { slug: input.slug } : {},
2417
+ ...input.status === "published" ? { status: "published" } : input.status ? { status: "draft" } : {},
2418
+ data: updatedData
2419
+ }, authorId);
2420
+ if (updated) {
2421
+ if (input.categoryIds) await this.engine.taxonomies.assignTerms(id, input.categoryIds);
2422
+ if (input.tagIds) await this.engine.taxonomies.assignTerms(id, input.tagIds);
2423
+ if (input.brandIds) await this.engine.taxonomies.assignTerms(id, input.brandIds);
2424
+ await this.engine.hooks.doAction("ecommerce.product_updated", updated);
2425
+ }
2426
+ return updated;
2427
+ }
2428
+ /**
2429
+ * Get a product by ID.
2430
+ */
2431
+ async getProduct(id) {
2432
+ return this.productsCollection.findById(id);
2433
+ }
2434
+ /**
2435
+ * Get a product by its URL slug.
2436
+ */
2437
+ async getProductBySlug(slug) {
2438
+ return this.productsCollection.findBySlug(slug);
2439
+ }
2440
+ /**
2441
+ * Delete a product by ID.
2442
+ */
2443
+ async deleteProduct(id) {
2444
+ const deleted = await this.productsCollection.delete(id);
2445
+ if (deleted) await this.engine.hooks.doAction("ecommerce.product_deleted", id);
2446
+ return deleted;
2447
+ }
2448
+ /**
2449
+ * List and filter catalog products.
2450
+ */
2451
+ async listProducts(query = {}) {
2452
+ let termIds;
2453
+ if (query.categorySlug) {
2454
+ const term = await this.engine.taxonomies.getTermBySlug(this.categoriesTaxonomy, query.categorySlug);
2455
+ if (term) termIds = [term.id];
2456
+ else return {
2457
+ items: [],
2458
+ total: 0,
2459
+ limit: query.limit ?? 20,
2460
+ offset: query.offset ?? 0,
2461
+ hasMore: false
2462
+ };
2463
+ } else if (query.categoryId) termIds = [query.categoryId];
2464
+ if (query.tagSlug) {
2465
+ const term = await this.engine.taxonomies.getTermBySlug(this.tagsTaxonomy, query.tagSlug);
2466
+ if (term) termIds = termIds ? [...termIds, term.id] : [term.id];
2467
+ }
2468
+ if (query.brandSlug) {
2469
+ const term = await this.engine.taxonomies.getTermBySlug(this.brandsTaxonomy, query.brandSlug);
2470
+ if (term) termIds = termIds ? [...termIds, term.id] : [term.id];
2471
+ }
2472
+ const contentStatus = query.status === "draft" ? "draft" : query.status === "published" ? "published" : void 0;
2473
+ const result = await this.productsCollection.find({
2474
+ search: query.search,
2475
+ termIds,
2476
+ status: contentStatus ?? "published",
2477
+ limit: query.limit ?? 20,
2478
+ offset: query.offset ?? 0,
2479
+ orderBy: query.orderBy === "price" || query.orderBy === "stock" ? void 0 : query.orderBy,
2480
+ orderDirection: query.orderDirection ?? "desc"
2481
+ });
2482
+ let items = result.items;
2483
+ if (query.status) {
2484
+ const statuses = Array.isArray(query.status) ? query.status : [query.status];
2485
+ items = items.filter((p) => statuses.includes(p.data.status));
2486
+ }
2487
+ if (query.minPrice !== void 0) items = items.filter((p) => p.data.price >= query.minPrice);
2488
+ if (query.maxPrice !== void 0) items = items.filter((p) => p.data.price <= query.maxPrice);
2489
+ if (query.inStock) items = items.filter((p) => !p.data.trackInventory || p.data.stock > 0);
2490
+ if (query.orderBy === "price") items.sort((a, b) => query.orderDirection === "asc" ? a.data.price - b.data.price : b.data.price - a.data.price);
2491
+ else if (query.orderBy === "stock") items.sort((a, b) => query.orderDirection === "asc" ? a.data.stock - b.data.stock : b.data.stock - a.data.stock);
2492
+ return {
2493
+ items,
2494
+ total: items.length,
2495
+ limit: result.limit,
2496
+ offset: result.offset,
2497
+ hasMore: result.offset + items.length < result.total
2498
+ };
2499
+ }
2500
+ /**
2501
+ * Upload and link a product image to its gallery and featured slot.
2502
+ */
2503
+ async uploadProductImage(productId, file, authorId) {
2504
+ const product = await this.getProduct(productId);
2505
+ if (!product) throw new Error(`[EcommerceService] Product '${productId}' not found.`);
2506
+ const media = await this.engine.media.upload({
2507
+ filename: file.filename,
2508
+ mimeType: file.mimeType,
2509
+ sizeBytes: file.sizeBytes,
2510
+ url: file.url,
2511
+ altText: file.altText ?? product.title,
2512
+ caption: file.caption,
2513
+ width: file.width,
2514
+ height: file.height
2515
+ }, authorId);
2516
+ const gallery = [...product.data.gallery || []];
2517
+ gallery.push({
2518
+ id: media.id,
2519
+ url: media.url,
2520
+ altText: media.altText,
2521
+ caption: media.caption,
2522
+ width: media.width,
2523
+ height: media.height
2524
+ });
2525
+ const isFeatured = file.isFeatured ?? !product.data.featuredImage;
2526
+ const updated = await this.productsCollection.update(productId, { data: {
2527
+ ...product.data,
2528
+ gallery,
2529
+ ...isFeatured ? { featuredImage: media.url } : {}
2530
+ } });
2531
+ await this.engine.hooks.doAction("ecommerce.product_image_added", {
2532
+ product: updated ?? product,
2533
+ media
2534
+ });
2535
+ return {
2536
+ media,
2537
+ product: updated ?? product
2538
+ };
2539
+ }
2540
+ /**
2541
+ * Adjust inventory stock for a product or variant.
2542
+ */
2543
+ async adjustStock(productId, delta, variantId) {
2544
+ const product = await this.getProduct(productId);
2545
+ if (!product) return null;
2546
+ if (!product.data.trackInventory) return product;
2547
+ let newStock = product.data.stock;
2548
+ const variants = [...product.data.variants || []];
2549
+ if (variantId) {
2550
+ const idx = variants.findIndex((v) => v.id === variantId);
2551
+ if (idx !== -1) {
2552
+ const currentVariantStock = variants[idx].stock ?? 0;
2553
+ const updatedVariantStock = Math.max(0, currentVariantStock + delta);
2554
+ variants[idx] = {
2555
+ ...variants[idx],
2556
+ stock: updatedVariantStock
2557
+ };
2558
+ }
2559
+ } else newStock = Math.max(0, product.data.stock + delta);
2560
+ const newStatus = newStock === 0 && product.data.status === "published" ? "out_of_stock" : product.data.status === "out_of_stock" && newStock > 0 ? "published" : product.data.status;
2561
+ const updated = await this.productsCollection.update(productId, { data: {
2562
+ ...product.data,
2563
+ stock: newStock,
2564
+ variants,
2565
+ status: newStatus
2566
+ } });
2567
+ await this.engine.hooks.doAction("ecommerce.stock_changed", {
2568
+ product: updated ?? product,
2569
+ delta,
2570
+ variantId,
2571
+ oldStock: product.data.stock,
2572
+ newStock
2573
+ });
2574
+ return updated;
2575
+ }
2576
+ /**
2577
+ * Create a catalog category in the hierarchical category taxonomy.
2578
+ */
2579
+ async createCategory(input) {
2580
+ return this.engine.taxonomies.createTerm(this.categoriesTaxonomy, input);
2581
+ }
2582
+ /**
2583
+ * Get all catalog categories.
2584
+ */
2585
+ async getCategories(options) {
2586
+ return this.engine.taxonomies.getTerms(this.categoriesTaxonomy, options);
2587
+ }
2588
+ /**
2589
+ * Get full hierarchical catalog category tree.
2590
+ */
2591
+ async getCategoryTree() {
2592
+ return this.engine.taxonomies.getTermTree(this.categoriesTaxonomy);
2593
+ }
2594
+ /**
2595
+ * Assign category IDs to a product.
2596
+ */
2597
+ async assignProductCategory(productId, categoryIds) {
2598
+ const ids = Array.isArray(categoryIds) ? categoryIds : [categoryIds];
2599
+ await this.engine.taxonomies.assignTerms(productId, ids);
2600
+ }
2601
+ /**
2602
+ * Get assigned categories for a product.
2603
+ */
2604
+ async getProductCategories(productId) {
2605
+ return this.engine.taxonomies.getContentTerms(productId, this.categoriesTaxonomy);
2606
+ }
2607
+ /**
2608
+ * Create a promotional coupon / discount code.
2609
+ */
2610
+ async createDiscount(input, authorId) {
2611
+ const normalizedCode = input.code.trim().toUpperCase();
2612
+ const discountData = {
2613
+ code: normalizedCode,
2614
+ discountType: input.discountType,
2615
+ value: input.value,
2616
+ minOrderAmount: input.minOrderAmount,
2617
+ maxDiscountAmount: input.maxDiscountAmount,
2618
+ maxUses: input.maxUses,
2619
+ usedCount: 0,
2620
+ startDate: input.startDate,
2621
+ endDate: input.endDate,
2622
+ status: input.status ?? "active",
2623
+ appliesToProductIds: input.appliesToProductIds ?? [],
2624
+ appliesToCategoryIds: input.appliesToCategoryIds ?? []
2625
+ };
2626
+ const discount = await this.discountsCollection.create({
2627
+ title: input.title,
2628
+ slug: normalizedCode.toLowerCase(),
2629
+ status: "published",
2630
+ data: discountData
2631
+ }, authorId);
2632
+ await this.engine.hooks.doAction("ecommerce.discount_created", discount);
2633
+ return discount;
2634
+ }
2635
+ /**
2636
+ * Find a discount code.
2637
+ */
2638
+ async getDiscountByCode(code) {
2639
+ const normalized = code.trim().toUpperCase();
2640
+ return (await this.discountsCollection.find({ limit: 100 })).items.find((d) => d.data.code?.toUpperCase() === normalized) ?? null;
2641
+ }
2642
+ /**
2643
+ * Validate a discount coupon against cart items and order subtotal.
2644
+ */
2645
+ async validateDiscount(code, cartSubtotal, productIds = []) {
2646
+ const normalized = code.trim().toUpperCase();
2647
+ const discount = await this.getDiscountByCode(normalized);
2648
+ if (!discount) return {
2649
+ valid: false,
2650
+ code: normalized,
2651
+ discountAmount: 0,
2652
+ message: `Discount code '${code}' not found.`
2653
+ };
2654
+ const { data } = discount;
2655
+ if (data.status !== "active") return {
2656
+ valid: false,
2657
+ code: normalized,
2658
+ discountAmount: 0,
2659
+ message: "Discount code is currently inactive."
2660
+ };
2661
+ const now = Date.now();
2662
+ if (data.startDate && new Date(data.startDate).getTime() > now) return {
2663
+ valid: false,
2664
+ code: normalized,
2665
+ discountAmount: 0,
2666
+ message: "Discount code is not yet active."
2667
+ };
2668
+ if (data.endDate && new Date(data.endDate).getTime() < now) return {
2669
+ valid: false,
2670
+ code: normalized,
2671
+ discountAmount: 0,
2672
+ message: "Discount code has expired."
2673
+ };
2674
+ if (data.maxUses !== void 0 && data.usedCount >= data.maxUses) return {
2675
+ valid: false,
2676
+ code: normalized,
2677
+ discountAmount: 0,
2678
+ message: "Discount code usage limit reached."
2679
+ };
2680
+ if (data.minOrderAmount !== void 0 && cartSubtotal < data.minOrderAmount) return {
2681
+ valid: false,
2682
+ code: normalized,
2683
+ discountAmount: 0,
2684
+ message: `Minimum order amount of ${data.minOrderAmount} required for this coupon.`
2685
+ };
2686
+ if (data.appliesToProductIds && data.appliesToProductIds.length > 0 && productIds.length > 0) {
2687
+ if (!productIds.some((id) => data.appliesToProductIds?.includes(id))) return {
2688
+ valid: false,
2689
+ code: normalized,
2690
+ discountAmount: 0,
2691
+ message: "Coupon is not applicable to any products in your cart."
2692
+ };
2693
+ }
2694
+ let discountAmount = 0;
2695
+ if (data.discountType === "percentage") {
2696
+ discountAmount = cartSubtotal * data.value / 100;
2697
+ if (data.maxDiscountAmount) discountAmount = Math.min(discountAmount, data.maxDiscountAmount);
2698
+ } else if (data.discountType === "fixed_amount") discountAmount = Math.min(data.value, cartSubtotal);
2699
+ else if (data.discountType === "free_shipping") discountAmount = 0;
2700
+ discountAmount = await this.engine.hooks.applyFilters("ecommerce.apply_discount", discountAmount, {
2701
+ discount,
2702
+ cartSubtotal
2703
+ });
2704
+ return {
2705
+ valid: true,
2706
+ code: normalized,
2707
+ discountAmount: Math.round(discountAmount * 100) / 100,
2708
+ discountType: data.discountType,
2709
+ discount
2710
+ };
2711
+ }
2712
+ /**
2713
+ * Calculate cart subtotals, apply discounts, shipping, and taxes.
2714
+ */
2715
+ async calculateCart(input) {
2716
+ const lineItems = [];
2717
+ let subtotal = 0;
2718
+ const productIds = [];
2719
+ for (const item of input.items) {
2720
+ const product = await this.getProduct(item.productId);
2721
+ if (!product) throw new Error(`[EcommerceService] Product '${item.productId}' not found.`);
2722
+ productIds.push(product.id);
2723
+ let price = product.data.price;
2724
+ let sku = product.data.sku;
2725
+ let title = product.title ?? "Product";
2726
+ let availableStock = product.data.stock;
2727
+ let image = product.data.featuredImage;
2728
+ if (item.variantId && product.data.variants) {
2729
+ const variant = product.data.variants.find((v) => v.id === item.variantId);
2730
+ if (variant) {
2731
+ price = variant.price ?? price;
2732
+ sku = variant.sku ?? sku;
2733
+ title = `${title} (${variant.title})`;
2734
+ availableStock = variant.stock ?? availableStock;
2735
+ image = variant.image ?? image;
2736
+ }
2737
+ }
2738
+ if (this.inventoryManagement && product.data.trackInventory && availableStock < item.quantity) throw new Error(`[EcommerceService] Insufficient stock for '${title}'. Available: ${availableStock}, Requested: ${item.quantity}.`);
2739
+ const itemSubtotal = Math.round(price * item.quantity * 100) / 100;
2740
+ subtotal += itemSubtotal;
2741
+ lineItems.push({
2742
+ productId: product.id,
2743
+ variantId: item.variantId,
2744
+ title,
2745
+ sku,
2746
+ price,
2747
+ quantity: item.quantity,
2748
+ subtotal: itemSubtotal,
2749
+ image
2750
+ });
2751
+ }
2752
+ subtotal = Math.round(subtotal * 100) / 100;
2753
+ let discountTotal = 0;
2754
+ let discountType;
2755
+ if (input.discountCode) {
2756
+ const validation = await this.validateDiscount(input.discountCode, subtotal, productIds);
2757
+ if (validation.valid) {
2758
+ discountTotal = validation.discountAmount;
2759
+ discountType = validation.discountType;
2760
+ }
2761
+ }
2762
+ let shippingTotal = input.shippingCost ?? this.options.defaultShippingCost ?? 0;
2763
+ if (discountType === "free_shipping") shippingTotal = 0;
2764
+ const taxRate = input.taxRate ?? this.options.defaultTaxRate ?? 0;
2765
+ const taxableAmount = Math.max(0, subtotal - discountTotal);
2766
+ const taxTotal = Math.round(taxableAmount * taxRate * 100) / 100;
2767
+ const total = Math.round((taxableAmount + shippingTotal + taxTotal) * 100) / 100;
2768
+ const result = {
2769
+ items: lineItems,
2770
+ subtotal,
2771
+ discountTotal,
2772
+ discountCode: input.discountCode,
2773
+ shippingTotal,
2774
+ taxTotal,
2775
+ total,
2776
+ currency: this.defaultCurrency
2777
+ };
2778
+ return this.engine.hooks.applyFilters("ecommerce.calculate_totals", result, input);
2779
+ }
2780
+ /**
2781
+ * Place a new order with cart validation, inventory deduction, and coupon counter updates.
2782
+ */
2783
+ async createOrder(input, authorId) {
2784
+ const calc = await this.calculateCart({
2785
+ items: input.items,
2786
+ discountCode: input.discountCode,
2787
+ shippingCost: input.shippingCost,
2788
+ taxRate: input.taxRate
2789
+ });
2790
+ const orderNumber = `ORD-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "")}-${Math.random().toString(36).substring(2, 6).toUpperCase()}`;
2791
+ if (this.inventoryManagement) for (const item of input.items) await this.adjustStock(item.productId, -item.quantity, item.variantId);
2792
+ if (input.discountCode) {
2793
+ const discount = await this.getDiscountByCode(input.discountCode);
2794
+ if (discount) await this.discountsCollection.update(discount.id, { data: {
2795
+ ...discount.data,
2796
+ usedCount: (discount.data.usedCount || 0) + 1
2797
+ } });
2798
+ }
2799
+ const orderData = {
2800
+ orderNumber,
2801
+ customerEmail: input.customerEmail,
2802
+ customerName: input.customerName,
2803
+ status: "pending",
2804
+ currency: calc.currency,
2805
+ items: calc.items,
2806
+ subtotal: calc.subtotal,
2807
+ discountTotal: calc.discountTotal,
2808
+ discountCode: calc.discountCode,
2809
+ shippingTotal: calc.shippingTotal,
2810
+ taxTotal: calc.taxTotal,
2811
+ total: calc.total,
2812
+ shippingAddress: input.shippingAddress,
2813
+ billingAddress: input.billingAddress,
2814
+ paymentMethod: input.paymentMethod,
2815
+ notes: input.notes
2816
+ };
2817
+ const order = await this.ordersCollection.create({
2818
+ title: `Order #${orderNumber}`,
2819
+ slug: orderNumber.toLowerCase(),
2820
+ status: "published",
2821
+ data: orderData
2822
+ }, authorId);
2823
+ await this.engine.hooks.doAction("ecommerce.order_created", order);
2824
+ return order;
2825
+ }
2826
+ /**
2827
+ * Get order by ID.
2828
+ */
2829
+ async getOrder(id) {
2830
+ return this.ordersCollection.findById(id);
2831
+ }
2832
+ /**
2833
+ * Get order by order number.
2834
+ */
2835
+ async getOrderByNumber(orderNumber) {
2836
+ return (await this.ordersCollection.find({ limit: 100 })).items.find((o) => o.data.orderNumber?.toUpperCase() === orderNumber.toUpperCase()) ?? null;
2837
+ }
2838
+ /**
2839
+ * Update the status of an order (e.g. pending -> paid -> shipped).
2840
+ */
2841
+ async updateOrderStatus(id, status, note) {
2842
+ const existing = await this.getOrder(id);
2843
+ if (!existing) return null;
2844
+ const oldStatus = existing.data.status;
2845
+ const updated = await this.ordersCollection.update(id, { data: {
2846
+ ...existing.data,
2847
+ status
2848
+ } }, void 0, note);
2849
+ if (updated) await this.engine.hooks.doAction("ecommerce.order_status_changed", {
2850
+ order: updated,
2851
+ oldStatus,
2852
+ newStatus: status,
2853
+ note
2854
+ });
2855
+ return updated;
2856
+ }
2857
+ };
2858
+ //#endregion
2859
+ //#region src/plugins/ecommerce/routes.ts
2860
+ function json(data, status = 200) {
2861
+ return new Response(JSON.stringify(data), {
2862
+ status,
2863
+ headers: {
2864
+ "Content-Type": "application/json",
2865
+ "Access-Control-Allow-Origin": "*"
2866
+ }
2867
+ });
2868
+ }
2869
+ function badRequest(message) {
2870
+ return json({ error: message }, 400);
2871
+ }
2872
+ function notFound(message) {
2873
+ return json({ error: message }, 404);
2874
+ }
2875
+ function registerEcommerceRoutes(ctx, service, options = {}) {
2876
+ const prefix = (options.apiPrefix ?? "/api/ecommerce").replace(/\/+$/, "");
2877
+ ctx.registerRoute("GET", `${prefix}/products`, async (_req, { url }) => {
2878
+ try {
2879
+ const categorySlug = url.searchParams.get("category") ?? void 0;
2880
+ const categoryId = url.searchParams.get("categoryId") ?? void 0;
2881
+ const tagSlug = url.searchParams.get("tag") ?? void 0;
2882
+ const brandSlug = url.searchParams.get("brand") ?? void 0;
2883
+ const search = url.searchParams.get("search") ?? void 0;
2884
+ const statusParam = url.searchParams.get("status");
2885
+ const inStock = url.searchParams.get("inStock") === "true";
2886
+ const minPriceStr = url.searchParams.get("minPrice");
2887
+ const maxPriceStr = url.searchParams.get("maxPrice");
2888
+ const minPrice = minPriceStr ? parseFloat(minPriceStr) : void 0;
2889
+ const maxPrice = maxPriceStr ? parseFloat(maxPriceStr) : void 0;
2890
+ const orderBy = url.searchParams.get("orderBy");
2891
+ const orderDirection = url.searchParams.get("orderDirection") ?? "desc";
2892
+ const limitStr = url.searchParams.get("limit");
2893
+ const offsetStr = url.searchParams.get("offset");
2894
+ const limit = limitStr ? parseInt(limitStr, 10) : 20;
2895
+ const offset = offsetStr ? parseInt(offsetStr, 10) : 0;
2896
+ return json(await service.listProducts({
2897
+ categorySlug,
2898
+ categoryId,
2899
+ tagSlug,
2900
+ brandSlug,
2901
+ search,
2902
+ status: statusParam,
2903
+ inStock,
2904
+ minPrice,
2905
+ maxPrice,
2906
+ orderBy,
2907
+ orderDirection,
2908
+ limit,
2909
+ offset
2910
+ }));
2911
+ } catch (err) {
2912
+ return badRequest(err instanceof Error ? err.message : String(err));
2913
+ }
2914
+ });
2915
+ ctx.registerRoute("GET", `${prefix}/products/:id`, async (_req, { params, url }) => {
2916
+ const id = params.id;
2917
+ const by = url.searchParams.get("by");
2918
+ let product = null;
2919
+ if (by === "slug") product = await service.getProductBySlug(id);
2920
+ else {
2921
+ product = await service.getProduct(id);
2922
+ if (!product) product = await service.getProductBySlug(id);
2923
+ }
2924
+ if (!product) return notFound(`Product '${id}' not found`);
2925
+ return json(product);
2926
+ });
2927
+ ctx.registerRoute("POST", `${prefix}/products`, async (req) => {
2928
+ try {
2929
+ const body = await req.json();
2930
+ if (!body.title) return badRequest("Product 'title' is required.");
2931
+ if (body.price === void 0 || body.price < 0) return badRequest("Valid product 'price' is required.");
2932
+ return json(await service.createProduct(body), 201);
2933
+ } catch (err) {
2934
+ return badRequest(err instanceof Error ? err.message : String(err));
2935
+ }
2936
+ });
2937
+ ctx.registerRoute("PUT", `${prefix}/products/:id`, async (req, { params }) => {
2938
+ try {
2939
+ const body = await req.json();
2940
+ const updated = await service.updateProduct(params.id, body);
2941
+ if (!updated) return notFound(`Product '${params.id}' not found.`);
2942
+ return json(updated);
2943
+ } catch (err) {
2944
+ return badRequest(err instanceof Error ? err.message : String(err));
2945
+ }
2946
+ });
2947
+ ctx.registerRoute("DELETE", `${prefix}/products/:id`, async (_req, { params }) => {
2948
+ if (!await service.deleteProduct(params.id)) return notFound(`Product '${params.id}' not found.`);
2949
+ return json({
2950
+ success: true,
2951
+ id: params.id
2952
+ });
2953
+ });
2954
+ ctx.registerRoute("POST", `${prefix}/products/:id/images`, async (req, { params }) => {
2955
+ try {
2956
+ const body = await req.json();
2957
+ if (!body.filename || !body.mimeType) return badRequest("'filename' and 'mimeType' are required.");
2958
+ return json(await service.uploadProductImage(params.id, {
2959
+ filename: body.filename,
2960
+ mimeType: body.mimeType,
2961
+ sizeBytes: body.sizeBytes ?? 0,
2962
+ url: body.url,
2963
+ altText: body.altText,
2964
+ caption: body.caption,
2965
+ width: body.width,
2966
+ height: body.height,
2967
+ isFeatured: body.isFeatured
2968
+ }), 201);
2969
+ } catch (err) {
2970
+ return badRequest(err instanceof Error ? err.message : String(err));
2971
+ }
2972
+ });
2973
+ ctx.registerRoute("GET", `${prefix}/categories`, async (_req, { url }) => {
2974
+ try {
2975
+ if (url.searchParams.get("tree") === "true") return json(await service.getCategoryTree());
2976
+ const parentId = url.searchParams.get("parentId");
2977
+ return json(await service.getCategories({ parentId: parentId === "null" ? null : parentId ?? void 0 }));
2978
+ } catch (err) {
2979
+ return badRequest(err instanceof Error ? err.message : String(err));
2980
+ }
2981
+ });
2982
+ ctx.registerRoute("POST", `${prefix}/categories`, async (req) => {
2983
+ try {
2984
+ const body = await req.json();
2985
+ if (!body.name) return badRequest("Category 'name' is required.");
2986
+ return json(await service.createCategory(body), 201);
2987
+ } catch (err) {
2988
+ return badRequest(err instanceof Error ? err.message : String(err));
2989
+ }
2990
+ });
2991
+ if (options.enableDiscounts !== false) {
2992
+ ctx.registerRoute("POST", `${prefix}/discounts`, async (req) => {
2993
+ try {
2994
+ const body = await req.json();
2995
+ if (!body.title || !body.code) return badRequest("'title' and 'code' are required.");
2996
+ if (body.value === void 0 || body.value < 0) return badRequest("Valid discount 'value' is required.");
2997
+ return json(await service.createDiscount(body), 201);
2998
+ } catch (err) {
2999
+ return badRequest(err instanceof Error ? err.message : String(err));
3000
+ }
3001
+ });
3002
+ ctx.registerRoute("POST", `${prefix}/discounts/validate`, async (req) => {
3003
+ try {
3004
+ const body = await req.json();
3005
+ if (!body.code) return badRequest("Discount 'code' is required.");
3006
+ const subtotal = Number(body.subtotal ?? 0);
3007
+ const productIds = Array.isArray(body.productIds) ? body.productIds : [];
3008
+ return json(await service.validateDiscount(body.code, subtotal, productIds));
3009
+ } catch (err) {
3010
+ return badRequest(err instanceof Error ? err.message : String(err));
3011
+ }
3012
+ });
3013
+ }
3014
+ ctx.registerRoute("POST", `${prefix}/cart/calculate`, async (req) => {
3015
+ try {
3016
+ const body = await req.json();
3017
+ if (!body.items || !Array.isArray(body.items) || body.items.length === 0) return badRequest("'items' array is required and must not be empty.");
3018
+ return json(await service.calculateCart(body));
3019
+ } catch (err) {
3020
+ return badRequest(err instanceof Error ? err.message : String(err));
3021
+ }
3022
+ });
3023
+ if (options.enableOrders !== false) {
3024
+ ctx.registerRoute("POST", `${prefix}/orders`, async (req) => {
3025
+ try {
3026
+ const body = await req.json();
3027
+ if (!body.customerEmail) return badRequest("'customerEmail' is required.");
3028
+ if (!body.items || !Array.isArray(body.items) || body.items.length === 0) return badRequest("'items' array is required.");
3029
+ return json(await service.createOrder(body), 201);
3030
+ } catch (err) {
3031
+ return badRequest(err instanceof Error ? err.message : String(err));
3032
+ }
3033
+ });
3034
+ ctx.registerRoute("GET", `${prefix}/orders/:id`, async (_req, { params, url }) => {
3035
+ const id = params.id;
3036
+ const by = url.searchParams.get("by");
3037
+ let order = null;
3038
+ if (by === "number") order = await service.getOrderByNumber(id);
3039
+ else {
3040
+ order = await service.getOrder(id);
3041
+ if (!order) order = await service.getOrderByNumber(id);
3042
+ }
3043
+ if (!order) return notFound(`Order '${id}' not found.`);
3044
+ return json(order);
3045
+ });
3046
+ ctx.registerRoute("PATCH", `${prefix}/orders/:id/status`, async (req, { params }) => {
3047
+ try {
3048
+ const body = await req.json();
3049
+ if (!body.status) return badRequest("New 'status' is required.");
3050
+ const updated = await service.updateOrderStatus(params.id, body.status, body.note);
3051
+ if (!updated) return notFound(`Order '${params.id}' not found.`);
3052
+ return json(updated);
3053
+ } catch (err) {
3054
+ return badRequest(err instanceof Error ? err.message : String(err));
3055
+ }
3056
+ });
3057
+ }
3058
+ }
3059
+ //#endregion
3060
+ //#region src/plugins/ecommerce/client.ts
3061
+ var EcommerceClient = class {
3062
+ client;
3063
+ options;
3064
+ service;
3065
+ prefix;
3066
+ constructor(client, options = {}) {
3067
+ this.client = client;
3068
+ this.options = options;
3069
+ this.prefix = (options.apiPrefix ?? "/api/ecommerce").replace(/\/+$/, "");
3070
+ const engine = client.getEngine();
3071
+ if (engine) this.service = new EcommerceService(engine, options);
3072
+ }
3073
+ products = {
3074
+ find: async (query = {}) => {
3075
+ if (this.service) return this.service.listProducts(query);
3076
+ const params = new URLSearchParams();
3077
+ if (query.categorySlug) params.set("category", query.categorySlug);
3078
+ if (query.categoryId) params.set("categoryId", query.categoryId);
3079
+ if (query.tagSlug) params.set("tag", query.tagSlug);
3080
+ if (query.brandSlug) params.set("brand", query.brandSlug);
3081
+ if (query.search) params.set("search", query.search);
3082
+ if (query.inStock) params.set("inStock", "true");
3083
+ if (query.minPrice !== void 0) params.set("minPrice", String(query.minPrice));
3084
+ if (query.maxPrice !== void 0) params.set("maxPrice", String(query.maxPrice));
3085
+ if (query.orderBy) params.set("orderBy", query.orderBy);
3086
+ if (query.orderDirection) params.set("orderDirection", query.orderDirection);
3087
+ if (query.limit !== void 0) params.set("limit", String(query.limit));
3088
+ if (query.offset !== void 0) params.set("offset", String(query.offset));
3089
+ const q = params.toString() ? `?${params.toString()}` : "";
3090
+ return this.client.request(`${this.prefix}/products${q}`);
3091
+ },
3092
+ get: async (idOrSlug, by) => {
3093
+ if (this.service) return by === "slug" ? this.service.getProductBySlug(idOrSlug) : this.service.getProduct(idOrSlug);
3094
+ const query = by ? `?by=${by}` : "";
3095
+ return this.client.request(`${this.prefix}/products/${encodeURIComponent(idOrSlug)}${query}`);
3096
+ },
3097
+ create: async (data) => {
3098
+ if (this.service) return this.service.createProduct(data);
3099
+ return this.client.request(`${this.prefix}/products`, {
3100
+ method: "POST",
3101
+ body: JSON.stringify(data)
3102
+ });
3103
+ },
3104
+ update: async (id, data) => {
3105
+ if (this.service) return this.service.updateProduct(id, data);
3106
+ return this.client.request(`${this.prefix}/products/${encodeURIComponent(id)}`, {
3107
+ method: "PUT",
3108
+ body: JSON.stringify(data)
3109
+ });
3110
+ },
3111
+ delete: async (id) => {
3112
+ if (this.service) return this.service.deleteProduct(id);
3113
+ const res = await this.client.request(`${this.prefix}/products/${encodeURIComponent(id)}`, { method: "DELETE" });
3114
+ return Boolean(res?.success);
3115
+ },
3116
+ uploadImage: async (productId, file) => {
3117
+ if (this.service) return this.service.uploadProductImage(productId, file);
3118
+ return this.client.request(`${this.prefix}/products/${encodeURIComponent(productId)}/images`, {
3119
+ method: "POST",
3120
+ body: JSON.stringify(file)
3121
+ });
3122
+ }
3123
+ };
3124
+ categories = {
3125
+ list: async (options) => {
3126
+ if (this.service) return this.service.getCategories(options);
3127
+ const q = options?.parentId !== void 0 ? `?parentId=${options.parentId}` : "";
3128
+ return this.client.request(`${this.prefix}/categories${q}`);
3129
+ },
3130
+ tree: async () => {
3131
+ if (this.service) return this.service.getCategoryTree();
3132
+ return this.client.request(`${this.prefix}/categories?tree=true`);
3133
+ },
3134
+ create: async (input) => {
3135
+ if (this.service) return this.service.createCategory(input);
3136
+ return this.client.request(`${this.prefix}/categories`, {
3137
+ method: "POST",
3138
+ body: JSON.stringify(input)
3139
+ });
3140
+ }
3141
+ };
3142
+ discounts = {
3143
+ validate: async (code, subtotal, productIds) => {
3144
+ if (this.service) return this.service.validateDiscount(code, subtotal, productIds);
3145
+ return this.client.request(`${this.prefix}/discounts/validate`, {
3146
+ method: "POST",
3147
+ body: JSON.stringify({
3148
+ code,
3149
+ subtotal,
3150
+ productIds
3151
+ })
3152
+ });
3153
+ },
3154
+ create: async (input) => {
3155
+ if (this.service) return this.service.createDiscount(input);
3156
+ return this.client.request(`${this.prefix}/discounts`, {
3157
+ method: "POST",
3158
+ body: JSON.stringify(input)
3159
+ });
3160
+ }
3161
+ };
3162
+ cart = { calculate: async (input) => {
3163
+ if (this.service) return this.service.calculateCart(input);
3164
+ return this.client.request(`${this.prefix}/cart/calculate`, {
3165
+ method: "POST",
3166
+ body: JSON.stringify(input)
3167
+ });
3168
+ } };
3169
+ orders = {
3170
+ create: async (input) => {
3171
+ if (this.service) return this.service.createOrder(input);
3172
+ return this.client.request(`${this.prefix}/orders`, {
3173
+ method: "POST",
3174
+ body: JSON.stringify(input)
3175
+ });
3176
+ },
3177
+ get: async (idOrNumber, by) => {
3178
+ if (this.service) return by === "number" ? this.service.getOrderByNumber(idOrNumber) : this.service.getOrder(idOrNumber);
3179
+ const q = by ? `?by=${by}` : "";
3180
+ return this.client.request(`${this.prefix}/orders/${encodeURIComponent(idOrNumber)}${q}`);
3181
+ },
3182
+ updateStatus: async (id, status, note) => {
3183
+ if (this.service) return this.service.updateOrderStatus(id, status, note);
3184
+ return this.client.request(`${this.prefix}/orders/${encodeURIComponent(id)}/status`, {
3185
+ method: "PATCH",
3186
+ body: JSON.stringify({
3187
+ status,
3188
+ note
3189
+ })
3190
+ });
3191
+ }
3192
+ };
3193
+ };
3194
+ /**
3195
+ * Get or create an EcommerceClient adapter for a CMSClient.
3196
+ */
3197
+ function getEcommerceClient(client, options) {
3198
+ return new EcommerceClient(client, options);
3199
+ }
3200
+ //#endregion
3201
+ //#region src/plugins/ecommerce/index.ts
3202
+ /**
3203
+ * @azlib/cms - Built-in E-commerce Plugin
3204
+ */
3205
+ /**
3206
+ * Built-in E-commerce plugin factory for @azlib/cms.
3207
+ * Equips the CMS engine with product catalogs, hierarchical categories,
3208
+ * image uploading, discount coupons, cart calculation, and order tracking.
3209
+ */
3210
+ const ecommercePlugin = definePlugin((options) => {
3211
+ const opts = options || {};
3212
+ const collections = [createProductCollection(opts)];
3213
+ if (opts.enableDiscounts !== false) collections.push(createDiscountCollection(opts));
3214
+ if (opts.enableOrders !== false) collections.push(createOrderCollection(opts));
3215
+ return {
3216
+ name: "ecommerce",
3217
+ version: "1.0.0",
3218
+ description: "Built-in E-commerce shopping, products, catalogs, discounts, and orders plugin",
3219
+ collections,
3220
+ taxonomies: createEcommerceTaxonomies(opts),
3221
+ setup(ctx) {
3222
+ const service = new EcommerceService(ctx.engine, opts);
3223
+ ctx.engine.__ecommerceService = service;
3224
+ registerEcommerceRoutes(ctx, service, opts);
3225
+ }
3226
+ };
3227
+ });
3228
+ /**
3229
+ * Retrieve the active EcommerceService instance associated with a CMSEngine.
3230
+ */
3231
+ function getEcommerceService(engine, options) {
3232
+ if (engine.__ecommerceService) return engine.__ecommerceService;
3233
+ const service = new EcommerceService(engine, options);
3234
+ engine.__ecommerceService = service;
3235
+ return service;
3236
+ }
3237
+ //#endregion
1950
3238
  exports.CMSClient = CMSClient;
1951
3239
  exports.CMSEngine = CMSEngine;
1952
3240
  exports.CMSRouter = CMSRouter;
1953
3241
  exports.ContentLifecycle = ContentLifecycle;
1954
3242
  exports.DEFAULT_COLLECTIONS = DEFAULT_COLLECTIONS;
1955
3243
  exports.DEFAULT_ROLE_CAPABILITIES = DEFAULT_ROLE_CAPABILITIES;
3244
+ exports.EcommerceClient = EcommerceClient;
3245
+ exports.EcommerceService = EcommerceService;
1956
3246
  exports.HooksManager = HooksManager;
1957
3247
  exports.MediaManager = MediaManager;
1958
3248
  exports.MemoryStorageAdapter = MemoryStorageAdapter;
@@ -1965,10 +3255,17 @@ exports.collection = collection;
1965
3255
  exports.createCMSEngine = createCMSEngine;
1966
3256
  exports.createCMSRouter = createCMSRouter;
1967
3257
  exports.createCmsClient = createCmsClient;
3258
+ exports.createDiscountCollection = createDiscountCollection;
3259
+ exports.createEcommerceTaxonomies = createEcommerceTaxonomies;
3260
+ exports.createOrderCollection = createOrderCollection;
3261
+ exports.createProductCollection = createProductCollection;
1968
3262
  exports.defaultHooks = defaultHooks;
1969
3263
  exports.defineConfig = defineConfig;
1970
3264
  exports.definePlugin = definePlugin;
3265
+ exports.ecommercePlugin = ecommercePlugin;
1971
3266
  exports.fields = fields;
3267
+ exports.getEcommerceClient = getEcommerceClient;
3268
+ exports.getEcommerceService = getEcommerceService;
1972
3269
  exports.normalizeConfig = normalizeConfig;
1973
3270
  exports.resolveUniqueSlug = resolveUniqueSlug;
1974
3271
  exports.slugify = slugify;