@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,1100 @@
1
+ 'use strict';
2
+
3
+ const { ensureEcommercePermission } = require('../utils/check-ecommerce-permission');
4
+ const { sendEmail } = require('../utils/send-email');
5
+
6
+ /**
7
+ * Order controller
8
+ */
9
+
10
+ module.exports = {
11
+ // Create checkout order
12
+ async checkout(ctx) {
13
+ try {
14
+ const user = ctx.state.user;
15
+ if (!user) {
16
+ return ctx.unauthorized('Authentication required');
17
+ }
18
+
19
+ // Check ecommerce permission
20
+ const hasPermission = await ensureEcommercePermission(ctx);
21
+ if (!hasPermission) {
22
+ return;
23
+ }
24
+
25
+ const {
26
+ billing_address,
27
+ shipping_address,
28
+ payment_method,
29
+ shipping_method,
30
+ notes,
31
+ tax_amount,
32
+ shipping_amount,
33
+ discount_amount
34
+ } = ctx.request.body;
35
+
36
+ // Validate required fields
37
+ if (!billing_address || !shipping_address || !payment_method) {
38
+ return ctx.badRequest('Billing address, shipping address, and payment method are required');
39
+ }
40
+
41
+ // Payment method is an enum string (e.g. "Stripe", "PayPal", "Razorpay", "COD").
42
+ // Handle case where payment_method might be an object
43
+ let normalizedPaymentMethod = 'COD'; // Default fallback
44
+ if (payment_method) {
45
+ if (typeof payment_method === 'object' && payment_method !== null) {
46
+ // Extract from object
47
+ normalizedPaymentMethod = payment_method.type || payment_method.method || payment_method.name || 'COD';
48
+ } else if (typeof payment_method === 'string') {
49
+ // Use string directly
50
+ normalizedPaymentMethod = payment_method;
51
+ }
52
+ }
53
+
54
+ // Get cart items
55
+ const cart = await strapi.db.query('plugin::webbycommerce.cart').findOne({
56
+ where: { user: user.id },
57
+ select: ['id'],
58
+ });
59
+
60
+ const cartItems = cart?.id
61
+ ? await strapi.db.query('plugin::webbycommerce.cart-item').findMany({
62
+ where: { cart: cart.id },
63
+ populate: {
64
+ product: {
65
+ populate: ['images'],
66
+ },
67
+ },
68
+ })
69
+ : [];
70
+
71
+ if (cartItems.length === 0) {
72
+ return ctx.badRequest('Cart is empty');
73
+ }
74
+
75
+ // Validate cart items and calculate totals
76
+ let subtotal = 0;
77
+ let totalItems = 0;
78
+ const orderItems = [];
79
+
80
+ for (const cartItem of cartItems) {
81
+ const product = cartItem.product;
82
+
83
+ // Check if product exists
84
+ if (!product) {
85
+ return ctx.badRequest(`Product with ID ${cartItem.product} not found`);
86
+ }
87
+
88
+ // Check stock availability
89
+ if (product.stock_status !== 'in_stock' || product.stock_quantity < cartItem.quantity) {
90
+ return ctx.badRequest(`Insufficient stock for product: ${product.name}`);
91
+ }
92
+
93
+ // Calculate item total
94
+ const itemPrice = parseFloat(product.price);
95
+ const itemTotal = itemPrice * cartItem.quantity;
96
+ subtotal += itemTotal;
97
+ totalItems += cartItem.quantity;
98
+
99
+ // Prepare order item
100
+ orderItems.push({
101
+ product_id: product.id,
102
+ product_name: product.name,
103
+ product_sku: product.sku,
104
+ product_price: itemPrice,
105
+ quantity: cartItem.quantity,
106
+ total_price: itemTotal,
107
+ product_image: product.images?.[0]?.url || null,
108
+ });
109
+ }
110
+
111
+ // Calculate totals using values from request (default to 0 if not provided)
112
+ const taxAmount = tax_amount != null ? (parseFloat(tax_amount) || 0) : 0;
113
+ const finalShippingAmount = shipping_amount != null ? (parseFloat(shipping_amount) || 0) : 0;
114
+ const finalDiscountAmount = discount_amount != null ? (parseFloat(discount_amount) || 0) : 0;
115
+ const total = subtotal + taxAmount + finalShippingAmount - finalDiscountAmount;
116
+
117
+ // Generate unique order number
118
+ const orderNumber = await this.generateOrderNumber();
119
+
120
+ // Prepare items connection for manyToMany relation
121
+ const itemsConnect = cartItems.length > 0
122
+ ? { connect: cartItems.map(item => ({ id: item.product.id })) }
123
+ : undefined;
124
+
125
+ // Create order
126
+ const orderData = {
127
+ order_number: orderNumber,
128
+ status: 'pending',
129
+ user: user.id,
130
+ subtotal: subtotal.toFixed(2),
131
+ tax_amount: taxAmount.toFixed(2),
132
+ shipping_amount: finalShippingAmount.toFixed(2), // Use from request
133
+ discount_amount: finalDiscountAmount.toFixed(2), // Use from request
134
+ total: total.toFixed(2),
135
+ currency: 'USD',
136
+ billing_address,
137
+ shipping_address,
138
+ payment_method: normalizedPaymentMethod, // Store as string
139
+ payment_status: 'pending',
140
+ shipping_method: shipping_method || null,
141
+ notes: notes || null,
142
+ };
143
+
144
+ if (itemsConnect) {
145
+ orderData.items = itemsConnect;
146
+ }
147
+
148
+ const order = await strapi.db.query('plugin::webbycommerce.order').create({
149
+ data: orderData,
150
+ });
151
+
152
+ // Update product stock quantities
153
+ for (const cartItem of cartItems) {
154
+ const newStockQuantity = cartItem.product.stock_quantity - cartItem.quantity;
155
+ const newStockStatus = newStockQuantity <= 0 ? 'out_of_stock' : 'in_stock';
156
+
157
+ await strapi.db.query('plugin::webbycommerce.product').update({
158
+ where: { id: cartItem.product.id },
159
+ data: {
160
+ stock_quantity: newStockQuantity,
161
+ stock_status: newStockStatus,
162
+ },
163
+ });
164
+ }
165
+
166
+ // Clear cart after successful order creation
167
+ await strapi.db.query('plugin::webbycommerce.cart-item').deleteMany({
168
+ where: cart?.id ? { cart: cart.id } : { user: user.id },
169
+ });
170
+
171
+ // Send order confirmation email (optional)
172
+ try {
173
+ await this.sendOrderConfirmationEmail(user, order);
174
+ } catch (emailError) {
175
+ strapi.log.error('Failed to send order confirmation email:', emailError);
176
+ // Don't fail the order if email fails
177
+ }
178
+
179
+ // Return order details - include all fields for thank you page
180
+ ctx.send({
181
+ data: {
182
+ order_id: order.id,
183
+ order_number: order.order_number,
184
+ status: order.status,
185
+ subtotal: parseFloat(order.subtotal),
186
+ tax_amount: parseFloat(order.tax_amount),
187
+ shipping_amount: parseFloat(order.shipping_amount),
188
+ discount_amount: parseFloat(order.discount_amount),
189
+ total: parseFloat(order.total),
190
+ currency: order.currency,
191
+ payment_method: order.payment_method, // Return payment method as string
192
+ shipping_method: order.shipping_method,
193
+ items: order.items,
194
+ created_at: order.createdAt,
195
+ },
196
+ message: 'Order created successfully',
197
+ });
198
+
199
+ } catch (error) {
200
+ strapi.log.error('Checkout error:', error);
201
+ ctx.badRequest('Failed to process checkout');
202
+ }
203
+ },
204
+
205
+ // Get user orders
206
+ async getOrders(ctx) {
207
+ try {
208
+ const user = ctx.state.user;
209
+ if (!user) {
210
+ return ctx.unauthorized('Authentication required');
211
+ }
212
+
213
+ // Check ecommerce permission
214
+ const hasPermission = await ensureEcommercePermission(ctx);
215
+ if (!hasPermission) {
216
+ return;
217
+ }
218
+
219
+ const { page = 1, limit = 10, status } = ctx.query;
220
+
221
+ const query = {
222
+ where: { user: user.id },
223
+ orderBy: { createdAt: 'desc' },
224
+ populate: ['items', 'billing_address', 'shipping_address', 'user', 'payment_transactions'],
225
+ limit: parseInt(limit),
226
+ offset: (parseInt(page) - 1) * parseInt(limit),
227
+ };
228
+
229
+ if (status) {
230
+ query.where.status = status;
231
+ }
232
+
233
+ const orders = await strapi.db.query('plugin::webbycommerce.order').findMany(query);
234
+ const total = await strapi.db.query('plugin::webbycommerce.order').count({
235
+ where: { user: user.id, ...(status && { status }) },
236
+ });
237
+
238
+ // Format orders with all data
239
+ const formattedOrders = await Promise.all(orders.map(async (order) => {
240
+ // Format items with full product details
241
+ let formattedItems = [];
242
+ if (order.items && order.items.length > 0) {
243
+ const itemIds = order.items.map(item => typeof item === 'object' && item.id ? item.id : item);
244
+
245
+ const products = await strapi.db.query('plugin::webbycommerce.product').findMany({
246
+ where: {
247
+ id: { $in: itemIds },
248
+ },
249
+ populate: {
250
+ images: true,
251
+ },
252
+ });
253
+
254
+ formattedItems = products.map(product => ({
255
+ id: product.id,
256
+ name: product.name,
257
+ sku: product.sku,
258
+ price: parseFloat(product.price || 0),
259
+ sale_price: product.sale_price ? parseFloat(product.sale_price) : null,
260
+ images: product.images || [],
261
+ slug: product.slug,
262
+ description: product.description,
263
+ }));
264
+ } else if (order.items && Array.isArray(order.items)) {
265
+ // Items are already populated, format them
266
+ formattedItems = order.items.map(item => ({
267
+ id: item.id,
268
+ name: item.name,
269
+ sku: item.sku,
270
+ price: parseFloat(item.price || 0),
271
+ sale_price: item.sale_price ? parseFloat(item.sale_price) : null,
272
+ images: item.images || [],
273
+ slug: item.slug,
274
+ description: item.description,
275
+ }));
276
+ }
277
+
278
+ const shippingAmount = parseFloat(order.shipping_amount || 0);
279
+ const subtotalAmount = parseFloat(order.subtotal || 0);
280
+ const discountAmount = parseFloat(order.discount_amount || 0);
281
+
282
+ return {
283
+ id: order.id,
284
+ order_number: order.order_number,
285
+ status: order.status,
286
+ payment_status: order.payment_status,
287
+ items: formattedItems,
288
+ items_count: formattedItems.length,
289
+ subtotal: subtotalAmount,
290
+ tax_amount: parseFloat(order.tax_amount || 0),
291
+ shipping: shippingAmount,
292
+ shipping_amount: shippingAmount,
293
+ discount: discountAmount,
294
+ discount_amount: discountAmount,
295
+ total: parseFloat(order.total || 0),
296
+ currency: order.currency,
297
+ billing_address: order.billing_address,
298
+ shipping_address: order.shipping_address,
299
+ payment_method: (() => {
300
+ // Ensure payment_method is always a string
301
+ if (!order.payment_method) return 'N/A';
302
+ if (typeof order.payment_method === 'object' && order.payment_method !== null) {
303
+ return order.payment_method.type || order.payment_method.method || order.payment_method.name || String(order.payment_method);
304
+ }
305
+ return String(order.payment_method);
306
+ })(),
307
+ shipping_method: order.shipping_method,
308
+ notes: order.notes,
309
+ tracking_number: order.tracking_number,
310
+ estimated_delivery: order.estimated_delivery,
311
+ payment_transactions: order.payment_transactions || [],
312
+ user: order.user ? {
313
+ id: order.user.id,
314
+ username: order.user.username,
315
+ email: order.user.email,
316
+ } : null,
317
+ created_at: order.createdAt,
318
+ updated_at: order.updatedAt,
319
+ };
320
+ }));
321
+
322
+ ctx.send({
323
+ data: formattedOrders,
324
+ meta: {
325
+ pagination: {
326
+ page: parseInt(page),
327
+ limit: parseInt(limit),
328
+ total,
329
+ pages: Math.ceil(total / parseInt(limit)),
330
+ },
331
+ },
332
+ });
333
+
334
+ } catch (error) {
335
+ strapi.log.error('Get orders error:', error);
336
+ ctx.badRequest('Failed to retrieve orders');
337
+ }
338
+ },
339
+
340
+ // Get specific order
341
+ async getOrder(ctx) {
342
+ try {
343
+ const user = ctx.state.user;
344
+ if (!user) {
345
+ return ctx.unauthorized('Authentication required');
346
+ }
347
+
348
+ // Check ecommerce permission
349
+ const hasPermission = await ensureEcommercePermission(ctx);
350
+ if (!hasPermission) {
351
+ return;
352
+ }
353
+
354
+ const { id } = ctx.params;
355
+
356
+ if (!id) {
357
+ return ctx.badRequest('Order ID or order number is required');
358
+ }
359
+
360
+ // Determine if id is a numeric ID or an order_number (starts with ORD-)
361
+ const isOrderNumber = id.toString().startsWith('ORD-');
362
+
363
+ const whereClause = {
364
+ user: user.id, // Filter by user ID directly for security
365
+ };
366
+
367
+ if (isOrderNumber) {
368
+ whereClause.order_number = id;
369
+ } else {
370
+ whereClause.id = id;
371
+ }
372
+
373
+ // First, get the order with basic populate
374
+ let order = await strapi.db.query('plugin::webbycommerce.order').findOne({
375
+ where: whereClause,
376
+ populate: ['billing_address', 'shipping_address', 'items', 'user'],
377
+ });
378
+
379
+ if (!order) {
380
+ // Don't reveal if order exists but belongs to another user
381
+ return ctx.notFound('Order not found');
382
+ }
383
+
384
+ // If items exist but aren't fully populated, fetch them separately
385
+ let formattedItems = [];
386
+ if (order.items && order.items.length > 0) {
387
+ // Items might be just IDs, so fetch full product details
388
+ const itemIds = order.items.map(item => typeof item === 'object' && item.id ? item.id : item);
389
+
390
+ const products = await strapi.db.query('plugin::webbycommerce.product').findMany({
391
+ where: {
392
+ id: { $in: itemIds },
393
+ },
394
+ populate: {
395
+ images: true,
396
+ },
397
+ });
398
+
399
+ formattedItems = products.map(product => ({
400
+ id: product.id,
401
+ name: product.name,
402
+ sku: product.sku,
403
+ price: parseFloat(product.price || 0),
404
+ sale_price: product.sale_price ? parseFloat(product.sale_price) : null,
405
+ images: product.images || [],
406
+ slug: product.slug,
407
+ description: product.description,
408
+ }));
409
+ } else if (order.items && Array.isArray(order.items)) {
410
+ // Items are already populated, format them
411
+ formattedItems = order.items.map(item => ({
412
+ id: item.id,
413
+ name: item.name,
414
+ sku: item.sku,
415
+ price: parseFloat(item.price || 0),
416
+ sale_price: item.sale_price ? parseFloat(item.sale_price) : null,
417
+ images: item.images || [],
418
+ slug: item.slug,
419
+ description: item.description,
420
+ }));
421
+ }
422
+
423
+ const shippingAmount = parseFloat(order.shipping_amount || 0);
424
+ const subtotalAmount = parseFloat(order.subtotal || 0);
425
+ const discountAmount = parseFloat(order.discount_amount || 0);
426
+
427
+ ctx.send({
428
+ data: {
429
+ id: order.id,
430
+ order_number: order.order_number,
431
+ status: order.status,
432
+ payment_status: order.payment_status,
433
+ items: formattedItems,
434
+ items_count: formattedItems.length,
435
+ subtotal: subtotalAmount,
436
+ tax_amount: parseFloat(order.tax_amount || 0),
437
+ shipping: shippingAmount,
438
+ shipping_amount: shippingAmount,
439
+ discount: discountAmount,
440
+ discount_amount: discountAmount,
441
+ total: parseFloat(order.total || 0),
442
+ currency: order.currency,
443
+ billing_address: order.billing_address,
444
+ shipping_address: order.shipping_address,
445
+ payment_method: (() => {
446
+ // Ensure payment_method is always a string
447
+ if (!order.payment_method) return 'N/A';
448
+ if (typeof order.payment_method === 'object' && order.payment_method !== null) {
449
+ return order.payment_method.type || order.payment_method.method || order.payment_method.name || String(order.payment_method);
450
+ }
451
+ return String(order.payment_method);
452
+ })(),
453
+ shipping_method: order.shipping_method,
454
+ notes: order.notes,
455
+ tracking_number: order.tracking_number,
456
+ estimated_delivery: order.estimated_delivery,
457
+ created_at: order.createdAt,
458
+ updated_at: order.updatedAt,
459
+ },
460
+ });
461
+
462
+ } catch (error) {
463
+ strapi.log.error('Get order error:', error);
464
+ ctx.badRequest('Failed to retrieve order');
465
+ }
466
+ },
467
+
468
+ // Cancel order (only if pending or processing)
469
+ async cancelOrder(ctx) {
470
+ try {
471
+ const user = ctx.state.user;
472
+ if (!user) {
473
+ return ctx.unauthorized('Authentication required');
474
+ }
475
+
476
+ // Check ecommerce permission
477
+ try {
478
+ const hasPermission = await ensureEcommercePermission(ctx);
479
+ if (!hasPermission) {
480
+ return; // ensureEcommercePermission already sent the response
481
+ }
482
+ } catch (permissionError) {
483
+ strapi.log.error(`[cancelOrder] Error checking permission:`, permissionError);
484
+ return ctx.badRequest('Permission check failed');
485
+ }
486
+
487
+ const { id } = ctx.params;
488
+
489
+ if (!id) {
490
+ return ctx.badRequest('Order ID is required');
491
+ }
492
+
493
+ // Normalize ID (handle both string and number)
494
+ const orderId = typeof id === 'string' ? (isNaN(id) ? id : parseInt(id, 10)) : id;
495
+
496
+ strapi.log.info(`[cancelOrder] Attempting to cancel order ${orderId} (original: ${id}, type: ${typeof id}) for user ${user.id}`);
497
+
498
+ // Query order - try with normalized ID first
499
+ let order;
500
+ try {
501
+ order = await strapi.db.query('plugin::webbycommerce.order').findOne({
502
+ where: {
503
+ id: orderId,
504
+ user: user.id, // Filter by user ID directly for security
505
+ },
506
+ populate: ['items', 'user'],
507
+ });
508
+
509
+ // If not found with normalized ID, try with original ID
510
+ if (!order && orderId !== id) {
511
+ strapi.log.info(`[cancelOrder] Order not found with normalized ID ${orderId}, trying original ID ${id}`);
512
+ order = await strapi.db.query('plugin::webbycommerce.order').findOne({
513
+ where: {
514
+ id: id,
515
+ user: user.id,
516
+ },
517
+ populate: ['items', 'user'],
518
+ });
519
+ }
520
+ } catch (queryError) {
521
+ strapi.log.error(`[cancelOrder] Error querying order ${orderId}:`, queryError);
522
+ strapi.log.error(`[cancelOrder] Query error message:`, queryError.message);
523
+ return ctx.badRequest(`Failed to retrieve order: ${queryError.message || 'Database error'}`);
524
+ }
525
+
526
+ if (!order) {
527
+ strapi.log.warn(`[cancelOrder] Order ${orderId} not found for user ${user.id}`);
528
+ // Don't reveal if order exists but belongs to another user
529
+ return ctx.notFound('Order not found');
530
+ }
531
+
532
+ strapi.log.info(`[cancelOrder] Found order ${order.id} (order_number: ${order.order_number}) with status: ${order.status || 'null/undefined'}`);
533
+
534
+ // Check if order can be cancelled
535
+ const orderStatus = order.status || '';
536
+ const cancellableStatuses = ['pending', 'processing'];
537
+
538
+ if (!orderStatus) {
539
+ strapi.log.warn(`[cancelOrder] Order ${order.id} has no status`);
540
+ return ctx.badRequest('Order status is invalid');
541
+ }
542
+
543
+ if (!cancellableStatuses.includes(orderStatus)) {
544
+ if (orderStatus === 'cancelled') {
545
+ return ctx.badRequest('Order is already cancelled');
546
+ }
547
+ if (orderStatus === 'delivered') {
548
+ return ctx.badRequest('Delivered orders cannot be cancelled');
549
+ }
550
+ if (orderStatus === 'shipped') {
551
+ return ctx.badRequest('Shipped orders cannot be cancelled. Please contact support for returns.');
552
+ }
553
+ return ctx.badRequest(`Order with status '${orderStatus}' cannot be cancelled`);
554
+ }
555
+
556
+ // Update order status
557
+ let updatedOrder;
558
+ try {
559
+ updatedOrder = await strapi.db.query('plugin::webbycommerce.order').update({
560
+ where: { id: order.id },
561
+ data: { status: 'cancelled' },
562
+ });
563
+
564
+ if (!updatedOrder) {
565
+ strapi.log.error(`[cancelOrder] Failed to update order ${order.id} status - update returned null`);
566
+ return ctx.badRequest('Failed to update order status');
567
+ }
568
+
569
+ strapi.log.info(`[cancelOrder] Successfully updated order ${order.id} status to cancelled`);
570
+ } catch (updateError) {
571
+ strapi.log.error(`[cancelOrder] Error updating order ${order.id}:`, updateError);
572
+ strapi.log.error(`[cancelOrder] Update error message:`, updateError.message);
573
+ strapi.log.error(`[cancelOrder] Update error stack:`, updateError.stack);
574
+ return ctx.badRequest(`Failed to update order status: ${updateError.message || 'Unknown error'}`);
575
+ }
576
+
577
+ // Restore product stock quantities (non-blocking - don't fail cancellation if this fails)
578
+ // Note: order.items is a manyToMany relation to products, so items are product objects
579
+ // Since quantities aren't stored in the order, we'll restore 1 unit per product
580
+ // This is a limitation of the current schema - ideally quantities should be stored
581
+ if (order.items && Array.isArray(order.items) && order.items.length > 0) {
582
+ try {
583
+ for (const item of order.items) {
584
+ // item is a product object from the manyToMany relation
585
+ const productId = typeof item === 'object' && item.id ? item.id : item;
586
+
587
+ if (!productId) {
588
+ strapi.log.warn(`[cancelOrder] Skipping item with invalid ID for order ${order.id}`);
589
+ continue;
590
+ }
591
+
592
+ try {
593
+ const product = await strapi.db.query('plugin::webbycommerce.product').findOne({
594
+ where: { id: productId },
595
+ });
596
+
597
+ if (product) {
598
+ // Since we don't have exact quantities, restore 1 unit per product
599
+ // This is a limitation - ideally order items should store quantities
600
+ const quantityToRestore = 1;
601
+ const newStockQuantity = (product.stock_quantity || 0) + quantityToRestore;
602
+ const newStockStatus = newStockQuantity > 0 ? 'in_stock' : 'out_of_stock';
603
+
604
+ await strapi.db.query('plugin::webbycommerce.product').update({
605
+ where: { id: productId },
606
+ data: {
607
+ stock_quantity: newStockQuantity,
608
+ stock_status: newStockStatus,
609
+ },
610
+ });
611
+
612
+ strapi.log.info(`[cancelOrder] Restored ${quantityToRestore} unit(s) of product ${productId} for order ${order.id}`);
613
+ } else {
614
+ strapi.log.warn(`[cancelOrder] Product ${productId} not found for order ${order.id}`);
615
+ }
616
+ } catch (productError) {
617
+ strapi.log.error(`[cancelOrder] Error processing product ${productId} for order ${order.id}:`, productError);
618
+ // Continue with other products
619
+ }
620
+ }
621
+ } catch (stockError) {
622
+ // Log error but don't fail the cancellation
623
+ strapi.log.error(`[cancelOrder] Error restoring stock for order ${order.id}:`, stockError);
624
+ }
625
+ }
626
+
627
+ // Send success response
628
+ try {
629
+ return ctx.send({
630
+ data: {
631
+ id: updatedOrder.id,
632
+ order_number: updatedOrder.order_number,
633
+ status: updatedOrder.status,
634
+ },
635
+ message: 'Order cancelled successfully',
636
+ });
637
+ } catch (sendError) {
638
+ strapi.log.error(`[cancelOrder] Error sending response for order ${order.id}:`, sendError);
639
+ // Even if sending response fails, order is already cancelled
640
+ return ctx.send({
641
+ data: {
642
+ id: updatedOrder.id,
643
+ order_number: updatedOrder.order_number,
644
+ status: updatedOrder.status,
645
+ },
646
+ message: 'Order cancelled successfully',
647
+ });
648
+ }
649
+
650
+ } catch (error) {
651
+ strapi.log.error(`[cancelOrder] Unexpected error cancelling order:`, error);
652
+ strapi.log.error(`[cancelOrder] Error name:`, error.name);
653
+ strapi.log.error(`[cancelOrder] Error message:`, error.message);
654
+ strapi.log.error(`[cancelOrder] Error stack:`, error.stack);
655
+
656
+ // Provide more detailed error message
657
+ const errorMessage = error.message || 'Unknown error occurred';
658
+ const errorDetails = error.details ? JSON.stringify(error.details) : '';
659
+
660
+ strapi.log.error(`[cancelOrder] Full error details:`, {
661
+ name: error.name,
662
+ message: errorMessage,
663
+ details: errorDetails,
664
+ stack: error.stack,
665
+ });
666
+
667
+ return ctx.badRequest(`Failed to cancel order: ${errorMessage}${errorDetails ? ` - ${errorDetails}` : ''}`);
668
+ }
669
+ },
670
+
671
+ // Update order status (admin only)
672
+ async updateOrderStatus(ctx) {
673
+ try {
674
+ const user = ctx.state.user;
675
+ if (!user) {
676
+ return ctx.unauthorized('Authentication required');
677
+ }
678
+
679
+ // Check ecommerce permission
680
+ const hasPermission = await ensureEcommercePermission(ctx);
681
+ if (!hasPermission) {
682
+ return;
683
+ }
684
+
685
+ const { id } = ctx.params;
686
+ const { status, tracking_number, estimated_delivery, notes } = ctx.request.body;
687
+
688
+ if (!id) {
689
+ return ctx.badRequest('Order ID is required');
690
+ }
691
+
692
+ if (!status) {
693
+ return ctx.badRequest('Status is required');
694
+ }
695
+
696
+ // Validate status
697
+ const validStatuses = ['pending', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded'];
698
+ if (!validStatuses.includes(status)) {
699
+ return ctx.badRequest(`Invalid status. Must be one of: ${validStatuses.join(', ')}`);
700
+ }
701
+
702
+ // Find order
703
+ const order = await strapi.db.query('plugin::webbycommerce.order').findOne({
704
+ where: { id: id },
705
+ populate: ['user'],
706
+ });
707
+
708
+ if (!order) {
709
+ return ctx.notFound('Order not found');
710
+ }
711
+
712
+ // For regular users, they can only view their own orders
713
+ // For admins, they can update any order status
714
+ const isAdmin = user.role && (user.role.type === 'superadmin' || user.role.name === 'Administrator');
715
+ if (!isAdmin && order.user.id !== user.id) {
716
+ return ctx.forbidden('You can only update your own orders');
717
+ }
718
+
719
+ // Prepare update data
720
+ const updateData = { status };
721
+
722
+ // Add optional fields if provided
723
+ if (tracking_number !== undefined) {
724
+ updateData.tracking_number = tracking_number;
725
+ }
726
+
727
+ if (estimated_delivery !== undefined) {
728
+ updateData.estimated_delivery = new Date(estimated_delivery);
729
+ }
730
+
731
+ if (notes !== undefined) {
732
+ updateData.notes = notes;
733
+ }
734
+
735
+ // Update order
736
+ const updatedOrder = await strapi.db.query('plugin::webbycommerce.order').update({
737
+ where: { id: id },
738
+ data: updateData,
739
+ });
740
+
741
+ // Send status update email notification
742
+ try {
743
+ if (order.user.email) {
744
+ await this.sendOrderStatusUpdateEmail(order.user, updatedOrder, status);
745
+ }
746
+ } catch (emailError) {
747
+ strapi.log.error('Failed to send order status update email:', emailError);
748
+ // Don't fail the update if email fails
749
+ }
750
+
751
+ // If order is being cancelled and wasn't already cancelled, restore stock
752
+ if (status === 'cancelled' && order.status !== 'cancelled') {
753
+ await this.restoreOrderStock(order);
754
+ }
755
+
756
+ ctx.send({
757
+ data: {
758
+ id: updatedOrder.id,
759
+ order_number: updatedOrder.order_number,
760
+ status: updatedOrder.status,
761
+ tracking_number: updatedOrder.tracking_number,
762
+ estimated_delivery: updatedOrder.estimated_delivery,
763
+ updated_at: updatedOrder.updatedAt,
764
+ },
765
+ message: 'Order status updated successfully',
766
+ });
767
+
768
+ } catch (error) {
769
+ strapi.log.error('Update order status error:', error);
770
+ ctx.badRequest('Failed to update order status');
771
+ }
772
+ },
773
+
774
+ // Get order tracking information
775
+ async getOrderTracking(ctx) {
776
+ try {
777
+ const user = ctx.state.user;
778
+ if (!user) {
779
+ return ctx.unauthorized('Authentication required');
780
+ }
781
+
782
+ // Check ecommerce permission
783
+ const hasPermission = await ensureEcommercePermission(ctx);
784
+ if (!hasPermission) {
785
+ return;
786
+ }
787
+
788
+ const { id } = ctx.params;
789
+
790
+ if (!id) {
791
+ return ctx.badRequest('Order ID is required');
792
+ }
793
+
794
+ const order = await strapi.db.query('plugin::webbycommerce.order').findOne({
795
+ where: {
796
+ id: id,
797
+ user: user.id, // Filter by user ID directly for security
798
+ },
799
+ populate: ['shipping_address', 'user'],
800
+ });
801
+
802
+ if (!order) {
803
+ // Don't reveal if order exists but belongs to another user
804
+ return ctx.notFound('Order not found');
805
+ }
806
+
807
+ // Generate tracking timeline based on order status
808
+ const trackingTimeline = this.generateTrackingTimeline(order);
809
+
810
+ ctx.send({
811
+ data: {
812
+ order_id: order.id,
813
+ order_number: order.order_number,
814
+ status: order.status,
815
+ tracking_number: order.tracking_number,
816
+ estimated_delivery: order.estimated_delivery,
817
+ shipping_method: order.shipping_method,
818
+ shipping_address: order.shipping_address,
819
+ tracking_timeline: trackingTimeline,
820
+ current_location: this.getCurrentLocation(order.status),
821
+ delivery_status: this.getDeliveryStatus(order.status),
822
+ },
823
+ });
824
+
825
+ } catch (error) {
826
+ strapi.log.error('Get order tracking error:', error);
827
+ ctx.badRequest('Failed to retrieve order tracking information');
828
+ }
829
+ },
830
+
831
+ // Generate unique order number
832
+ async generateOrderNumber() {
833
+ const timestamp = Date.now();
834
+ const random = Math.floor(Math.random() * 1000).toString().padStart(3, '0');
835
+ return `ORD-${timestamp}-${random}`;
836
+ },
837
+
838
+ // Send order confirmation email
839
+ async sendOrderConfirmationEmail(user, order) {
840
+ try {
841
+ const settings = await strapi.store({ type: 'plugin', name: 'webbycommerce' }).get({ key: 'settings' });
842
+ const smtpSettings = settings?.smtp;
843
+
844
+ if (!smtpSettings) {
845
+ strapi.log.warn('SMTP settings not configured, skipping order confirmation email');
846
+ return;
847
+ }
848
+
849
+ // Ensure order has items populated
850
+ let orderWithItems = order;
851
+ if (!order.items || !Array.isArray(order.items) || order.items.length === 0) {
852
+ // Fetch order with items populated
853
+ orderWithItems = await strapi.db.query('plugin::webbycommerce.order').findOne({
854
+ where: { id: order.id },
855
+ populate: ['items'],
856
+ });
857
+ }
858
+
859
+ // Format items for email
860
+ // Note: order.items is a manyToMany relation to products, not order items with quantities
861
+ let itemsHtml = '<li>No items found</li>';
862
+
863
+ if (orderWithItems && orderWithItems.items && Array.isArray(orderWithItems.items) && orderWithItems.items.length > 0) {
864
+ // Items are product objects, not order items with quantities
865
+ // Since we don't have quantities stored, we'll just show product names
866
+ itemsHtml = orderWithItems.items.map(item => {
867
+ if (!item) return '';
868
+ const productName = item.name || item.product_name || 'Unknown Product';
869
+ const productPrice = item.price || item.product_price || 0;
870
+ // We don't have quantity, so we'll show the product name and price
871
+ return `<li>${productName} - $${parseFloat(productPrice).toFixed(2)}</li>`;
872
+ }).filter(item => item !== '').join('');
873
+
874
+ if (!itemsHtml) {
875
+ itemsHtml = '<li>No items found</li>';
876
+ }
877
+ }
878
+
879
+ const emailData = {
880
+ to: user.email,
881
+ subject: `Order Confirmation - ${order.order_number || orderWithItems?.order_number || 'N/A'}`,
882
+ html: `
883
+ <h2>Order Confirmation</h2>
884
+ <p>Dear ${user.username || 'Customer'},</p>
885
+ <p>Thank you for your order! Here are the details:</p>
886
+ <h3>Order #${order.order_number || orderWithItems?.order_number || 'N/A'}</h3>
887
+ <p><strong>Total: $${order.total || orderWithItems?.total || 0} ${order.currency || orderWithItems?.currency || 'USD'}</strong></p>
888
+ <p><strong>Status:</strong> ${order.status || orderWithItems?.status || 'pending'}</p>
889
+ <p><strong>Items:</strong></p>
890
+ <ul>
891
+ ${itemsHtml}
892
+ </ul>
893
+ <p>We will process your order shortly.</p>
894
+ <p>Best regards,<br>Your Ecommerce Team</p>
895
+ `,
896
+ };
897
+
898
+ await sendEmail(emailData);
899
+ strapi.log.info(`Order confirmation email sent successfully for order ${order.id || orderWithItems?.id}`);
900
+ } catch (error) {
901
+ strapi.log.error('Error sending order confirmation email:', error);
902
+ strapi.log.error('Email error details:', {
903
+ message: error.message,
904
+ stack: error.stack,
905
+ orderId: order?.id,
906
+ orderNumber: order?.order_number,
907
+ });
908
+ // Don't throw - email failures shouldn't break the order creation
909
+ }
910
+ },
911
+
912
+ // Send order status update email
913
+ async sendOrderStatusUpdateEmail(user, order, newStatus) {
914
+ try {
915
+ const settings = await strapi.store({ type: 'plugin', name: 'webbycommerce' }).get({ key: 'settings' });
916
+ const smtpSettings = settings?.smtp;
917
+
918
+ if (!smtpSettings) {
919
+ strapi.log.warn('SMTP settings not configured, skipping order status update email');
920
+ return;
921
+ }
922
+
923
+ const statusMessages = {
924
+ pending: 'Your order is being prepared',
925
+ processing: 'Your order is now being processed',
926
+ shipped: 'Your order has been shipped',
927
+ delivered: 'Your order has been delivered successfully',
928
+ cancelled: 'Your order has been cancelled',
929
+ refunded: 'Your order has been refunded'
930
+ };
931
+
932
+ const emailData = {
933
+ to: user.email,
934
+ subject: `Order Status Update - ${order.order_number || 'N/A'}`,
935
+ html: `
936
+ <h2>Order Status Update</h2>
937
+ <p>Dear ${user.username || 'Customer'},</p>
938
+ <p>Your order status has been updated:</p>
939
+ <h3>Order #${order.order_number || 'N/A'}</h3>
940
+ <p><strong>Status: ${newStatus ? newStatus.toUpperCase() : 'UPDATED'}</strong></p>
941
+ <p><strong>Message:</strong> ${statusMessages[newStatus] || 'Status updated'}</p>
942
+ ${order.tracking_number ? `<p><strong>Tracking Number:</strong> ${order.tracking_number}</p>` : ''}
943
+ ${order.estimated_delivery ? `<p><strong>Estimated Delivery:</strong> ${new Date(order.estimated_delivery).toLocaleDateString()}</p>` : ''}
944
+ <p>You can track your order at any time using our order tracking feature.</p>
945
+ <p>Best regards,<br>Your Ecommerce Team</p>
946
+ `,
947
+ };
948
+
949
+ await sendEmail(emailData);
950
+ strapi.log.info(`Order status update email sent successfully for order ${order.id}`);
951
+ } catch (error) {
952
+ strapi.log.error('Error sending order status update email:', error);
953
+ strapi.log.error('Email error details:', {
954
+ message: error.message,
955
+ stack: error.stack,
956
+ orderId: order?.id,
957
+ orderNumber: order?.order_number,
958
+ newStatus: newStatus,
959
+ });
960
+ // Don't throw - email failures shouldn't break the status update
961
+ }
962
+ },
963
+
964
+ // Restore stock when order is cancelled
965
+ async restoreOrderStock(order) {
966
+ try {
967
+ if (!order.items || !Array.isArray(order.items) || order.items.length === 0) {
968
+ strapi.log.warn(`[restoreOrderStock] No items found for order ${order.id}`);
969
+ return;
970
+ }
971
+
972
+ for (const item of order.items) {
973
+ // item is a product object from the manyToMany relation
974
+ const productId = typeof item === 'object' && item.id ? item.id : item;
975
+
976
+ if (!productId) {
977
+ strapi.log.warn(`[restoreOrderStock] Skipping item with invalid ID for order ${order.id}`);
978
+ continue;
979
+ }
980
+
981
+ const product = await strapi.db.query('plugin::webbycommerce.product').findOne({
982
+ where: { id: productId },
983
+ });
984
+
985
+ if (product) {
986
+ // Since we don't have exact quantities stored in order, restore 1 unit per product
987
+ // This is a limitation - ideally order items should store quantities
988
+ const quantityToRestore = 1;
989
+ const newStockQuantity = (product.stock_quantity || 0) + quantityToRestore;
990
+ const newStockStatus = newStockQuantity > 0 ? 'in_stock' : 'out_of_stock';
991
+
992
+ await strapi.db.query('plugin::webbycommerce.product').update({
993
+ where: { id: productId },
994
+ data: {
995
+ stock_quantity: newStockQuantity,
996
+ stock_status: newStockStatus,
997
+ },
998
+ });
999
+
1000
+ strapi.log.info(`[restoreOrderStock] Restored ${quantityToRestore} unit(s) of product ${productId} for order ${order.id}`);
1001
+ } else {
1002
+ strapi.log.warn(`[restoreOrderStock] Product ${productId} not found for order ${order.id}`);
1003
+ }
1004
+ }
1005
+ } catch (error) {
1006
+ strapi.log.error(`[restoreOrderStock] Failed to restore order stock for order ${order.id}:`, error);
1007
+ }
1008
+ },
1009
+
1010
+ // Generate tracking timeline based on order status
1011
+ generateTrackingTimeline(order) {
1012
+ const timeline = [];
1013
+ const createdAt = new Date(order.createdAt);
1014
+ const updatedAt = new Date(order.updatedAt);
1015
+
1016
+ // Order placed
1017
+ timeline.push({
1018
+ status: 'Order Placed',
1019
+ description: 'Your order has been successfully placed',
1020
+ timestamp: createdAt.toISOString(),
1021
+ completed: true,
1022
+ });
1023
+
1024
+ // Order confirmed/processing
1025
+ if (['processing', 'shipped', 'delivered'].includes(order.status)) {
1026
+ timeline.push({
1027
+ status: 'Order Confirmed',
1028
+ description: 'Your order has been confirmed and is being prepared',
1029
+ timestamp: createdAt.toISOString(),
1030
+ completed: true,
1031
+ });
1032
+ } else {
1033
+ timeline.push({
1034
+ status: 'Order Confirmed',
1035
+ description: 'Your order has been confirmed and is being prepared',
1036
+ completed: false,
1037
+ });
1038
+ }
1039
+
1040
+ // Order shipped
1041
+ if (['shipped', 'delivered'].includes(order.status)) {
1042
+ timeline.push({
1043
+ status: 'Order Shipped',
1044
+ description: `Your order has been shipped${order.tracking_number ? ` (Tracking: ${order.tracking_number})` : ''}`,
1045
+ timestamp: order.status === 'shipped' ? updatedAt.toISOString() : createdAt.toISOString(),
1046
+ completed: true,
1047
+ });
1048
+ } else if (order.status === 'processing') {
1049
+ timeline.push({
1050
+ status: 'Order Shipped',
1051
+ description: 'Your order will be shipped soon',
1052
+ completed: false,
1053
+ });
1054
+ }
1055
+
1056
+ // Order delivered
1057
+ if (order.status === 'delivered') {
1058
+ timeline.push({
1059
+ status: 'Order Delivered',
1060
+ description: 'Your order has been successfully delivered',
1061
+ timestamp: updatedAt.toISOString(),
1062
+ completed: true,
1063
+ });
1064
+ } else {
1065
+ timeline.push({
1066
+ status: 'Order Delivered',
1067
+ description: `Estimated delivery: ${order.estimated_delivery ? new Date(order.estimated_delivery).toLocaleDateString() : 'TBD'}`,
1068
+ completed: false,
1069
+ });
1070
+ }
1071
+
1072
+ return timeline;
1073
+ },
1074
+
1075
+ // Get current location based on status
1076
+ getCurrentLocation(status) {
1077
+ const locations = {
1078
+ pending: 'Order Processing Center',
1079
+ processing: 'Order Processing Center',
1080
+ shipped: 'In Transit',
1081
+ delivered: 'Delivered to customer',
1082
+ cancelled: 'Order cancelled',
1083
+ refunded: 'Refund processed'
1084
+ };
1085
+ return locations[status] || 'Unknown';
1086
+ },
1087
+
1088
+ // Get delivery status description
1089
+ getDeliveryStatus(status) {
1090
+ const statuses = {
1091
+ pending: 'Your order is being prepared',
1092
+ processing: 'Your order is being processed',
1093
+ shipped: 'Your order is on the way',
1094
+ delivered: 'Your order has been delivered successfully',
1095
+ cancelled: 'Your order has been cancelled',
1096
+ refunded: 'Your order has been refunded'
1097
+ };
1098
+ return statuses[status] || 'Status unknown';
1099
+ },
1100
+ };