@webbycrown/webbycommerce 1.2.0 → 2.0.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.
Files changed (162) hide show
  1. package/README.md +26 -3
  2. package/admin/app.js +3 -0
  3. package/admin/jsconfig.json +20 -0
  4. package/admin/src/components/ApiCollectionsContent.jsx +4626 -0
  5. package/admin/src/components/CompareContent.jsx +300 -0
  6. package/admin/src/components/ConfigureContent.jsx +407 -0
  7. package/admin/src/components/Initializer.jsx +64 -0
  8. package/admin/src/components/LoginRegisterContent.jsx +280 -0
  9. package/admin/src/components/PluginIcon.jsx +6 -0
  10. package/admin/src/components/ShippingTypeContent.jsx +230 -0
  11. package/admin/src/components/SmtpContent.jsx +316 -0
  12. package/admin/src/components/WishlistContent.jsx +273 -0
  13. package/admin/src/index.js +81 -0
  14. package/admin/src/pages/ApiCollections.jsx +169 -0
  15. package/admin/src/pages/Configure.jsx +55 -0
  16. package/admin/src/pages/Settings.jsx +93 -0
  17. package/admin/src/pluginId.js +4 -0
  18. package/{dist/_chunks/en-CiQ97iC8.js → admin/src/translations/en.json} +712 -574
  19. package/bin/setup.js +50 -3
  20. package/package.json +14 -13
  21. package/server/bootstrap.js +3 -0
  22. package/server/register.js +3 -0
  23. package/server/src/bootstrap.js +3826 -0
  24. package/server/src/components/content-block.json +37 -0
  25. package/server/src/components/shipping-zone-location.json +27 -0
  26. package/server/src/config/index.js +7 -0
  27. package/server/src/content-types/address/index.js +7 -0
  28. package/server/src/content-types/address/schema.json +74 -0
  29. package/server/src/content-types/cart/index.js +61 -0
  30. package/server/src/content-types/cart-item/index.js +79 -0
  31. package/server/src/content-types/compare.js +73 -0
  32. package/server/src/content-types/coupon/index.js +7 -0
  33. package/server/src/content-types/coupon/schema.json +67 -0
  34. package/server/src/content-types/index.js +42 -0
  35. package/server/src/content-types/order/index.js +7 -0
  36. package/server/src/content-types/order/schema.json +121 -0
  37. package/server/src/content-types/payment-transaction/index.js +7 -0
  38. package/server/src/content-types/payment-transaction/schema.json +73 -0
  39. package/server/src/content-types/product/index.js +7 -0
  40. package/server/src/content-types/product/schema.json +104 -0
  41. package/server/src/content-types/product-attribute/index.js +7 -0
  42. package/server/src/content-types/product-attribute/schema.json +80 -0
  43. package/server/src/content-types/product-attribute-value/index.js +7 -0
  44. package/server/src/content-types/product-attribute-value/schema.json +52 -0
  45. package/server/src/content-types/product-category/index.js +7 -0
  46. package/server/src/content-types/product-category/schema.json +54 -0
  47. package/server/src/content-types/product-tag/index.js +7 -0
  48. package/server/src/content-types/product-tag/schema.json +38 -0
  49. package/server/src/content-types/product-variation/index.js +7 -0
  50. package/server/src/content-types/product-variation/schema.json +74 -0
  51. package/server/src/content-types/shipping-method/index.js +7 -0
  52. package/server/src/content-types/shipping-method/schema.json +91 -0
  53. package/server/src/content-types/shipping-rate/index.js +7 -0
  54. package/server/src/content-types/shipping-rate/schema.json +73 -0
  55. package/server/src/content-types/shipping-rule/index.js +7 -0
  56. package/server/src/content-types/shipping-rule/schema.json +84 -0
  57. package/server/src/content-types/shipping-zone/index.js +7 -0
  58. package/server/src/content-types/shipping-zone/schema.json +57 -0
  59. package/server/src/content-types/wishlist.js +66 -0
  60. package/server/src/controllers/address.js +374 -0
  61. package/server/src/controllers/auth.js +1409 -0
  62. package/server/src/controllers/cart.js +337 -0
  63. package/server/src/controllers/category.js +388 -0
  64. package/server/src/controllers/compare.js +246 -0
  65. package/server/src/controllers/controller.js +168 -0
  66. package/server/src/controllers/ecommerce.js +20 -0
  67. package/server/src/controllers/index.js +34 -0
  68. package/server/src/controllers/order.js +1100 -0
  69. package/server/src/controllers/payment.js +243 -0
  70. package/server/src/controllers/product.js +1006 -0
  71. package/server/src/controllers/productTag.js +370 -0
  72. package/server/src/controllers/productVariation.js +181 -0
  73. package/server/src/controllers/shipping.js +1046 -0
  74. package/server/src/controllers/wishlist.js +332 -0
  75. package/server/src/destroy.js +6 -0
  76. package/server/src/index.js +26 -0
  77. package/server/src/middlewares/index.js +4 -0
  78. package/server/src/policies/index.js +4 -0
  79. package/server/src/register.js +67 -0
  80. package/server/src/routes/index.js +1130 -0
  81. package/server/src/services/cart.js +531 -0
  82. package/server/src/services/compare.js +300 -0
  83. package/server/src/services/index.js +16 -0
  84. package/server/src/services/service.js +19 -0
  85. package/server/src/services/shipping.js +513 -0
  86. package/server/src/services/wishlist.js +238 -0
  87. package/server/src/utils/check-ecommerce-permission.js +204 -0
  88. package/server/src/utils/extend-user-schema.js +161 -0
  89. package/server/src/utils/seed-data.js +639 -0
  90. package/server/src/utils/send-email.js +98 -0
  91. package/strapi-server.js +1 -6
  92. package/dist/_chunks/Settings-DZXAkI24.js +0 -31539
  93. package/dist/_chunks/Settings-yLx-YvVy.mjs +0 -31520
  94. package/dist/_chunks/en-DE15m4xZ.mjs +0 -574
  95. package/dist/_chunks/index-CXGrFKp6.mjs +0 -128
  96. package/dist/_chunks/index-DgocXUgC.js +0 -127
  97. package/dist/admin/index.js +0 -3
  98. package/dist/admin/index.mjs +0 -4
  99. package/dist/robots.txt +0 -3
  100. package/dist/server/index.js +0 -27078
  101. package/dist/uploads/.gitkeep +0 -0
  102. package/dist/uploads/accessories_category_2a5631094b.jpeg +0 -0
  103. package/dist/uploads/beauty_personal_care_category_57f8a8f1e3.jpeg +0 -0
  104. package/dist/uploads/books_category_a9a253eada.jpeg +0 -0
  105. package/dist/uploads/classic_cotton_tshirt_1_cd713425f6.png +0 -0
  106. package/dist/uploads/clothing_category_d5c60ef07b.jpeg +0 -0
  107. package/dist/uploads/daviddoe_strapi_adbcd41787.jpeg +0 -0
  108. package/dist/uploads/electronics_category_fc3e5ef571.jpeg +0 -0
  109. package/dist/uploads/ergonomic_office_chair_1_c751cffb07.png +0 -0
  110. package/dist/uploads/home_garden_category_4f6eb3f8d6.jpeg +0 -0
  111. package/dist/uploads/istockphoto_1188462138_612x612_11f295b9c0.jpg +0 -0
  112. package/dist/uploads/istockphoto_1188462138_612x612_396fb272fd.jpg +0 -0
  113. package/dist/uploads/large_daviddoe_strapi_adbcd41787.jpeg +0 -0
  114. package/dist/uploads/leather_travel_backpack_1_238bc1ae4d.png +0 -0
  115. package/dist/uploads/mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
  116. package/dist/uploads/medium_classic_cotton_tshirt_1_cd713425f6.png +0 -0
  117. package/dist/uploads/medium_daviddoe_strapi_adbcd41787.jpeg +0 -0
  118. package/dist/uploads/medium_ergonomic_office_chair_1_c751cffb07.png +0 -0
  119. package/dist/uploads/medium_leather_travel_backpack_1_238bc1ae4d.png +0 -0
  120. package/dist/uploads/medium_mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
  121. package/dist/uploads/medium_smart_watch_series_5_1_cdc2511fb7.png +0 -0
  122. package/dist/uploads/medium_smartphone_x_pro_1_c3f0cbd080.png +0 -0
  123. package/dist/uploads/medium_the_great_gatsby_special_1_2e7c76d997.png +0 -0
  124. package/dist/uploads/medium_wireless_headphones_1_fa75cd50c3.png +0 -0
  125. package/dist/uploads/medium_yoga_mat_premium_1_01f9a3b5fa.png +0 -0
  126. package/dist/uploads/predictive_maintenance_icons_industry_automation_600nw_2685943461_e18a8aa3b0.webp +0 -0
  127. package/dist/uploads/small_classic_cotton_tshirt_1_cd713425f6.png +0 -0
  128. package/dist/uploads/small_daviddoe_strapi_adbcd41787.jpeg +0 -0
  129. package/dist/uploads/small_ergonomic_office_chair_1_c751cffb07.png +0 -0
  130. package/dist/uploads/small_leather_travel_backpack_1_238bc1ae4d.png +0 -0
  131. package/dist/uploads/small_mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
  132. package/dist/uploads/small_smart_watch_series_5_1_cdc2511fb7.png +0 -0
  133. package/dist/uploads/small_smartphone_x_pro_1_c3f0cbd080.png +0 -0
  134. package/dist/uploads/small_the_great_gatsby_special_1_2e7c76d997.png +0 -0
  135. package/dist/uploads/small_wireless_headphones_1_fa75cd50c3.png +0 -0
  136. package/dist/uploads/small_yoga_mat_premium_1_01f9a3b5fa.png +0 -0
  137. package/dist/uploads/smart_watch_series_5_1_cdc2511fb7.png +0 -0
  138. package/dist/uploads/smartphone_x_pro_1_c3f0cbd080.png +0 -0
  139. package/dist/uploads/the_great_gatsby_special_1_2e7c76d997.png +0 -0
  140. package/dist/uploads/thumbnail_accessories_category_2a5631094b.jpeg +0 -0
  141. package/dist/uploads/thumbnail_beauty_personal_care_category_57f8a8f1e3.jpeg +0 -0
  142. package/dist/uploads/thumbnail_books_category_a9a253eada.jpeg +0 -0
  143. package/dist/uploads/thumbnail_classic_cotton_tshirt_1_cd713425f6.png +0 -0
  144. package/dist/uploads/thumbnail_clothing_category_d5c60ef07b.jpeg +0 -0
  145. package/dist/uploads/thumbnail_daviddoe_strapi_adbcd41787.jpeg +0 -0
  146. package/dist/uploads/thumbnail_electronics_category_fc3e5ef571.jpeg +0 -0
  147. package/dist/uploads/thumbnail_ergonomic_office_chair_1_c751cffb07.png +0 -0
  148. package/dist/uploads/thumbnail_home_garden_category_4f6eb3f8d6.jpeg +0 -0
  149. package/dist/uploads/thumbnail_istockphoto_1188462138_612x612_11f295b9c0.jpg +0 -0
  150. package/dist/uploads/thumbnail_istockphoto_1188462138_612x612_396fb272fd.jpg +0 -0
  151. package/dist/uploads/thumbnail_leather_travel_backpack_1_238bc1ae4d.png +0 -0
  152. package/dist/uploads/thumbnail_mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
  153. package/dist/uploads/thumbnail_predictive_maintenance_icons_industry_automation_600nw_2685943461_e18a8aa3b0.webp +0 -0
  154. package/dist/uploads/thumbnail_smart_watch_series_5_1_cdc2511fb7.png +0 -0
  155. package/dist/uploads/thumbnail_smartphone_x_pro_1_c3f0cbd080.png +0 -0
  156. package/dist/uploads/thumbnail_the_great_gatsby_special_1_2e7c76d997.png +0 -0
  157. package/dist/uploads/thumbnail_wireless_headphones_1_fa75cd50c3.png +0 -0
  158. package/dist/uploads/thumbnail_yoga_mat_premium_1_01f9a3b5fa.png +0 -0
  159. package/dist/uploads/webby-commerce.png +0 -0
  160. package/dist/uploads/wireless_headphones_1_fa75cd50c3.png +0 -0
  161. package/dist/uploads/yoga_mat_premium_1_01f9a3b5fa.png +0 -0
  162. /package/{dist → server/src}/data/demo-data.json +0 -0
