@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.
- package/README.md +21 -3
- package/admin/app.js +3 -0
- package/admin/jsconfig.json +20 -0
- package/admin/src/components/ApiCollectionsContent.jsx +4626 -0
- package/admin/src/components/CompareContent.jsx +300 -0
- package/admin/src/components/ConfigureContent.jsx +407 -0
- package/admin/src/components/Initializer.jsx +64 -0
- package/admin/src/components/LoginRegisterContent.jsx +280 -0
- package/admin/src/components/PluginIcon.jsx +6 -0
- package/admin/src/components/ShippingTypeContent.jsx +230 -0
- package/admin/src/components/SmtpContent.jsx +316 -0
- package/admin/src/components/WishlistContent.jsx +273 -0
- package/admin/src/index.js +81 -0
- package/admin/src/pages/ApiCollections.jsx +169 -0
- package/admin/src/pages/Configure.jsx +55 -0
- package/admin/src/pages/Settings.jsx +93 -0
- package/admin/src/pluginId.js +4 -0
- package/{dist/_chunks/en-CiQ97iC8.js → admin/src/translations/en.json} +712 -574
- package/bin/setup.js +50 -3
- package/package.json +14 -13
- package/server/bootstrap.js +3 -0
- package/server/register.js +3 -0
- package/server/src/bootstrap.js +3826 -0
- package/server/src/components/content-block.json +37 -0
- package/server/src/components/shipping-zone-location.json +27 -0
- package/server/src/config/index.js +7 -0
- package/server/src/content-types/address/index.js +7 -0
- package/server/src/content-types/address/schema.json +74 -0
- package/server/src/content-types/cart/index.js +61 -0
- package/server/src/content-types/cart-item/index.js +79 -0
- package/server/src/content-types/compare.js +73 -0
- package/server/src/content-types/coupon/index.js +7 -0
- package/server/src/content-types/coupon/schema.json +67 -0
- package/server/src/content-types/index.js +42 -0
- package/server/src/content-types/order/index.js +7 -0
- package/server/src/content-types/order/schema.json +121 -0
- package/server/src/content-types/payment-transaction/index.js +7 -0
- package/server/src/content-types/payment-transaction/schema.json +73 -0
- package/server/src/content-types/product/index.js +7 -0
- package/server/src/content-types/product/schema.json +104 -0
- package/server/src/content-types/product-attribute/index.js +7 -0
- package/server/src/content-types/product-attribute/schema.json +80 -0
- package/server/src/content-types/product-attribute-value/index.js +7 -0
- package/server/src/content-types/product-attribute-value/schema.json +52 -0
- package/server/src/content-types/product-category/index.js +7 -0
- package/server/src/content-types/product-category/schema.json +54 -0
- package/server/src/content-types/product-tag/index.js +7 -0
- package/server/src/content-types/product-tag/schema.json +38 -0
- package/server/src/content-types/product-variation/index.js +7 -0
- package/server/src/content-types/product-variation/schema.json +74 -0
- package/server/src/content-types/shipping-method/index.js +7 -0
- package/server/src/content-types/shipping-method/schema.json +91 -0
- package/server/src/content-types/shipping-rate/index.js +7 -0
- package/server/src/content-types/shipping-rate/schema.json +73 -0
- package/server/src/content-types/shipping-rule/index.js +7 -0
- package/server/src/content-types/shipping-rule/schema.json +84 -0
- package/server/src/content-types/shipping-zone/index.js +7 -0
- package/server/src/content-types/shipping-zone/schema.json +57 -0
- package/server/src/content-types/wishlist.js +66 -0
- package/server/src/controllers/address.js +374 -0
- package/server/src/controllers/auth.js +1409 -0
- package/server/src/controllers/cart.js +337 -0
- package/server/src/controllers/category.js +388 -0
- package/server/src/controllers/compare.js +246 -0
- package/server/src/controllers/controller.js +168 -0
- package/server/src/controllers/ecommerce.js +20 -0
- package/server/src/controllers/index.js +34 -0
- package/server/src/controllers/order.js +1100 -0
- package/server/src/controllers/payment.js +243 -0
- package/server/src/controllers/product.js +1006 -0
- package/server/src/controllers/productTag.js +370 -0
- package/server/src/controllers/productVariation.js +181 -0
- package/server/src/controllers/shipping.js +1046 -0
- package/server/src/controllers/wishlist.js +332 -0
- package/server/src/destroy.js +6 -0
- package/server/src/index.js +26 -0
- package/server/src/middlewares/index.js +4 -0
- package/server/src/policies/index.js +4 -0
- package/server/src/register.js +67 -0
- package/server/src/routes/index.js +1130 -0
- package/server/src/services/cart.js +531 -0
- package/server/src/services/compare.js +300 -0
- package/server/src/services/index.js +16 -0
- package/server/src/services/service.js +19 -0
- package/server/src/services/shipping.js +513 -0
- package/server/src/services/wishlist.js +238 -0
- package/server/src/utils/check-ecommerce-permission.js +204 -0
- package/server/src/utils/extend-user-schema.js +161 -0
- package/server/src/utils/seed-data.js +639 -0
- package/server/src/utils/send-email.js +98 -0
- package/strapi-server.js +1 -6
- package/dist/_chunks/Settings-Bg2JyQ4c.js +0 -31518
- package/dist/_chunks/Settings-BonPzbwr.mjs +0 -31499
- package/dist/_chunks/en-DE15m4xZ.mjs +0 -574
- package/dist/_chunks/index-BWVy9o1d.mjs +0 -128
- package/dist/_chunks/index-NRuOdjd7.js +0 -127
- package/dist/admin/index.js +0 -3
- package/dist/admin/index.mjs +0 -4
- package/dist/robots.txt +0 -3
- package/dist/server/index.js +0 -27336
- package/dist/uploads/.gitkeep +0 -0
- package/dist/uploads/accessories_category_2a5631094b.jpeg +0 -0
- package/dist/uploads/beauty_personal_care_category_57f8a8f1e3.jpeg +0 -0
- package/dist/uploads/books_category_a9a253eada.jpeg +0 -0
- package/dist/uploads/classic_cotton_tshirt_1_cd713425f6.png +0 -0
- package/dist/uploads/clothing_category_d5c60ef07b.jpeg +0 -0
- package/dist/uploads/daviddoe_strapi_adbcd41787.jpeg +0 -0
- package/dist/uploads/electronics_category_fc3e5ef571.jpeg +0 -0
- package/dist/uploads/ergonomic_office_chair_1_c751cffb07.png +0 -0
- package/dist/uploads/home_garden_category_4f6eb3f8d6.jpeg +0 -0
- package/dist/uploads/istockphoto_1188462138_612x612_11f295b9c0.jpg +0 -0
- package/dist/uploads/istockphoto_1188462138_612x612_396fb272fd.jpg +0 -0
- package/dist/uploads/large_daviddoe_strapi_adbcd41787.jpeg +0 -0
- package/dist/uploads/leather_travel_backpack_1_238bc1ae4d.png +0 -0
- package/dist/uploads/mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
- package/dist/uploads/medium_classic_cotton_tshirt_1_cd713425f6.png +0 -0
- package/dist/uploads/medium_daviddoe_strapi_adbcd41787.jpeg +0 -0
- package/dist/uploads/medium_ergonomic_office_chair_1_c751cffb07.png +0 -0
- package/dist/uploads/medium_leather_travel_backpack_1_238bc1ae4d.png +0 -0
- package/dist/uploads/medium_mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
- package/dist/uploads/medium_smart_watch_series_5_1_cdc2511fb7.png +0 -0
- package/dist/uploads/medium_smartphone_x_pro_1_c3f0cbd080.png +0 -0
- package/dist/uploads/medium_the_great_gatsby_special_1_2e7c76d997.png +0 -0
- package/dist/uploads/medium_wireless_headphones_1_fa75cd50c3.png +0 -0
- package/dist/uploads/medium_yoga_mat_premium_1_01f9a3b5fa.png +0 -0
- package/dist/uploads/predictive_maintenance_icons_industry_automation_600nw_2685943461_e18a8aa3b0.webp +0 -0
- package/dist/uploads/small_classic_cotton_tshirt_1_cd713425f6.png +0 -0
- package/dist/uploads/small_daviddoe_strapi_adbcd41787.jpeg +0 -0
- package/dist/uploads/small_ergonomic_office_chair_1_c751cffb07.png +0 -0
- package/dist/uploads/small_leather_travel_backpack_1_238bc1ae4d.png +0 -0
- package/dist/uploads/small_mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
- package/dist/uploads/small_smart_watch_series_5_1_cdc2511fb7.png +0 -0
- package/dist/uploads/small_smartphone_x_pro_1_c3f0cbd080.png +0 -0
- package/dist/uploads/small_the_great_gatsby_special_1_2e7c76d997.png +0 -0
- package/dist/uploads/small_wireless_headphones_1_fa75cd50c3.png +0 -0
- package/dist/uploads/small_yoga_mat_premium_1_01f9a3b5fa.png +0 -0
- package/dist/uploads/smart_watch_series_5_1_cdc2511fb7.png +0 -0
- package/dist/uploads/smartphone_x_pro_1_c3f0cbd080.png +0 -0
- package/dist/uploads/the_great_gatsby_special_1_2e7c76d997.png +0 -0
- package/dist/uploads/thumbnail_accessories_category_2a5631094b.jpeg +0 -0
- package/dist/uploads/thumbnail_beauty_personal_care_category_57f8a8f1e3.jpeg +0 -0
- package/dist/uploads/thumbnail_books_category_a9a253eada.jpeg +0 -0
- package/dist/uploads/thumbnail_classic_cotton_tshirt_1_cd713425f6.png +0 -0
- package/dist/uploads/thumbnail_clothing_category_d5c60ef07b.jpeg +0 -0
- package/dist/uploads/thumbnail_daviddoe_strapi_adbcd41787.jpeg +0 -0
- package/dist/uploads/thumbnail_electronics_category_fc3e5ef571.jpeg +0 -0
- package/dist/uploads/thumbnail_ergonomic_office_chair_1_c751cffb07.png +0 -0
- package/dist/uploads/thumbnail_home_garden_category_4f6eb3f8d6.jpeg +0 -0
- package/dist/uploads/thumbnail_istockphoto_1188462138_612x612_11f295b9c0.jpg +0 -0
- package/dist/uploads/thumbnail_istockphoto_1188462138_612x612_396fb272fd.jpg +0 -0
- package/dist/uploads/thumbnail_leather_travel_backpack_1_238bc1ae4d.png +0 -0
- package/dist/uploads/thumbnail_mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
- package/dist/uploads/thumbnail_predictive_maintenance_icons_industry_automation_600nw_2685943461_e18a8aa3b0.webp +0 -0
- package/dist/uploads/thumbnail_smart_watch_series_5_1_cdc2511fb7.png +0 -0
- package/dist/uploads/thumbnail_smartphone_x_pro_1_c3f0cbd080.png +0 -0
- package/dist/uploads/thumbnail_the_great_gatsby_special_1_2e7c76d997.png +0 -0
- package/dist/uploads/thumbnail_wireless_headphones_1_fa75cd50c3.png +0 -0
- package/dist/uploads/thumbnail_yoga_mat_premium_1_01f9a3b5fa.png +0 -0
- package/dist/uploads/webby-commerce.png +0 -0
- package/dist/uploads/wireless_headphones_1_fa75cd50c3.png +0 -0
- package/dist/uploads/yoga_mat_premium_1_01f9a3b5fa.png +0 -0
- /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
|
+
};
|