@webbycrown/webbycommerce 1.2.1 → 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 +21 -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-Bg2JyQ4c.js +0 -31518
  93. package/dist/_chunks/Settings-BonPzbwr.mjs +0 -31499
  94. package/dist/_chunks/en-DE15m4xZ.mjs +0 -574
  95. package/dist/_chunks/index-BWVy9o1d.mjs +0 -128
  96. package/dist/_chunks/index-NRuOdjd7.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 -27336
  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,370 @@
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
+ /**
18
+ * List tags
19
+ */
20
+ async getTags(ctx) {
21
+ try {
22
+ if (!(await ensureEcommercePermission(ctx))) {
23
+ return;
24
+ }
25
+
26
+ const { search, limit = 50, start = 0 } = ctx.query;
27
+
28
+ // Strapi v5 stores drafts + published versions as separate rows (document model).
29
+ // For storefront APIs we only want published records, otherwise you can see duplicates.
30
+ const where = { publishedAt: { $notNull: true } };
31
+ if (search) {
32
+ where.name = { $containsi: search };
33
+ }
34
+
35
+ const tags = await strapi.db.query('plugin::webbycommerce.product-tag').findMany({
36
+ where,
37
+ limit: parseInt(limit, 10),
38
+ start: parseInt(start, 10),
39
+ orderBy: { createdAt: 'desc' },
40
+ });
41
+
42
+ const total = await strapi.db.query('plugin::webbycommerce.product-tag').count({ where });
43
+
44
+ ctx.send({ data: tags, meta: { total, limit: parseInt(limit, 10), start: parseInt(start, 10) } });
45
+ } catch (error) {
46
+ strapi.log.error(`[${PLUGIN_ID}] Error in getTags:`, error);
47
+ ctx.internalServerError('Failed to fetch tags. Please try again.');
48
+ }
49
+ },
50
+
51
+ /**
52
+ * Get single tag
53
+ */
54
+ async getTag(ctx) {
55
+ try {
56
+ if (!(await ensureEcommercePermission(ctx))) {
57
+ return;
58
+ }
59
+
60
+ const rawId = ctx.params?.id;
61
+ const idOrSlug = typeof rawId === 'string' ? decodeURIComponent(rawId).trim() : '';
62
+
63
+ if (!idOrSlug) {
64
+ return ctx.badRequest('Tag ID or slug is required.');
65
+ }
66
+
67
+ // Check if it's a numeric ID or a slug
68
+ const isNumericId = /^[0-9]+$/.test(idOrSlug);
69
+ let tag;
70
+
71
+ if (isNumericId) {
72
+ // Query by ID
73
+ tag = await strapi.db.query('plugin::webbycommerce.product-tag').findOne({
74
+ where: { id: idOrSlug },
75
+ populate: {
76
+ products: {
77
+ where: { publishedAt: { $notNull: true } },
78
+ },
79
+ },
80
+ });
81
+ } else {
82
+ // Query by slug
83
+ const results = await strapi.db.query('plugin::webbycommerce.product-tag').findMany({
84
+ where: { slug: idOrSlug, publishedAt: { $notNull: true } },
85
+ limit: 1,
86
+ orderBy: { publishedAt: 'desc', id: 'desc' },
87
+ populate: {
88
+ products: {
89
+ where: { publishedAt: { $notNull: true } },
90
+ },
91
+ },
92
+ });
93
+
94
+ tag = results?.[0];
95
+ }
96
+
97
+ if (!tag) {
98
+ return ctx.notFound('Tag not found.');
99
+ }
100
+
101
+ // Filter and deduplicate products by documentId (Strapi v5 document model)
102
+ if (Array.isArray(tag?.products)) {
103
+ const publishedOnly = tag.products.filter((p) => p && p.publishedAt);
104
+ tag.products = uniqBy(publishedOnly, (p) => p.documentId || String(p.id));
105
+ }
106
+
107
+ ctx.send({ data: tag });
108
+ } catch (error) {
109
+ strapi.log.error(`[${PLUGIN_ID}] Error in getTag:`, error);
110
+ ctx.internalServerError('Failed to fetch tag. Please try again.');
111
+ }
112
+ },
113
+
114
+ /**
115
+ * Create tag
116
+ */
117
+ async createTag(ctx) {
118
+ try {
119
+ if (!(await ensureEcommercePermission(ctx))) {
120
+ return;
121
+ }
122
+
123
+ const { name, slug } = ctx.request.body || {};
124
+
125
+ if (!name) {
126
+ return ctx.badRequest('Name is required.');
127
+ }
128
+
129
+ // Use entityService for Strapi v5 document model compatibility
130
+ // This ensures tags appear in the content manager
131
+ const tag = await strapi.entityService.create('plugin::webbycommerce.product-tag', {
132
+ data: {
133
+ name,
134
+ slug: slug || undefined,
135
+ publishedAt: new Date(), // Set publishedAt so it appears in content manager
136
+ },
137
+ });
138
+
139
+ // Re-fetch with populated relations
140
+ const populated = await strapi.entityService.findOne('plugin::webbycommerce.product-tag', tag.id, {
141
+ populate: ['products'],
142
+ });
143
+
144
+ ctx.send({ data: populated || tag });
145
+ } catch (error) {
146
+ strapi.log.error(`[${PLUGIN_ID}] Error in createTag:`, error);
147
+ ctx.internalServerError('Failed to create tag. Please try again.');
148
+ }
149
+ },
150
+
151
+ /**
152
+ * Update tag
153
+ */
154
+ async updateTag(ctx) {
155
+ try {
156
+ if (!(await ensureEcommercePermission(ctx))) {
157
+ return;
158
+ }
159
+
160
+ const { id } = ctx.params;
161
+ const updateData = ctx.request.body || {};
162
+
163
+ const updated = await strapi.db.query('plugin::webbycommerce.product-tag').update({
164
+ where: { id },
165
+ data: updateData,
166
+ });
167
+
168
+ if (!updated) {
169
+ return ctx.notFound('Tag not found.');
170
+ }
171
+
172
+ ctx.send({ data: updated });
173
+ } catch (error) {
174
+ strapi.log.error(`[${PLUGIN_ID}] Error in updateTag:`, error);
175
+ ctx.internalServerError('Failed to update tag. Please try again.');
176
+ }
177
+ },
178
+
179
+ /**
180
+ * Delete tag
181
+ */
182
+ async deleteTag(ctx) {
183
+ try {
184
+ if (!(await ensureEcommercePermission(ctx))) {
185
+ return;
186
+ }
187
+
188
+ const { id } = ctx.params;
189
+
190
+ const existing = await strapi.db.query('plugin::webbycommerce.product-tag').findOne({ where: { id } });
191
+ if (!existing) {
192
+ return ctx.notFound('Tag not found.');
193
+ }
194
+
195
+ await strapi.db.query('plugin::webbycommerce.product-tag').delete({ where: { id } });
196
+
197
+ ctx.send({ data: { id } });
198
+ } catch (error) {
199
+ strapi.log.error(`[${PLUGIN_ID}] Error in deleteTag:`, error);
200
+ ctx.internalServerError('Failed to delete tag. Please try again.');
201
+ }
202
+ },
203
+
204
+ // Standard Strapi controller methods for content manager
205
+ async find(ctx) {
206
+ try {
207
+ const { search, limit = 25, start = 0, sort = 'createdAt:desc' } = ctx.query;
208
+
209
+ const where = { publishedAt: { $notNull: true } };
210
+
211
+ if (search) {
212
+ where.name = { $containsi: search };
213
+ }
214
+
215
+ const tags = await strapi.db.query('plugin::webbycommerce.product-tag').findMany({
216
+ where,
217
+ limit: parseInt(limit, 10),
218
+ start: parseInt(start, 10),
219
+ orderBy: sort.split(':').reduce((acc, val, i) => {
220
+ if (i === 0) acc[val] = 'asc';
221
+ else acc[val] = 'desc';
222
+ return acc;
223
+ }, {}),
224
+ populate: ['products'],
225
+ });
226
+
227
+ const total = await strapi.db.query('plugin::webbycommerce.product-tag').count({ where });
228
+
229
+ ctx.send({
230
+ data: tags,
231
+ meta: {
232
+ pagination: {
233
+ page: Math.floor(parseInt(start, 10) / parseInt(limit, 10)) + 1,
234
+ pageSize: parseInt(limit, 10),
235
+ pageCount: Math.ceil(total / parseInt(limit, 10)),
236
+ total,
237
+ },
238
+ },
239
+ });
240
+ } catch (error) {
241
+ strapi.log.error(`[${PLUGIN_ID}] Error in find:`, error);
242
+ ctx.internalServerError('Failed to fetch tags.');
243
+ }
244
+ },
245
+
246
+ async findOne(ctx) {
247
+ try {
248
+ const rawId = ctx.params?.id;
249
+ const idOrSlug = typeof rawId === 'string' ? decodeURIComponent(rawId).trim() : '';
250
+
251
+ if (!idOrSlug) {
252
+ return ctx.badRequest('Tag ID or slug is required.');
253
+ }
254
+
255
+ // Check if it's a numeric ID or a slug
256
+ const isNumericId = /^[0-9]+$/.test(idOrSlug);
257
+ let tag;
258
+
259
+ if (isNumericId) {
260
+ // Query by ID
261
+ tag = await strapi.db.query('plugin::webbycommerce.product-tag').findOne({
262
+ where: { id: idOrSlug, publishedAt: { $notNull: true } },
263
+ populate: {
264
+ products: {
265
+ where: { publishedAt: { $notNull: true } },
266
+ },
267
+ },
268
+ });
269
+ } else {
270
+ // Query by slug
271
+ const results = await strapi.db.query('plugin::webbycommerce.product-tag').findMany({
272
+ where: { slug: idOrSlug, publishedAt: { $notNull: true } },
273
+ limit: 1,
274
+ orderBy: { publishedAt: 'desc', id: 'desc' },
275
+ populate: {
276
+ products: {
277
+ where: { publishedAt: { $notNull: true } },
278
+ },
279
+ },
280
+ });
281
+
282
+ tag = results?.[0];
283
+ }
284
+
285
+ if (!tag) {
286
+ return ctx.notFound('Tag not found.');
287
+ }
288
+
289
+ // Filter and deduplicate products by documentId (Strapi v5 document model)
290
+ if (Array.isArray(tag?.products)) {
291
+ const publishedOnly = tag.products.filter((p) => p && p.publishedAt);
292
+ tag.products = uniqBy(publishedOnly, (p) => p.documentId || String(p.id));
293
+ }
294
+
295
+ ctx.send({ data: tag });
296
+ } catch (error) {
297
+ strapi.log.error(`[${PLUGIN_ID}] Error in findOne:`, error);
298
+ ctx.internalServerError('Failed to fetch tag.');
299
+ }
300
+ },
301
+
302
+ async create(ctx) {
303
+ try {
304
+ const data = ctx.request.body.data || ctx.request.body;
305
+
306
+ if (!data.publishedAt) {
307
+ data.publishedAt = new Date();
308
+ }
309
+
310
+ // Use entityService for Strapi v5 document model compatibility
311
+ const tag = await strapi.entityService.create('plugin::webbycommerce.product-tag', {
312
+ data,
313
+ });
314
+
315
+ const populated = await strapi.entityService.findOne('plugin::webbycommerce.product-tag', tag.id, {
316
+ populate: ['products'],
317
+ });
318
+
319
+ ctx.send({ data: populated || tag });
320
+ } catch (error) {
321
+ strapi.log.error(`[${PLUGIN_ID}] Error in create:`, error);
322
+ ctx.internalServerError('Failed to create tag.');
323
+ }
324
+ },
325
+
326
+ async update(ctx) {
327
+ try {
328
+ const { id } = ctx.params;
329
+ const data = ctx.request.body.data || ctx.request.body;
330
+
331
+ const tag = await strapi.db.query('plugin::webbycommerce.product-tag').update({
332
+ where: { id },
333
+ data,
334
+ });
335
+
336
+ if (!tag) {
337
+ return ctx.notFound('Tag not found.');
338
+ }
339
+
340
+ const populated = await strapi.db.query('plugin::webbycommerce.product-tag').findOne({
341
+ where: { id: tag.id },
342
+ populate: ['products'],
343
+ });
344
+
345
+ ctx.send({ data: populated || tag });
346
+ } catch (error) {
347
+ strapi.log.error(`[${PLUGIN_ID}] Error in update:`, error);
348
+ ctx.internalServerError('Failed to update tag.');
349
+ }
350
+ },
351
+
352
+ async delete(ctx) {
353
+ try {
354
+ const { id } = ctx.params;
355
+
356
+ const tag = await strapi.db.query('plugin::webbycommerce.product-tag').delete({
357
+ where: { id },
358
+ });
359
+
360
+ if (!tag) {
361
+ return ctx.notFound('Tag not found.');
362
+ }
363
+
364
+ ctx.send({ data: tag });
365
+ } catch (error) {
366
+ strapi.log.error(`[${PLUGIN_ID}] Error in delete:`, error);
367
+ ctx.internalServerError('Failed to delete tag.');
368
+ }
369
+ },
370
+ };
@@ -0,0 +1,181 @@
1
+ 'use strict';
2
+
3
+ const PLUGIN_ID = 'webbycommerce';
4
+ const { ensureEcommercePermission } = require('../utils/check-ecommerce-permission');
5
+
6
+ module.exports = {
7
+ // Standard Strapi controller methods for content manager
8
+ async find(ctx) {
9
+ try {
10
+ // Check ecommerce permission
11
+ if (!(await ensureEcommercePermission(ctx))) {
12
+ return;
13
+ }
14
+
15
+ const { limit = 25, start = 0, sort = 'createdAt:desc' } = ctx.query;
16
+
17
+ const variations = await strapi.db.query('plugin::webbycommerce.product-variation').findMany({
18
+ limit: parseInt(limit, 10),
19
+ start: parseInt(start, 10),
20
+ orderBy: sort.split(':').reduce((acc, val, i) => {
21
+ if (i === 0) acc[val] = 'asc';
22
+ else acc[val] = 'desc';
23
+ return acc;
24
+ }, {}),
25
+ populate: ['product', 'attributes', 'attributeValues'],
26
+ });
27
+
28
+ const total = await strapi.db.query('plugin::webbycommerce.product-variation').count();
29
+
30
+ ctx.send({
31
+ data: variations,
32
+ meta: {
33
+ pagination: {
34
+ page: Math.floor(parseInt(start, 10) / parseInt(limit, 10)) + 1,
35
+ pageSize: parseInt(limit, 10),
36
+ pageCount: Math.ceil(total / parseInt(limit, 10)),
37
+ total,
38
+ },
39
+ },
40
+ });
41
+ } catch (error) {
42
+ strapi.log.error(`[${PLUGIN_ID}] Error in find:`, error);
43
+ ctx.internalServerError('Failed to fetch product variations.');
44
+ }
45
+ },
46
+
47
+ async findOne(ctx) {
48
+ try {
49
+ // Check ecommerce permission
50
+ if (!(await ensureEcommercePermission(ctx))) {
51
+ return;
52
+ }
53
+
54
+ const rawId = ctx.params?.id;
55
+ const idOrSlug = typeof rawId === 'string' ? decodeURIComponent(rawId).trim() : '';
56
+
57
+ if (!idOrSlug) {
58
+ return ctx.badRequest('Variation ID or slug is required.');
59
+ }
60
+
61
+ // Check if it's a numeric ID or a slug
62
+ const isNumericId = /^[0-9]+$/.test(idOrSlug);
63
+ let variation;
64
+
65
+ if (isNumericId) {
66
+ // Query by ID
67
+ variation = await strapi.db.query('plugin::webbycommerce.product-variation').findOne({
68
+ where: { id: idOrSlug },
69
+ populate: ['product', 'attributes', 'attributeValues'],
70
+ });
71
+ } else {
72
+ // Query by slug
73
+ const results = await strapi.db.query('plugin::webbycommerce.product-variation').findMany({
74
+ where: { slug: idOrSlug },
75
+ limit: 1,
76
+ orderBy: { id: 'desc' },
77
+ populate: ['product', 'attributes', 'attributeValues'],
78
+ });
79
+
80
+ variation = results?.[0];
81
+ }
82
+
83
+ if (!variation) {
84
+ return ctx.notFound('Product variation not found.');
85
+ }
86
+
87
+ ctx.send({ data: variation });
88
+ } catch (error) {
89
+ strapi.log.error(`[${PLUGIN_ID}] Error in findOne:`, error);
90
+ ctx.internalServerError('Failed to fetch product variation.');
91
+ }
92
+ },
93
+
94
+ async create(ctx) {
95
+ try {
96
+ // Check ecommerce permission
97
+ if (!(await ensureEcommercePermission(ctx))) {
98
+ return;
99
+ }
100
+
101
+ const data = ctx.request.body.data || ctx.request.body;
102
+
103
+ // Ensure slug is handled properly - if not provided, Strapi will auto-generate from name
104
+ if (data.slug === '') {
105
+ data.slug = undefined;
106
+ }
107
+
108
+ const variation = await strapi.db.query('plugin::webbycommerce.product-variation').create({ data });
109
+
110
+ const populated = await strapi.db.query('plugin::webbycommerce.product-variation').findOne({
111
+ where: { id: variation.id },
112
+ populate: ['product', 'attributes', 'attributeValues'],
113
+ });
114
+
115
+ ctx.send({ data: populated || variation });
116
+ } catch (error) {
117
+ strapi.log.error(`[${PLUGIN_ID}] Error in create:`, error);
118
+ ctx.internalServerError('Failed to create product variation.');
119
+ }
120
+ },
121
+
122
+ async update(ctx) {
123
+ try {
124
+ // Check ecommerce permission
125
+ if (!(await ensureEcommercePermission(ctx))) {
126
+ return;
127
+ }
128
+
129
+ const { id } = ctx.params;
130
+ const data = ctx.request.body.data || ctx.request.body;
131
+
132
+ // Ensure slug is handled properly - if empty string, set to undefined to allow auto-generation
133
+ if (data.slug === '') {
134
+ data.slug = undefined;
135
+ }
136
+
137
+ const variation = await strapi.db.query('plugin::webbycommerce.product-variation').update({
138
+ where: { id },
139
+ data,
140
+ });
141
+
142
+ if (!variation) {
143
+ return ctx.notFound('Product variation not found.');
144
+ }
145
+
146
+ const populated = await strapi.db.query('plugin::webbycommerce.product-variation').findOne({
147
+ where: { id: variation.id },
148
+ populate: ['product', 'attributes', 'attributeValues'],
149
+ });
150
+
151
+ ctx.send({ data: populated || variation });
152
+ } catch (error) {
153
+ strapi.log.error(`[${PLUGIN_ID}] Error in update:`, error);
154
+ ctx.internalServerError('Failed to update product variation.');
155
+ }
156
+ },
157
+
158
+ async delete(ctx) {
159
+ try {
160
+ // Check ecommerce permission
161
+ if (!(await ensureEcommercePermission(ctx))) {
162
+ return;
163
+ }
164
+
165
+ const { id } = ctx.params;
166
+
167
+ const variation = await strapi.db.query('plugin::webbycommerce.product-variation').delete({
168
+ where: { id },
169
+ });
170
+
171
+ if (!variation) {
172
+ return ctx.notFound('Product variation not found.');
173
+ }
174
+
175
+ ctx.send({ data: variation });
176
+ } catch (error) {
177
+ strapi.log.error(`[${PLUGIN_ID}] Error in delete:`, error);
178
+ ctx.internalServerError('Failed to delete product variation.');
179
+ }
180
+ },
181
+ };