@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,531 @@
1
+ 'use strict';
2
+
3
+ const { createCoreService } = require('@strapi/strapi').factories;
4
+
5
+ const CART_UID = 'plugin::webbycommerce.cart';
6
+ const CART_ITEM_UID = 'plugin::webbycommerce.cart-item';
7
+ const PRODUCT_UID = 'plugin::webbycommerce.product';
8
+ const COUPON_UID = 'plugin::webbycommerce.coupon';
9
+
10
+ const asInt = (value) => {
11
+ const n = Number.parseInt(String(value), 10);
12
+ return Number.isFinite(n) ? n : null;
13
+ };
14
+
15
+ const asQty = (value) => {
16
+ const n = asInt(value);
17
+ if (!n || n < 1) return null;
18
+ return n;
19
+ };
20
+
21
+ module.exports = createCoreService(CART_ITEM_UID, ({ strapi }) => ({
22
+ async getOrCreateCart({ userId, guestId }) {
23
+ let cart;
24
+ if (userId) {
25
+ cart = await strapi.db.query(CART_UID).findOne({
26
+ where: { user: userId },
27
+ select: ['id', 'guest_id', 'currency'],
28
+ });
29
+ if (cart?.id) {
30
+ // Populate coupon separately
31
+ const cartWithCoupon = await strapi.db.query(CART_UID).findOne({
32
+ where: { id: cart.id },
33
+ populate: { coupon: true },
34
+ });
35
+ return cartWithCoupon || cart;
36
+ }
37
+
38
+ cart = await strapi.db.query(CART_UID).create({
39
+ data: {
40
+ user: userId,
41
+ currency: 'USD',
42
+ },
43
+ select: ['id', 'guest_id', 'currency'],
44
+ });
45
+ // Populate coupon for new cart
46
+ return await strapi.db.query(CART_UID).findOne({
47
+ where: { id: cart.id },
48
+ populate: { coupon: true },
49
+ });
50
+ }
51
+
52
+ // Guest cart flow
53
+ if (guestId) {
54
+ cart = await strapi.db.query(CART_UID).findOne({
55
+ where: { guest_id: String(guestId) },
56
+ select: ['id', 'guest_id', 'currency'],
57
+ });
58
+ if (cart?.id) {
59
+ // Populate coupon separately
60
+ const cartWithCoupon = await strapi.db.query(CART_UID).findOne({
61
+ where: { id: cart.id },
62
+ populate: { coupon: true },
63
+ });
64
+ return cartWithCoupon || cart;
65
+ }
66
+
67
+ cart = await strapi.db.query(CART_UID).create({
68
+ data: {
69
+ guest_id: String(guestId),
70
+ currency: 'USD',
71
+ },
72
+ select: ['id', 'guest_id', 'currency'],
73
+ });
74
+ // Populate coupon for new cart
75
+ return await strapi.db.query(CART_UID).findOne({
76
+ where: { id: cart.id },
77
+ populate: { coupon: true },
78
+ });
79
+ }
80
+
81
+ // Caller should pass guestId if they want determinism
82
+ cart = await strapi.db.query(CART_UID).create({
83
+ data: {
84
+ currency: 'USD',
85
+ },
86
+ select: ['id', 'guest_id', 'currency'],
87
+ });
88
+ // Populate coupon for new cart
89
+ return await strapi.db.query(CART_UID).findOne({
90
+ where: { id: cart.id },
91
+ populate: { coupon: true },
92
+ });
93
+ },
94
+
95
+ async getCartItems({ cartId }) {
96
+ return await strapi.db.query(CART_ITEM_UID).findMany({
97
+ where: { cart: cartId },
98
+ orderBy: { createdAt: 'desc' },
99
+ populate: {
100
+ product: { populate: ['images'] },
101
+ variation: true,
102
+ attributes: true,
103
+ attributeValues: true,
104
+ },
105
+ });
106
+ },
107
+
108
+ async getTotalsFromItems(items, coupon = null) {
109
+ const safe = Array.isArray(items) ? items : [];
110
+ const totalItems = safe.reduce((sum, it) => sum + (Number(it.quantity) || 0), 0);
111
+ const subtotal = safe.reduce((sum, it) => sum + (Number(it.total_price) || 0), 0);
112
+
113
+ // Calculate discount from coupon
114
+ let discount = 0;
115
+ if (coupon) {
116
+ if (coupon.type === 'percentage') {
117
+ discount = (subtotal * Number(coupon.value || 0)) / 100;
118
+ } else if (coupon.type === 'fixed') {
119
+ discount = Number(coupon.value || 0);
120
+ // Don't allow discount to exceed subtotal
121
+ if (discount > subtotal) {
122
+ discount = subtotal;
123
+ }
124
+ }
125
+ }
126
+
127
+ const tax = 0;
128
+ const shipping = 0;
129
+ const total = subtotal + tax + shipping - discount;
130
+
131
+ return {
132
+ totalItems,
133
+ subtotal: Number(subtotal.toFixed(2)),
134
+ tax: Number(tax.toFixed(2)),
135
+ shipping: Number(shipping.toFixed(2)),
136
+ discount: Number(discount.toFixed(2)),
137
+ total: Number(total.toFixed(2)),
138
+ currency: 'USD',
139
+ };
140
+ },
141
+
142
+ async addOrUpdateItem({ cartId, userId, productId, quantity }) {
143
+ const qty = asQty(quantity);
144
+ const pid = asInt(productId);
145
+ if (!pid) {
146
+ const err = new Error('Product ID is required');
147
+ // @ts-ignore
148
+ err.status = 400;
149
+ throw err;
150
+ }
151
+ if (!qty) {
152
+ const err = new Error('Quantity must be an integer >= 1');
153
+ // @ts-ignore
154
+ err.status = 400;
155
+ throw err;
156
+ }
157
+
158
+ const product = await strapi.db.query(PRODUCT_UID).findOne({
159
+ where: { id: pid },
160
+ select: ['id', 'name', 'price', 'sku', 'stock_status', 'stock_quantity'],
161
+ });
162
+ if (!product) {
163
+ const err = new Error('Product not found');
164
+ // @ts-ignore
165
+ err.status = 404;
166
+ throw err;
167
+ }
168
+
169
+ // Stock validation:
170
+ // - If stock_quantity is null/undefined => treat as unlimited (common for digital products)
171
+ // - Else enforce quantity <= stock_quantity when not on backorder
172
+ if (
173
+ product.stock_status === 'out_of_stock' ||
174
+ (product.stock_quantity !== null &&
175
+ product.stock_quantity !== undefined &&
176
+ product.stock_status !== 'on_backorder' &&
177
+ product.stock_quantity < qty)
178
+ ) {
179
+ const available =
180
+ product.stock_quantity === null || product.stock_quantity === undefined
181
+ ? null
182
+ : product.stock_quantity;
183
+ const err = new Error(
184
+ available === null
185
+ ? 'Product is out of stock'
186
+ : `Insufficient stock. Available: ${available}, Requested: ${qty}`
187
+ );
188
+ // @ts-ignore
189
+ err.status = 400;
190
+ throw err;
191
+ }
192
+
193
+ const unitPrice = Number(product.price || 0);
194
+
195
+ const existing = await strapi.db.query(CART_ITEM_UID).findOne({
196
+ where: { cart: cartId, product: product.id },
197
+ select: ['id', 'quantity'],
198
+ });
199
+
200
+ if (existing?.id) {
201
+ const newQty = (Number(existing.quantity) || 0) + qty;
202
+ if (
203
+ product.stock_quantity !== null &&
204
+ product.stock_quantity !== undefined &&
205
+ product.stock_status !== 'on_backorder' &&
206
+ product.stock_quantity < newQty
207
+ ) {
208
+ const err = new Error(
209
+ `Insufficient stock for updated quantity. Available: ${product.stock_quantity}, Total requested: ${newQty}`
210
+ );
211
+ // @ts-ignore
212
+ err.status = 400;
213
+ throw err;
214
+ }
215
+
216
+ return await strapi.db.query(CART_ITEM_UID).update({
217
+ where: { id: existing.id },
218
+ data: {
219
+ user: userId || null,
220
+ cart: cartId,
221
+ quantity: newQty,
222
+ unit_price: unitPrice,
223
+ total_price: unitPrice * newQty,
224
+ },
225
+ });
226
+ }
227
+
228
+ return await strapi.db.query(CART_ITEM_UID).create({
229
+ data: {
230
+ user: userId || null,
231
+ cart: cartId,
232
+ product: product.id,
233
+ quantity: qty,
234
+ unit_price: unitPrice,
235
+ total_price: unitPrice * qty,
236
+ },
237
+ });
238
+ },
239
+
240
+ async updateItemQuantity({ cartId, userId, cartItemId, quantity }) {
241
+ const qty = asQty(quantity);
242
+ const id = asInt(cartItemId);
243
+ if (!id) {
244
+ const err = new Error('Cart item ID is required');
245
+ // @ts-ignore
246
+ err.status = 400;
247
+ throw err;
248
+ }
249
+ if (!qty) {
250
+ const err = new Error('Quantity must be an integer >= 1');
251
+ // @ts-ignore
252
+ err.status = 400;
253
+ throw err;
254
+ }
255
+
256
+ const existing = await strapi.db.query(CART_ITEM_UID).findOne({
257
+ where: { id, cart: cartId },
258
+ populate: { product: { select: ['id', 'price', 'stock_status', 'stock_quantity', 'name'] } },
259
+ });
260
+ if (!existing) {
261
+ const err = new Error('Cart item not found');
262
+ // @ts-ignore
263
+ err.status = 404;
264
+ throw err;
265
+ }
266
+
267
+ const product = existing.product;
268
+ if (!product) {
269
+ const err = new Error('Product not found for this cart item');
270
+ // @ts-ignore
271
+ err.status = 400;
272
+ throw err;
273
+ }
274
+
275
+ if (
276
+ product.stock_status === 'out_of_stock' ||
277
+ (product.stock_quantity !== null &&
278
+ product.stock_quantity !== undefined &&
279
+ product.stock_status !== 'on_backorder' &&
280
+ product.stock_quantity < qty)
281
+ ) {
282
+ const available =
283
+ product.stock_quantity === null || product.stock_quantity === undefined
284
+ ? null
285
+ : product.stock_quantity;
286
+ const err = new Error(
287
+ available === null
288
+ ? `Product is out of stock: ${product.name}`
289
+ : `Insufficient stock. Available: ${available}, Requested: ${qty}`
290
+ );
291
+ // @ts-ignore
292
+ err.status = 400;
293
+ throw err;
294
+ }
295
+
296
+ const unitPrice = Number(product.price || existing.unit_price || 0);
297
+ return await strapi.db.query(CART_ITEM_UID).update({
298
+ where: { id: existing.id },
299
+ data: {
300
+ user: userId || null,
301
+ cart: cartId,
302
+ quantity: qty,
303
+ unit_price: unitPrice,
304
+ total_price: unitPrice * qty,
305
+ },
306
+ });
307
+ },
308
+
309
+ async removeItem({ cartId, cartItemId }) {
310
+ const id = asInt(cartItemId);
311
+ if (!id) {
312
+ const err = new Error('Cart item ID is required');
313
+ // @ts-ignore
314
+ err.status = 400;
315
+ throw err;
316
+ }
317
+
318
+ const existing = await strapi.db.query(CART_ITEM_UID).findOne({
319
+ where: { id, cart: cartId },
320
+ select: ['id'],
321
+ });
322
+ if (!existing) {
323
+ const err = new Error('Cart item not found');
324
+ // @ts-ignore
325
+ err.status = 404;
326
+ throw err;
327
+ }
328
+
329
+ await strapi.db.query(CART_ITEM_UID).delete({ where: { id: existing.id } });
330
+ return true;
331
+ },
332
+
333
+ async clearCart(cartId) {
334
+ await strapi.db.query(CART_ITEM_UID).deleteMany({
335
+ where: { cart: cartId },
336
+ });
337
+ return true;
338
+ },
339
+
340
+ async validateAndApplyCoupon({ cartId, couponCode }) {
341
+ if (!couponCode || typeof couponCode !== 'string' || !couponCode.trim()) {
342
+ const err = new Error('Coupon code is required');
343
+ err.status = 400;
344
+ throw err;
345
+ }
346
+
347
+ // Find coupon by code (case-insensitive search)
348
+ const normalizedCode = couponCode.trim();
349
+ strapi.log.info(`[webbycommerce] Looking for coupon code: "${normalizedCode}"`);
350
+
351
+ let coupon = null;
352
+
353
+ // Try multiple approaches to find the coupon
354
+ try {
355
+ // Approach 1: Use entityService with simple filter
356
+ const coupons = await strapi.entityService.findMany(COUPON_UID, {
357
+ filters: {
358
+ code: normalizedCode,
359
+ },
360
+ });
361
+
362
+ strapi.log.debug(`[webbycommerce] entityService found ${coupons?.length || 0} coupons with exact match`);
363
+ if (coupons && Array.isArray(coupons) && coupons.length > 0) {
364
+ coupon = coupons[0];
365
+ strapi.log.debug(`[webbycommerce] Found via entityService: "${coupon.code}"`);
366
+ }
367
+ } catch (entityServiceError) {
368
+ strapi.log.warn(`[webbycommerce] entityService query failed:`, entityServiceError.message);
369
+ }
370
+
371
+ // Approach 2: If not found, try db.query with exact match
372
+ if (!coupon) {
373
+ try {
374
+ coupon = await strapi.db.query(COUPON_UID).findOne({
375
+ where: { code: normalizedCode },
376
+ });
377
+ if (coupon) {
378
+ strapi.log.debug(`[webbycommerce] Found via db.query: "${coupon.code}"`);
379
+ } else {
380
+ strapi.log.debug(`[webbycommerce] db.query exact match returned null`);
381
+ }
382
+ } catch (dbError) {
383
+ strapi.log.warn(`[webbycommerce] db.query exact match failed:`, dbError.message);
384
+ }
385
+ }
386
+
387
+ // Approach 2b: Try db.query with case variations
388
+ if (!coupon) {
389
+ try {
390
+ coupon = await strapi.db.query(COUPON_UID).findOne({
391
+ where: { code: normalizedCode.toUpperCase() },
392
+ });
393
+ if (coupon) {
394
+ strapi.log.debug(`[webbycommerce] Found via db.query (uppercase): "${coupon.code}"`);
395
+ }
396
+ } catch (dbError) {
397
+ // Ignore
398
+ }
399
+ }
400
+
401
+ if (!coupon) {
402
+ try {
403
+ coupon = await strapi.db.query(COUPON_UID).findOne({
404
+ where: { code: normalizedCode.toLowerCase() },
405
+ });
406
+ if (coupon) {
407
+ strapi.log.debug(`[webbycommerce] Found via db.query (lowercase): "${coupon.code}"`);
408
+ }
409
+ } catch (dbError) {
410
+ // Ignore
411
+ }
412
+ }
413
+
414
+ // Approach 3: If still not found, get all coupons and do case-insensitive match
415
+ if (!coupon) {
416
+ try {
417
+ strapi.log.debug(`[webbycommerce] Trying fallback: fetching all coupons for case-insensitive match`);
418
+ const allCoupons = await strapi.entityService.findMany(COUPON_UID, {
419
+ filters: {},
420
+ });
421
+
422
+ strapi.log.debug(`[webbycommerce] Fetched ${allCoupons?.length || 0} total coupons`);
423
+
424
+ if (allCoupons && Array.isArray(allCoupons)) {
425
+ // Log all coupon codes for debugging
426
+ const allCodes = allCoupons.map(c => `"${c.code || 'N/A'}"`).join(', ');
427
+ strapi.log.debug(`[webbycommerce] All coupon codes: ${allCodes}`);
428
+
429
+ // Case-insensitive match
430
+ coupon = allCoupons.find(
431
+ (c) => {
432
+ const couponCode = c.code ? String(c.code).trim() : '';
433
+ const searchCode = normalizedCode.toLowerCase();
434
+ const match = couponCode.toLowerCase() === searchCode;
435
+ if (match) {
436
+ strapi.log.debug(`[webbycommerce] Case-insensitive match found: "${couponCode}" === "${normalizedCode}"`);
437
+ }
438
+ return match;
439
+ }
440
+ );
441
+ }
442
+ } catch (fallbackError) {
443
+ strapi.log.error(`[webbycommerce] Fallback query failed:`, fallbackError.message, fallbackError.stack);
444
+ }
445
+ }
446
+
447
+ // If still not found, log for debugging
448
+ if (!coupon) {
449
+ strapi.log.error(`[webbycommerce] Coupon not found: "${normalizedCode}"`);
450
+ // List available coupons for debugging
451
+ try {
452
+ const availableCoupons = await strapi.entityService.findMany(COUPON_UID, {
453
+ filters: {},
454
+ });
455
+ if (availableCoupons && Array.isArray(availableCoupons)) {
456
+ const codes = availableCoupons.slice(0, 20).map(c => `"${c.code || 'N/A'}"`).join(', ');
457
+ strapi.log.error(`[webbycommerce] Available coupons (${availableCoupons.length}): ${codes}`);
458
+ } else {
459
+ strapi.log.error(`[webbycommerce] No coupons found in database or query returned invalid format`);
460
+ }
461
+ } catch (debugError) {
462
+ strapi.log.error(`[webbycommerce] Error fetching coupons for debug:`, debugError.message, debugError.stack);
463
+ }
464
+ const err = new Error('Invalid coupon code');
465
+ err.status = 400;
466
+ throw err;
467
+ }
468
+
469
+ strapi.log.info(`[webbycommerce] Found coupon: "${coupon.code}" (ID: ${coupon.id}, Active: ${coupon.is_active})`);
470
+
471
+ // Check if coupon is active
472
+ if (coupon.is_active === false) {
473
+ const err = new Error('This coupon is not active');
474
+ err.status = 400;
475
+ throw err;
476
+ }
477
+
478
+ // Check if coupon has expired
479
+ if (coupon.expires_at) {
480
+ const now = new Date();
481
+ const expiresAt = new Date(coupon.expires_at);
482
+ if (expiresAt < now) {
483
+ const err = new Error('This coupon has expired');
484
+ err.status = 400;
485
+ throw err;
486
+ }
487
+ }
488
+
489
+ // Check usage limit
490
+ if (coupon.usage_limit !== null && coupon.usage_limit !== undefined) {
491
+ const usedCount = coupon.used_count || 0;
492
+ if (usedCount >= coupon.usage_limit) {
493
+ const err = new Error('This coupon has reached its usage limit');
494
+ err.status = 400;
495
+ throw err;
496
+ }
497
+ }
498
+
499
+ // Get cart items to calculate subtotal
500
+ const items = await this.getCartItems({ cartId });
501
+ const subtotal = items.reduce((sum, it) => sum + (Number(it.total_price) || 0), 0);
502
+
503
+ // Check minimum order amount
504
+ if (coupon.minimum_order_amount !== null && coupon.minimum_order_amount !== undefined) {
505
+ if (subtotal < Number(coupon.minimum_order_amount)) {
506
+ const err = new Error(
507
+ `Minimum order amount of ${coupon.minimum_order_amount} required for this coupon`
508
+ );
509
+ err.status = 400;
510
+ throw err;
511
+ }
512
+ }
513
+
514
+ // Apply coupon to cart
515
+ await strapi.db.query(CART_UID).update({
516
+ where: { id: cartId },
517
+ data: { coupon: coupon.id },
518
+ });
519
+
520
+ return coupon;
521
+ },
522
+
523
+ async removeCoupon({ cartId }) {
524
+ await strapi.db.query(CART_UID).update({
525
+ where: { id: cartId },
526
+ data: { coupon: null },
527
+ });
528
+ return true;
529
+ },
530
+ }));
531
+