@@ -0,0 +1,388 @@
1
+ 'use strict';
2
+
3
+ const PLUGIN_ID = 'webbycommerce';
4
+ const { ensureEcommercePermission } = require('../utils/check-ecommerce-permission');
5
+
6
+ const uniqBy = (items, keyFn) => {
7
+ const seen = new Set();
8
+ return (Array.isArray(items) ? items : []).filter((item) => {
9
+ const key = keyFn(item);
10
+ if (seen.has(key)) return false;
11
+ seen.add(key);
12
+ return true;
13
+ });
14
+ };
15
+
16
+ module.exports = {
17
+ async getProductCategories(ctx) {
18
+ try {
19
+ if (!(await ensureEcommercePermission(ctx))) {
20
+ return;
21
+ }
22
+
23
+ const { search, limit = 50, start = 0, publicationState, status, preview } = ctx.query;
24
+
25
+ // Strapi v5 stores drafts + published versions as separate rows (document model).
26
+ // For storefront/public APIs we should default to published records to avoid duplicates.
27
+ const includeDrafts =
28
+ preview === 'true' || publicationState === 'preview' || status === 'preview' || status === 'draft';
29
+
30
+ const where = {};
31
+ if (search) {
32
+ where.name = { $containsi: search };
33
+ }
34
+
35
+ if (!includeDrafts) {
36
+ where.publishedAt = { $notNull: true };
37
+ }
38
+
39
+ const productCategories = await strapi.db.query('plugin::webbycommerce.product-category').findMany({
40
+ where,
41
+ limit: parseInt(limit, 10),
42
+ start: parseInt(start, 10),
43
+ orderBy: { createdAt: 'desc' },
44
+ populate: includeDrafts
45
+ ? ['products', 'image']
46
+ : {
47
+ products: {
48
+ where: { publishedAt: { $notNull: true } },
49
+ },
50
+ image: true,
51
+ },
52
+ });
53
+
54
+ if (!includeDrafts && Array.isArray(productCategories)) {
55
+ for (const category of productCategories) {
56
+ if (Array.isArray(category?.products)) {
57
+ // Safety net in case relation-level filtering is not applied by the ORM in some scenarios.
58
+ const publishedOnly = category.products.filter((p) => p && p.publishedAt);
59
+ category.products = uniqBy(publishedOnly, (p) => p.documentId || String(p.id));
60
+ }
61
+ }
62
+ }
63
+
64
+ const total = await strapi.db.query('plugin::webbycommerce.product-category').count({ where });
65
+
66
+ ctx.send({ data: productCategories, meta: { total, limit: parseInt(limit, 10), start: parseInt(start, 10) } });
67
+ } catch (error) {
68
+ strapi.log.error(`[${PLUGIN_ID}] Error in getProductCategories:`, error);
69
+ ctx.internalServerError('Failed to fetch product categories. Please try again.');
70
+ }
71
+ },
72
+
73
+ async getProductCategory(ctx) {
74
+ try {
75
+ if (!(await ensureEcommercePermission(ctx))) {
76
+ return;
77
+ }
78
+
79
+ const rawId = ctx.params?.id;
80
+ const idOrSlug = typeof rawId === 'string' ? decodeURIComponent(rawId).trim() : '';
81
+ const { publicationState, status, preview } = ctx.query;
82
+ const includeDrafts =
83
+ preview === 'true' || publicationState === 'preview' || status === 'preview' || status === 'draft';
84
+
85
+ if (!idOrSlug) {
86
+ return ctx.badRequest('Category ID or slug is required.');
87
+ }
88
+
89
+ // Check if it's a numeric ID or a slug
90
+ const isNumericId = /^[0-9]+$/.test(idOrSlug);
91
+ let productCategory;
92
+
93
+ if (isNumericId) {
94
+ // Query by ID
95
+ productCategory = await strapi.db.query('plugin::webbycommerce.product-category').findOne({
96
+ where: includeDrafts ? { id: idOrSlug } : { id: idOrSlug, publishedAt: { $notNull: true } },
97
+ populate: includeDrafts
98
+ ? ['products', 'image', 'parent']
99
+ : {
100
+ products: {
101
+ where: { publishedAt: { $notNull: true } },
102
+ },
103
+ image: true,
104
+ parent: true,
105
+ },
106
+ });
107
+ } else {
108
+ // Query by slug
109
+ const where = includeDrafts ? { slug: idOrSlug } : { slug: idOrSlug, publishedAt: { $notNull: true } };
110
+
111
+ // For slug queries, we might get multiple results (drafts + published), so get the latest published one
112
+ const results = await strapi.db.query('plugin::webbycommerce.product-category').findMany({
113
+ where,
114
+ limit: 1,
115
+ orderBy: { publishedAt: 'desc', id: 'desc' },
116
+ populate: includeDrafts
117
+ ? ['products', 'image', 'parent']
118
+ : {
119
+ products: {
120
+ where: { publishedAt: { $notNull: true } },
121
+ },
122
+ image: true,
123
+ parent: true,
124
+ },
125
+ });
126
+
127
+ productCategory = results?.[0];
128
+ }
129
+
130
+ if (!productCategory) {
131
+ return ctx.notFound('Product category not found.');
132
+ }
133
+
134
+ if (!includeDrafts && Array.isArray(productCategory?.products)) {
135
+ const publishedOnly = productCategory.products.filter((p) => p && p.publishedAt);
136
+ productCategory.products = uniqBy(publishedOnly, (p) => p.documentId || String(p.id));
137
+ }
138
+
139
+ ctx.send({ data: productCategory });
140
+ } catch (error) {
141
+ strapi.log.error(`[${PLUGIN_ID}] Error in getProductCategory:`, error);
142
+ ctx.internalServerError('Failed to fetch product category. Please try again.');
143
+ }
144
+ },
145
+
146
+ async createProductCategory(ctx) {
147
+ try {
148
+ if (!(await ensureEcommercePermission(ctx))) {
149
+ return;
150
+ }
151
+
152
+ const { name, slug, description, image, parent } = ctx.request.body || {};
153
+
154
+ if (!name) {
155
+ return ctx.badRequest('Name is required.');
156
+ }
157
+
158
+ const data = {
159
+ name,
160
+ slug: slug || undefined,
161
+ description: description || undefined,
162
+ image: image || undefined,
163
+ publishedAt: new Date(), // Set publishedAt so it appears in content manager
164
+ };
165
+
166
+ // Handle parent relation if provided
167
+ if (parent) {
168
+ data.parent = typeof parent === 'object' && parent.id ? parent.id : parent;
169
+ }
170
+
171
+ // Use entityService for Strapi v5 document model compatibility
172
+ // This ensures categories appear in the content manager
173
+ const productCategory = await strapi.entityService.create('plugin::webbycommerce.product-category', {
174
+ data,
175
+ });
176
+
177
+ // Re-fetch with populated relations
178
+ const populated = await strapi.entityService.findOne('plugin::webbycommerce.product-category', productCategory.id, {
179
+ populate: ['products', 'image', 'parent'],
180
+ });
181
+
182
+ ctx.send({ data: populated || productCategory });
183
+ } catch (error) {
184
+ strapi.log.error(`[${PLUGIN_ID}] Error in createProductCategory:`, error);
185
+ ctx.internalServerError('Failed to create product category. Please try again.');
186
+ }
187
+ },
188
+
189
+ async updateProductCategory(ctx) {
190
+ try {
191
+ if (!(await ensureEcommercePermission(ctx))) {
192
+ return;
193
+ }
194
+
195
+ const { id } = ctx.params;
196
+ const updateData = ctx.request.body || {};
197
+
198
+ const updated = await strapi.db.query('plugin::webbycommerce.product-category').update({
199
+ where: { id },
200
+ data: updateData,
201
+ });
202
+
203
+ if (!updated) {
204
+ return ctx.notFound('Product category not found.');
205
+ }
206
+
207
+ ctx.send({ data: updated });
208
+ } catch (error) {
209
+ strapi.log.error(`[${PLUGIN_ID}] Error in updateProductCategory:`, error);
210
+ ctx.internalServerError('Failed to update product category. Please try again.');
211
+ }
212
+ },
213
+
214
+ async deleteProductCategory(ctx) {
215
+ try {
216
+ if (!(await ensureEcommercePermission(ctx))) {
217
+ return;
218
+ }
219
+
220
+ const { id } = ctx.params;
221
+
222
+ const existing = await strapi.db.query('plugin::webbycommerce.product-category').findOne({ where: { id } });
223
+ if (!existing) {
224
+ return ctx.notFound('Product category not found.');
225
+ }
226
+
227
+ await strapi.db.query('plugin::webbycommerce.product-category').delete({ where: { id } });
228
+
229
+ ctx.send({ data: { id } });
230
+ } catch (error) {
231
+ strapi.log.error(`[${PLUGIN_ID}] Error in deleteProductCategory:`, error);
232
+ ctx.internalServerError('Failed to delete product category. Please try again.');
233
+ }
234
+ },
235
+
236
+ // Standard Strapi controller methods for content manager
237
+ async find(ctx) {
238
+ try {
239
+ const { search, limit = 25, start = 0, sort = 'createdAt:desc' } = ctx.query;
240
+
241
+ const where = { publishedAt: { $notNull: true } };
242
+
243
+ if (search) {
244
+ where.name = { $containsi: search };
245
+ }
246
+
247
+ const categories = await strapi.db.query('plugin::webbycommerce.product-category').findMany({
248
+ where,
249
+ limit: parseInt(limit, 10),
250
+ start: parseInt(start, 10),
251
+ orderBy: sort.split(':').reduce((acc, val, i) => {
252
+ if (i === 0) acc[val] = 'asc';
253
+ else acc[val] = 'desc';
254
+ return acc;
255
+ }, {}),
256
+ populate: ['products', 'image'],
257
+ });
258
+
259
+ const total = await strapi.db.query('plugin::webbycommerce.product-category').count({ where });
260
+
261
+ ctx.send({
262
+ data: categories,
263
+ meta: {
264
+ pagination: {
265
+ page: Math.floor(parseInt(start, 10) / parseInt(limit, 10)) + 1,
266
+ pageSize: parseInt(limit, 10),
267
+ pageCount: Math.ceil(total / parseInt(limit, 10)),
268
+ total,
269
+ },
270
+ },
271
+ });
272
+ } catch (error) {
273
+ strapi.log.error(`[${PLUGIN_ID}] Error in find:`, error);
274
+ ctx.internalServerError('Failed to fetch product categories.');
275
+ }
276
+ },
277
+
278
+ async findOne(ctx) {
279
+ try {
280
+ const rawId = ctx.params?.id;
281
+ const idOrSlug = typeof rawId === 'string' ? decodeURIComponent(rawId).trim() : '';
282
+
283
+ if (!idOrSlug) {
284
+ return ctx.badRequest('Category ID or slug is required.');
285
+ }
286
+
287
+ // Check if it's a numeric ID or a slug
288
+ const isNumericId = /^[0-9]+$/.test(idOrSlug);
289
+ let category;
290
+
291
+ if (isNumericId) {
292
+ // Query by ID
293
+ category = await strapi.db.query('plugin::webbycommerce.product-category').findOne({
294
+ where: { id: idOrSlug, publishedAt: { $notNull: true } },
295
+ populate: ['products', 'image', 'parent'],
296
+ });
297
+ } else {
298
+ // Query by slug
299
+ const results = await strapi.db.query('plugin::webbycommerce.product-category').findMany({
300
+ where: { slug: idOrSlug, publishedAt: { $notNull: true } },
301
+ limit: 1,
302
+ orderBy: { publishedAt: 'desc', id: 'desc' },
303
+ populate: ['products', 'image', 'parent'],
304
+ });
305
+
306
+ category = results?.[0];
307
+ }
308
+
309
+ if (!category) {
310
+ return ctx.notFound('Product category not found.');
311
+ }
312
+
313
+ ctx.send({ data: category });
314
+ } catch (error) {
315
+ strapi.log.error(`[${PLUGIN_ID}] Error in findOne:`, error);
316
+ ctx.internalServerError('Failed to fetch product category.');
317
+ }
318
+ },
319
+
320
+ async create(ctx) {
321
+ try {
322
+ const data = ctx.request.body.data || ctx.request.body;
323
+
324
+ if (!data.publishedAt) {
325
+ data.publishedAt = new Date();
326
+ }
327
+
328
+ // Use entityService for Strapi v5 document model compatibility
329
+ const category = await strapi.entityService.create('plugin::webbycommerce.product-category', {
330
+ data,
331
+ });
332
+
333
+ const populated = await strapi.entityService.findOne('plugin::webbycommerce.product-category', category.id, {
334
+ populate: ['products', 'image', 'parent'],
335
+ });
336
+
337
+ ctx.send({ data: populated || category });
338
+ } catch (error) {
339
+ strapi.log.error(`[${PLUGIN_ID}] Error in create:`, error);
340
+ ctx.internalServerError('Failed to create product category.');
341
+ }
342
+ },
343
+
344
+ async update(ctx) {
345
+ try {
346
+ const { id } = ctx.params;
347
+ const data = ctx.request.body.data || ctx.request.body;
348
+
349
+ const category = await strapi.db.query('plugin::webbycommerce.product-category').update({
350
+ where: { id },
351
+ data,
352
+ });
353
+
354
+ if (!category) {
355
+ return ctx.notFound('Product category not found.');
356
+ }
357
+
358
+ const populated = await strapi.db.query('plugin::webbycommerce.product-category').findOne({
359
+ where: { id: category.id },
360
+ populate: ['products', 'image'],
361
+ });
362
+
363
+ ctx.send({ data: populated || category });
364
+ } catch (error) {
365
+ strapi.log.error(`[${PLUGIN_ID}] Error in update:`, error);
366
+ ctx.internalServerError('Failed to update product category.');
367
+ }
368
+ },
369
+
370
+ async delete(ctx) {
371
+ try {
372
+ const { id } = ctx.params;
373
+
374
+ const category = await strapi.db.query('plugin::webbycommerce.product-category').delete({
375
+ where: { id },
376
+ });
377
+
378
+ if (!category) {
379
+ return ctx.notFound('Product category not found.');
380
+ }
381
+
382
+ ctx.send({ data: category });
383
+ } catch (error) {
384
+ strapi.log.error(`[${PLUGIN_ID}] Error in delete:`, error);
385
+ ctx.internalServerError('Failed to delete product category.');
386
+ }
387
+ },
388
+ };
@@ -0,0 +1,246 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * compare controller
5
+ */
6
+
7
+ const { createCoreController } = require('@strapi/strapi').factories;
8
+
9
+ module.exports = createCoreController('plugin::webbycommerce.compare', ({ strapi }) => ({
10
+ async getCompare(ctx) {
11
+ try {
12
+ const user = ctx.state.user;
13
+ if (!user) {
14
+ return ctx.unauthorized('Authentication required');
15
+ }
16
+
17
+ const compare = await strapi
18
+ .plugin('webbycommerce')
19
+ .service('compare')
20
+ .findUserCompare(user.id);
21
+
22
+ if (!compare) {
23
+ // Return empty compare structure
24
+ return ctx.send({
25
+ data: {
26
+ id: null,
27
+ userId: user.id,
28
+ userEmail: user.email,
29
+ products: [],
30
+ category: null,
31
+ isPublic: false,
32
+ name: null,
33
+ notes: null,
34
+ },
35
+ meta: {
36
+ totalProducts: 0,
37
+ comparisonData: {},
38
+ },
39
+ });
40
+ }
41
+
42
+ const compareData = await strapi
43
+ .plugin('webbycommerce')
44
+ .service('compare')
45
+ .getCompareData(user.id);
46
+
47
+ ctx.send({
48
+ data: compare,
49
+ meta: {
50
+ totalProducts: compare?.products?.length || 0,
51
+ comparisonData: compareData.comparisonData,
52
+ },
53
+ });
54
+ } catch (error) {
55
+ strapi.log.error('Error fetching compare:', error);
56
+ ctx.badRequest('Failed to fetch compare list', { error: error.message });
57
+ }
58
+ },
59
+
60
+ async addToCompare(ctx) {
61
+ try {
62
+ const user = ctx.state.user;
63
+ if (!user) {
64
+ return ctx.unauthorized('Authentication required');
65
+ }
66
+
67
+ const { productId } = ctx.request.body;
68
+ if (!productId) {
69
+ return ctx.badRequest('Product ID is required');
70
+ }
71
+
72
+ const compare = await strapi
73
+ .plugin('webbycommerce')
74
+ .service('compare')
75
+ .addProductToCompare(user.id, user.email, productId);
76
+
77
+ const compareData = await strapi
78
+ .plugin('webbycommerce')
79
+ .service('compare')
80
+ .getCompareData(user.id);
81
+
82
+ ctx.send({
83
+ data: compare,
84
+ meta: {
85
+ totalProducts: compare?.products?.length || 0,
86
+ comparisonData: compareData.comparisonData,
87
+ },
88
+ message: 'Product added to compare list successfully',
89
+ });
90
+ } catch (error) {
91
+ strapi.log.error('Error adding to compare:', error);
92
+ ctx.badRequest('Failed to add product to compare list', { error: error.message });
93
+ }
94
+ },
95
+
96
+ async removeFromCompare(ctx) {
97
+ try {
98
+ const user = ctx.state.user;
99
+ if (!user) {
100
+ return ctx.unauthorized('Authentication required');
101
+ }
102
+
103
+ const { productId } = ctx.params;
104
+ if (!productId) {
105
+ return ctx.badRequest('Product ID is required');
106
+ }
107
+
108
+ const compare = await strapi
109
+ .plugin('webbycommerce')
110
+ .service('compare')
111
+ .removeProductFromCompare(user.id, productId);
112
+
113
+ const compareData = await strapi
114
+ .plugin('webbycommerce')
115
+ .service('compare')
116
+ .getCompareData(user.id);
117
+
118
+ ctx.send({
119
+ data: compare,
120
+ meta: {
121
+ totalProducts: compare?.products?.length || 0,
122
+ comparisonData: compareData.comparisonData,
123
+ },
124
+ message: 'Product removed from compare list successfully',
125
+ });
126
+ } catch (error) {
127
+ strapi.log.error('Error removing from compare:', error);
128
+ ctx.badRequest('Failed to remove product from compare list', { error: error.message });
129
+ }
130
+ },
131
+
132
+ async clearCompare(ctx) {
133
+ try {
134
+ const user = ctx.state.user;
135
+ if (!user) {
136
+ return ctx.unauthorized('Authentication required');
137
+ }
138
+
139
+ const compare = await strapi
140
+ .plugin('webbycommerce')
141
+ .service('compare')
142
+ .clearCompare(user.id);
143
+
144
+ ctx.send({
145
+ data: compare,
146
+ meta: {
147
+ totalProducts: 0,
148
+ comparisonData: {},
149
+ },
150
+ message: 'Compare list cleared successfully',
151
+ });
152
+ } catch (error) {
153
+ strapi.log.error('Error clearing compare:', error);
154
+ ctx.badRequest('Failed to clear compare list', { error: error.message });
155
+ }
156
+ },
157
+
158
+ async updateCompare(ctx) {
159
+ try {
160
+ const user = ctx.state.user;
161
+ if (!user) {
162
+ return ctx.unauthorized('Authentication required');
163
+ }
164
+
165
+ const { name, notes, isPublic } = ctx.request.body;
166
+
167
+ const compare = await strapi
168
+ .plugin('webbycommerce')
169
+ .service('compare')
170
+ .updateCompare(user.id, { name, notes, isPublic });
171
+
172
+ ctx.send({
173
+ data: compare,
174
+ message: 'Compare list updated successfully',
175
+ });
176
+ } catch (error) {
177
+ strapi.log.error('Error updating compare:', error);
178
+ ctx.badRequest('Failed to update compare list', { error: error.message });
179
+ }
180
+ },
181
+
182
+ async getComparisonData(ctx) {
183
+ try {
184
+ const user = ctx.state.user;
185
+ if (!user) {
186
+ return ctx.unauthorized('Authentication required');
187
+ }
188
+
189
+ const compareData = await strapi
190
+ .plugin('webbycommerce')
191
+ .service('compare')
192
+ .getCompareData(user.id);
193
+
194
+ ctx.send({
195
+ data: compareData,
196
+ });
197
+ } catch (error) {
198
+ strapi.log.error('Error getting comparison data:', error);
199
+ ctx.badRequest('Failed to get comparison data', { error: error.message });
200
+ }
201
+ },
202
+
203
+ async checkCompareStatus(ctx) {
204
+ try {
205
+ const user = ctx.state.user;
206
+ if (!user) {
207
+ return ctx.unauthorized('Authentication required');
208
+ }
209
+
210
+ const { productIds } = ctx.query;
211
+ if (!productIds) {
212
+ return ctx.badRequest('Product IDs are required');
213
+ }
214
+
215
+ const compare = await strapi
216
+ .plugin('webbycommerce')
217
+ .service('compare')
218
+ .findUserCompare(user.id);
219
+
220
+ const productIdArray = Array.isArray(productIds)
221
+ ? productIds.map(id => parseInt(id))
222
+ : [parseInt(productIds)];
223
+
224
+ const inCompare = {};
225
+ if (compare && compare.products && Array.isArray(compare.products)) {
226
+ productIdArray.forEach(productId => {
227
+ inCompare[productId] = compare.products.some(product => {
228
+ const productIdValue = typeof product === 'object' && product !== null ? product.id : product;
229
+ return productIdValue === productId;
230
+ });
231
+ });
232
+ } else {
233
+ productIdArray.forEach(productId => {
234
+ inCompare[productId] = false;
235
+ });
236
+ }
237
+
238
+ ctx.send({
239
+ data: inCompare,
240
+ });
241
+ } catch (error) {
242
+ strapi.log.error('Error checking compare status:', error);
243
+ ctx.badRequest('Failed to check compare status', { error: error.message });
244
+ }
245
+ },
246
+ }));