@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,1006 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const PLUGIN_ID = 'webbycommerce';
|
|
4
|
+
const { ensureEcommercePermission } = require('../utils/check-ecommerce-permission');
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
/**
|
|
8
|
+
* Get all products
|
|
9
|
+
*/
|
|
10
|
+
async getProducts(ctx) {
|
|
11
|
+
try {
|
|
12
|
+
// Check ecommerce permission
|
|
13
|
+
if (!(await ensureEcommercePermission(ctx))) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { product_category, tag, search, limit, start = 0, getAll } = ctx.query;
|
|
18
|
+
|
|
19
|
+
// Strapi v5 stores drafts + published versions as separate rows (document model).
|
|
20
|
+
// For storefront APIs we only want published records, otherwise you can see duplicates.
|
|
21
|
+
const where = { publishedAt: { $notNull: true } };
|
|
22
|
+
|
|
23
|
+
if (product_category) {
|
|
24
|
+
where.product_categories = { id: product_category };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (tag) {
|
|
28
|
+
where.tags = { id: tag };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (search) {
|
|
32
|
+
where.name = { $containsi: search };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const total = await strapi.db.query('plugin::webbycommerce.product').count({ where });
|
|
36
|
+
|
|
37
|
+
// If getAll=true or limit is not provided, return all products without pagination
|
|
38
|
+
const shouldGetAll = getAll === 'true' || getAll === true || limit === undefined || limit === null;
|
|
39
|
+
|
|
40
|
+
const queryOptions = {
|
|
41
|
+
where,
|
|
42
|
+
orderBy: { createdAt: 'desc' },
|
|
43
|
+
populate: {
|
|
44
|
+
product_categories: true,
|
|
45
|
+
tags: true,
|
|
46
|
+
images: true,
|
|
47
|
+
variations: {
|
|
48
|
+
populate: ['attributes', 'attributeValues'],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Only add pagination if getAll is false and limit is provided
|
|
54
|
+
if (!shouldGetAll && limit) {
|
|
55
|
+
queryOptions.limit = parseInt(limit, 10);
|
|
56
|
+
queryOptions.start = parseInt(start, 10);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const products = await strapi.db.query('plugin::webbycommerce.product').findMany(queryOptions);
|
|
60
|
+
|
|
61
|
+
const responseMeta = shouldGetAll
|
|
62
|
+
? { total, returned: products.length, pagination: false }
|
|
63
|
+
: {
|
|
64
|
+
total,
|
|
65
|
+
limit: parseInt(limit, 10),
|
|
66
|
+
start: parseInt(start, 10),
|
|
67
|
+
pagination: true,
|
|
68
|
+
page: Math.floor(parseInt(start, 10) / parseInt(limit, 10)) + 1,
|
|
69
|
+
pageCount: Math.ceil(total / parseInt(limit, 10))
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
ctx.send({ data: products, meta: responseMeta });
|
|
73
|
+
} catch (error) {
|
|
74
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in getProducts:`, error);
|
|
75
|
+
ctx.internalServerError('Failed to fetch products. Please try again.');
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get single product
|
|
81
|
+
*/
|
|
82
|
+
async getProduct(ctx) {
|
|
83
|
+
try {
|
|
84
|
+
// Check ecommerce permission
|
|
85
|
+
if (!(await ensureEcommercePermission(ctx))) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { id } = ctx.params;
|
|
90
|
+
|
|
91
|
+
const product = await strapi.db.query('plugin::webbycommerce.product').findOne({
|
|
92
|
+
where: { id, publishedAt: { $notNull: true } },
|
|
93
|
+
populate: ['product_categories', 'tags', 'images', 'variations'],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (!product) {
|
|
97
|
+
return ctx.notFound('Product not found.');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
ctx.send({ data: product });
|
|
101
|
+
} catch (error) {
|
|
102
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in getProduct:`, error);
|
|
103
|
+
ctx.internalServerError('Failed to fetch product. Please try again.');
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get single product by slug
|
|
109
|
+
*/
|
|
110
|
+
async getProductBySlug(ctx) {
|
|
111
|
+
try {
|
|
112
|
+
// Check ecommerce permission
|
|
113
|
+
if (!(await ensureEcommercePermission(ctx))) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const rawSlug = ctx.params?.slug;
|
|
118
|
+
const slug = typeof rawSlug === 'string' ? decodeURIComponent(rawSlug).trim() : '';
|
|
119
|
+
|
|
120
|
+
if (!slug) {
|
|
121
|
+
return ctx.badRequest('Slug is required.');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Allow numeric slugs to still work like "get by id"
|
|
125
|
+
if (/^[0-9]+$/.test(slug)) {
|
|
126
|
+
const product = await strapi.db.query('plugin::webbycommerce.product').findOne({
|
|
127
|
+
where: { id: slug, publishedAt: { $notNull: true } },
|
|
128
|
+
populate: ['product_categories', 'tags', 'images', 'variations'],
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (!product) {
|
|
132
|
+
return ctx.notFound('Product not found.');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
ctx.send({ data: product });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Strapi v5 can store draft + published versions as separate rows (document model).
|
|
140
|
+
// For storefront APIs we only want published records.
|
|
141
|
+
const results = await strapi.db.query('plugin::webbycommerce.product').findMany({
|
|
142
|
+
where: { slug, publishedAt: { $notNull: true } },
|
|
143
|
+
limit: 1,
|
|
144
|
+
orderBy: { publishedAt: 'desc', id: 'desc' },
|
|
145
|
+
populate: ['product_categories', 'tags', 'images', 'variations'],
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const product = results?.[0];
|
|
149
|
+
|
|
150
|
+
if (!product) {
|
|
151
|
+
return ctx.notFound('Product not found.');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
ctx.send({ data: product });
|
|
155
|
+
} catch (error) {
|
|
156
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in getProductBySlug:`, error);
|
|
157
|
+
ctx.internalServerError('Failed to fetch product. Please try again.');
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Create product
|
|
163
|
+
*/
|
|
164
|
+
async createProduct(ctx) {
|
|
165
|
+
try {
|
|
166
|
+
const { name, description, price, sale_price, sku, slug, stock_quantity, stock_status, weight, dimensions, product_categories, tags, images } = ctx.request.body || {};
|
|
167
|
+
|
|
168
|
+
if (!name || price === undefined || price === null) {
|
|
169
|
+
return ctx.badRequest('Name and price are required.');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Validate and parse numeric fields
|
|
173
|
+
const parsedPrice = parseFloat(price);
|
|
174
|
+
if (isNaN(parsedPrice) || parsedPrice < 0) {
|
|
175
|
+
return ctx.badRequest('Price must be a valid positive number.');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let parsedSalePrice = null;
|
|
179
|
+
if (sale_price !== undefined && sale_price !== null) {
|
|
180
|
+
parsedSalePrice = parseFloat(sale_price);
|
|
181
|
+
if (isNaN(parsedSalePrice) || parsedSalePrice < 0) {
|
|
182
|
+
return ctx.badRequest('Sale price must be a valid positive number.');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let parsedStockQuantity = 0;
|
|
187
|
+
if (stock_quantity !== undefined && stock_quantity !== null) {
|
|
188
|
+
parsedStockQuantity = parseInt(stock_quantity, 10);
|
|
189
|
+
if (isNaN(parsedStockQuantity) || parsedStockQuantity < 0) {
|
|
190
|
+
return ctx.badRequest('Stock quantity must be a valid non-negative integer.');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let parsedWeight = null;
|
|
195
|
+
if (weight !== undefined && weight !== null) {
|
|
196
|
+
parsedWeight = parseFloat(weight);
|
|
197
|
+
if (isNaN(parsedWeight) || parsedWeight < 0) {
|
|
198
|
+
return ctx.badRequest('Weight must be a valid positive number.');
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const data = {
|
|
203
|
+
name,
|
|
204
|
+
description,
|
|
205
|
+
price: parsedPrice,
|
|
206
|
+
sale_price: parsedSalePrice,
|
|
207
|
+
sku,
|
|
208
|
+
slug,
|
|
209
|
+
stock_quantity: parsedStockQuantity,
|
|
210
|
+
stock_status: stock_status || 'in_stock',
|
|
211
|
+
weight: parsedWeight,
|
|
212
|
+
dimensions,
|
|
213
|
+
publishedAt: new Date(),
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// Prepare relation IDs
|
|
217
|
+
let categoryIds = [];
|
|
218
|
+
if (product_categories && Array.isArray(product_categories) && product_categories.length > 0) {
|
|
219
|
+
categoryIds = product_categories.map(id =>
|
|
220
|
+
typeof id === 'object' && id.id ? id.id : id
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let tagIds = [];
|
|
225
|
+
if (tags && Array.isArray(tags) && tags.length > 0) {
|
|
226
|
+
tagIds = tags.map(id =>
|
|
227
|
+
typeof id === 'object' && id.id ? id.id : id
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let imageIds = [];
|
|
232
|
+
if (images && Array.isArray(images) && images.length > 0) {
|
|
233
|
+
imageIds = images.map(id =>
|
|
234
|
+
typeof id === 'object' && id.id ? id.id : id
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Use entityService for Strapi v5 document model compatibility
|
|
239
|
+
// This ensures products appear in the content manager
|
|
240
|
+
const product = await strapi.entityService.create('plugin::webbycommerce.product', {
|
|
241
|
+
data,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Update product with relations to ensure they're properly linked in document model
|
|
245
|
+
const updateData = {};
|
|
246
|
+
if (categoryIds.length > 0) {
|
|
247
|
+
updateData.product_categories = categoryIds;
|
|
248
|
+
}
|
|
249
|
+
if (tagIds.length > 0) {
|
|
250
|
+
updateData.tags = tagIds;
|
|
251
|
+
}
|
|
252
|
+
if (imageIds.length > 0) {
|
|
253
|
+
updateData.images = imageIds;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let updatedProduct = product;
|
|
257
|
+
if (Object.keys(updateData).length > 0) {
|
|
258
|
+
updatedProduct = await strapi.entityService.update('plugin::webbycommerce.product', product.id, {
|
|
259
|
+
data: updateData,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Re-fetch the product with relations populated using entityService
|
|
264
|
+
const populated = await strapi.entityService.findOne('plugin::webbycommerce.product', updatedProduct.id, {
|
|
265
|
+
populate: ['product_categories', 'tags', 'images', 'variations'],
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
ctx.send({ data: populated || updatedProduct });
|
|
269
|
+
} catch (error) {
|
|
270
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in createProduct:`, error);
|
|
271
|
+
ctx.internalServerError('Failed to create product. Please try again.');
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Create products in bulk
|
|
277
|
+
*/
|
|
278
|
+
async createBulkProducts(ctx) {
|
|
279
|
+
try {
|
|
280
|
+
// Check ecommerce permission
|
|
281
|
+
if (!(await ensureEcommercePermission(ctx))) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const { products } = ctx.request.body || {};
|
|
286
|
+
|
|
287
|
+
if (!Array.isArray(products)) {
|
|
288
|
+
return ctx.badRequest('Products must be an array.');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (products.length === 0) {
|
|
292
|
+
return ctx.badRequest('Products array cannot be empty.');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Limit bulk operations to prevent performance issues
|
|
296
|
+
const MAX_BULK_SIZE = 100;
|
|
297
|
+
if (products.length > MAX_BULK_SIZE) {
|
|
298
|
+
return ctx.badRequest(`Cannot create more than ${MAX_BULK_SIZE} products at once.`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Helper function to build connect payloads
|
|
302
|
+
const buildConnect = (arr) => {
|
|
303
|
+
if (!arr) return undefined;
|
|
304
|
+
if (Array.isArray(arr) && arr.length > 0) {
|
|
305
|
+
if (typeof arr[0] === 'object') return arr;
|
|
306
|
+
return arr.map((id) => ({ id }));
|
|
307
|
+
}
|
|
308
|
+
return undefined;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// Helper function to validate and prepare a single product
|
|
312
|
+
const validateAndPrepareProduct = (productData, index) => {
|
|
313
|
+
const errors = [];
|
|
314
|
+
|
|
315
|
+
if (!productData.name || productData.price === undefined || productData.price === null) {
|
|
316
|
+
errors.push('Name and price are required.');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const parsedPrice = parseFloat(productData.price);
|
|
320
|
+
if (isNaN(parsedPrice) || parsedPrice < 0) {
|
|
321
|
+
errors.push('Price must be a valid positive number.');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
let parsedSalePrice = null;
|
|
325
|
+
if (productData.sale_price !== undefined && productData.sale_price !== null) {
|
|
326
|
+
parsedSalePrice = parseFloat(productData.sale_price);
|
|
327
|
+
if (isNaN(parsedSalePrice) || parsedSalePrice < 0) {
|
|
328
|
+
errors.push('Sale price must be a valid positive number.');
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
let parsedStockQuantity = 0;
|
|
333
|
+
if (productData.stock_quantity !== undefined && productData.stock_quantity !== null) {
|
|
334
|
+
parsedStockQuantity = parseInt(productData.stock_quantity, 10);
|
|
335
|
+
if (isNaN(parsedStockQuantity) || parsedStockQuantity < 0) {
|
|
336
|
+
errors.push('Stock quantity must be a valid non-negative integer.');
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
let parsedWeight = null;
|
|
341
|
+
if (productData.weight !== undefined && productData.weight !== null) {
|
|
342
|
+
parsedWeight = parseFloat(productData.weight);
|
|
343
|
+
if (isNaN(parsedWeight) || parsedWeight < 0) {
|
|
344
|
+
errors.push('Weight must be a valid positive number.');
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (errors.length > 0) {
|
|
349
|
+
return { valid: false, errors, index };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const data = {
|
|
353
|
+
name: productData.name,
|
|
354
|
+
description: productData.description,
|
|
355
|
+
price: parsedPrice,
|
|
356
|
+
sale_price: parsedSalePrice,
|
|
357
|
+
sku: productData.sku,
|
|
358
|
+
slug: productData.slug,
|
|
359
|
+
stock_quantity: parsedStockQuantity,
|
|
360
|
+
stock_status: productData.stock_status || 'in_stock',
|
|
361
|
+
weight: parsedWeight,
|
|
362
|
+
dimensions: productData.dimensions,
|
|
363
|
+
publishedAt: new Date(),
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// Prepare relation IDs separately
|
|
367
|
+
const relations = {
|
|
368
|
+
product_categories: [],
|
|
369
|
+
tags: [],
|
|
370
|
+
images: [],
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
if (productData.product_categories && Array.isArray(productData.product_categories) && productData.product_categories.length > 0) {
|
|
374
|
+
relations.product_categories = productData.product_categories.map(id =>
|
|
375
|
+
typeof id === 'object' && id.id ? id.id : id
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (productData.tags && Array.isArray(productData.tags) && productData.tags.length > 0) {
|
|
380
|
+
relations.tags = productData.tags.map(id =>
|
|
381
|
+
typeof id === 'object' && id.id ? id.id : id
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (productData.images && Array.isArray(productData.images) && productData.images.length > 0) {
|
|
386
|
+
relations.images = productData.images.map(id =>
|
|
387
|
+
typeof id === 'object' && id.id ? id.id : id
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return { valid: true, data, relations, index };
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const results = {
|
|
395
|
+
success: [],
|
|
396
|
+
failed: [],
|
|
397
|
+
summary: {
|
|
398
|
+
total: products.length,
|
|
399
|
+
successful: 0,
|
|
400
|
+
failed: 0,
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// Process each product
|
|
405
|
+
for (let i = 0; i < products.length; i++) {
|
|
406
|
+
const productData = products[i];
|
|
407
|
+
const validation = validateAndPrepareProduct(productData, i);
|
|
408
|
+
|
|
409
|
+
if (!validation.valid) {
|
|
410
|
+
results.failed.push({
|
|
411
|
+
index: i,
|
|
412
|
+
product: productData,
|
|
413
|
+
errors: validation.errors,
|
|
414
|
+
});
|
|
415
|
+
results.summary.failed++;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
// Use entityService for Strapi v5 document model compatibility
|
|
421
|
+
// This ensures products appear in the content manager
|
|
422
|
+
const product = await strapi.entityService.create('plugin::webbycommerce.product', {
|
|
423
|
+
data: validation.data,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Update product with relations to ensure they're properly linked in document model
|
|
427
|
+
const updateData = {};
|
|
428
|
+
if (validation.relations.product_categories.length > 0) {
|
|
429
|
+
updateData.product_categories = validation.relations.product_categories;
|
|
430
|
+
}
|
|
431
|
+
if (validation.relations.tags.length > 0) {
|
|
432
|
+
updateData.tags = validation.relations.tags;
|
|
433
|
+
}
|
|
434
|
+
if (validation.relations.images.length > 0) {
|
|
435
|
+
updateData.images = validation.relations.images;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
let updatedProduct = product;
|
|
439
|
+
if (Object.keys(updateData).length > 0) {
|
|
440
|
+
updatedProduct = await strapi.entityService.update('plugin::webbycommerce.product', product.id, {
|
|
441
|
+
data: updateData,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Re-fetch with populated relations using entityService
|
|
446
|
+
const populated = await strapi.entityService.findOne('plugin::webbycommerce.product', updatedProduct.id, {
|
|
447
|
+
populate: ['product_categories', 'tags', 'images', 'variations'],
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
results.success.push({
|
|
451
|
+
index: i,
|
|
452
|
+
product: populated || updatedProduct,
|
|
453
|
+
});
|
|
454
|
+
results.summary.successful++;
|
|
455
|
+
} catch (error) {
|
|
456
|
+
strapi.log.error(`[${PLUGIN_ID}] Error creating product at index ${i}:`, error);
|
|
457
|
+
results.failed.push({
|
|
458
|
+
index: i,
|
|
459
|
+
product: productData,
|
|
460
|
+
errors: [error.message || 'Failed to create product.'],
|
|
461
|
+
});
|
|
462
|
+
results.summary.failed++;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Return 207 Multi-Status if there are mixed results, 200 if all succeeded, 400 if all failed
|
|
467
|
+
const statusCode = results.summary.failed === 0 ? 200 : results.summary.successful === 0 ? 400 : 207;
|
|
468
|
+
ctx.status = statusCode;
|
|
469
|
+
ctx.send({ data: results });
|
|
470
|
+
} catch (error) {
|
|
471
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in createBulkProducts:`, error);
|
|
472
|
+
ctx.internalServerError('Failed to create products in bulk. Please try again.');
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Update product
|
|
478
|
+
*/
|
|
479
|
+
async updateProduct(ctx) {
|
|
480
|
+
try {
|
|
481
|
+
// Check ecommerce permission
|
|
482
|
+
if (!(await ensureEcommercePermission(ctx))) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const { id } = ctx.params;
|
|
487
|
+
|
|
488
|
+
const body = ctx.request.body || {};
|
|
489
|
+
|
|
490
|
+
const updateData = { ...body };
|
|
491
|
+
|
|
492
|
+
// Convert relation id arrays to connect format for update
|
|
493
|
+
const buildConnectForUpdate = (key, target) => {
|
|
494
|
+
const val = body[key];
|
|
495
|
+
if (val === undefined) return;
|
|
496
|
+
if (Array.isArray(val) && val.length > 0) {
|
|
497
|
+
if (typeof val[0] === 'object') {
|
|
498
|
+
updateData[key] = { connect: val };
|
|
499
|
+
} else {
|
|
500
|
+
updateData[key] = { connect: val.map((id) => ({ id })) };
|
|
501
|
+
}
|
|
502
|
+
} else if (Array.isArray(val) && val.length === 0) {
|
|
503
|
+
// empty array -> disconnect all
|
|
504
|
+
updateData[key] = { disconnect: [] };
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
buildConnectForUpdate('product_categories');
|
|
509
|
+
buildConnectForUpdate('tags');
|
|
510
|
+
buildConnectForUpdate('images');
|
|
511
|
+
|
|
512
|
+
const product = await strapi.db.query('plugin::webbycommerce.product').update({
|
|
513
|
+
where: { id },
|
|
514
|
+
data: updateData,
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
if (!product) {
|
|
518
|
+
return ctx.notFound('Product not found.');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Re-fetch with populated relations
|
|
522
|
+
const populatedUpdated = await strapi.db.query('plugin::webbycommerce.product').findOne({
|
|
523
|
+
where: { id: product.id },
|
|
524
|
+
populate: ['product_categories', 'tags', 'images', 'variations'],
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
ctx.send({ data: populatedUpdated || product });
|
|
528
|
+
} catch (error) {
|
|
529
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in updateProduct:`, error);
|
|
530
|
+
ctx.internalServerError('Failed to update product. Please try again.');
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Delete product
|
|
536
|
+
*/
|
|
537
|
+
async deleteProduct(ctx) {
|
|
538
|
+
try {
|
|
539
|
+
// Check ecommerce permission
|
|
540
|
+
if (!(await ensureEcommercePermission(ctx))) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const { id } = ctx.params;
|
|
545
|
+
|
|
546
|
+
const product = await strapi.db.query('plugin::webbycommerce.product').delete({
|
|
547
|
+
where: { id },
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
if (!product) {
|
|
551
|
+
return ctx.notFound('Product not found.');
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
ctx.send({ data: product });
|
|
555
|
+
} catch (error) {
|
|
556
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in deleteProduct:`, error);
|
|
557
|
+
ctx.internalServerError('Failed to delete product. Please try again.');
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Get related products
|
|
563
|
+
*/
|
|
564
|
+
async getRelatedProducts(ctx) {
|
|
565
|
+
try {
|
|
566
|
+
// Check ecommerce permission
|
|
567
|
+
if (!(await ensureEcommercePermission(ctx))) {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const { id } = ctx.params;
|
|
572
|
+
const { limit = 4 } = ctx.query;
|
|
573
|
+
|
|
574
|
+
// Get the current product to find related products
|
|
575
|
+
const product = await strapi.db.query('plugin::webbycommerce.product').findOne({
|
|
576
|
+
where: { id },
|
|
577
|
+
populate: ['product_categories', 'tags'],
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
if (!product) {
|
|
581
|
+
return ctx.notFound('Product not found.');
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const where = {
|
|
585
|
+
id: { $ne: id }, // Exclude current product
|
|
586
|
+
publishedAt: { $notNull: true } // Only published products
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
// Find products in same categories or with same tags
|
|
590
|
+
const categoryIds = product.product_categories?.map(cat => cat.id) || [];
|
|
591
|
+
const tagIds = product.tags?.map(tag => tag.id) || [];
|
|
592
|
+
|
|
593
|
+
if (categoryIds.length > 0 || tagIds.length > 0) {
|
|
594
|
+
where.$or = [];
|
|
595
|
+
if (categoryIds.length > 0) {
|
|
596
|
+
where.$or.push({ product_categories: { id: { $in: categoryIds } } });
|
|
597
|
+
}
|
|
598
|
+
if (tagIds.length > 0) {
|
|
599
|
+
where.$or.push({ tags: { id: { $in: tagIds } } });
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const relatedProducts = await strapi.db.query('plugin::webbycommerce.product').findMany({
|
|
604
|
+
where,
|
|
605
|
+
limit: parseInt(limit, 10),
|
|
606
|
+
orderBy: { createdAt: 'desc' },
|
|
607
|
+
populate: ['product_categories', 'tags', 'images', 'variations'],
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
ctx.send({ data: relatedProducts });
|
|
611
|
+
} catch (error) {
|
|
612
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in getRelatedProducts:`, error);
|
|
613
|
+
ctx.internalServerError('Failed to fetch related products. Please try again.');
|
|
614
|
+
}
|
|
615
|
+
},
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Get product categories
|
|
619
|
+
*/
|
|
620
|
+
async getCategories(ctx) {
|
|
621
|
+
try {
|
|
622
|
+
// Check ecommerce permission
|
|
623
|
+
if (!(await ensureEcommercePermission(ctx))) {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const { limit = 20, start = 0, parent } = ctx.query;
|
|
628
|
+
|
|
629
|
+
const where = { is_active: true };
|
|
630
|
+
|
|
631
|
+
if (parent !== undefined) {
|
|
632
|
+
if (parent === 'null' || parent === '') {
|
|
633
|
+
where.parent = null; // Root categories
|
|
634
|
+
} else {
|
|
635
|
+
where.parent = { id: parent };
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const categories = await strapi.db.query('plugin::webbycommerce.product-category').findMany({
|
|
640
|
+
where,
|
|
641
|
+
limit: parseInt(limit, 10),
|
|
642
|
+
start: parseInt(start, 10),
|
|
643
|
+
orderBy: { sort_order: 'asc' },
|
|
644
|
+
populate: ['parent', 'children'],
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
const total = await strapi.db.query('plugin::webbycommerce.product-category').count({ where });
|
|
648
|
+
|
|
649
|
+
ctx.send({
|
|
650
|
+
data: categories,
|
|
651
|
+
meta: {
|
|
652
|
+
total,
|
|
653
|
+
limit: parseInt(limit, 10),
|
|
654
|
+
start: parseInt(start, 10)
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
} catch (error) {
|
|
658
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in getCategories:`, error);
|
|
659
|
+
ctx.internalServerError('Failed to fetch categories. Please try again.');
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Get product tags
|
|
665
|
+
*/
|
|
666
|
+
async getTags(ctx) {
|
|
667
|
+
try {
|
|
668
|
+
// Check ecommerce permission
|
|
669
|
+
if (!(await ensureEcommercePermission(ctx))) {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const { limit = 20, start = 0, search } = ctx.query;
|
|
674
|
+
|
|
675
|
+
// Strapi v5 stores drafts + published versions as separate rows (document model).
|
|
676
|
+
// For storefront APIs we only want published records, otherwise you can see duplicates.
|
|
677
|
+
const where = { publishedAt: { $notNull: true } };
|
|
678
|
+
|
|
679
|
+
if (search) {
|
|
680
|
+
where.name = { $containsi: search };
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const tags = await strapi.db.query('plugin::webbycommerce.product-tag').findMany({
|
|
684
|
+
where,
|
|
685
|
+
limit: parseInt(limit, 10),
|
|
686
|
+
start: parseInt(start, 10),
|
|
687
|
+
orderBy: { name: 'asc' },
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
const total = await strapi.db.query('plugin::webbycommerce.product-tag').count({ where });
|
|
691
|
+
|
|
692
|
+
ctx.send({
|
|
693
|
+
data: tags,
|
|
694
|
+
meta: {
|
|
695
|
+
total,
|
|
696
|
+
limit: parseInt(limit, 10),
|
|
697
|
+
start: parseInt(start, 10)
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
} catch (error) {
|
|
701
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in getTags:`, error);
|
|
702
|
+
ctx.internalServerError('Failed to fetch tags. Please try again.');
|
|
703
|
+
}
|
|
704
|
+
},
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Get product attributes
|
|
708
|
+
*/
|
|
709
|
+
async getAttributes(ctx) {
|
|
710
|
+
try {
|
|
711
|
+
// Check ecommerce permission
|
|
712
|
+
if (!(await ensureEcommercePermission(ctx))) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const { limit = 20, start = 0, is_variation } = ctx.query;
|
|
717
|
+
|
|
718
|
+
const where = {};
|
|
719
|
+
|
|
720
|
+
if (is_variation !== undefined) {
|
|
721
|
+
where.is_variation = is_variation === 'true';
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const attributes = await strapi.db.query('plugin::webbycommerce.product-attribute').findMany({
|
|
725
|
+
where,
|
|
726
|
+
limit: parseInt(limit, 10),
|
|
727
|
+
start: parseInt(start, 10),
|
|
728
|
+
orderBy: { sort_order: 'asc' },
|
|
729
|
+
populate: ['product_attribute_values'],
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
const total = await strapi.db.query('plugin::webbycommerce.product-attribute').count({ where });
|
|
733
|
+
|
|
734
|
+
ctx.send({
|
|
735
|
+
data: attributes,
|
|
736
|
+
meta: {
|
|
737
|
+
total,
|
|
738
|
+
limit: parseInt(limit, 10),
|
|
739
|
+
start: parseInt(start, 10)
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
} catch (error) {
|
|
743
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in getAttributes:`, error);
|
|
744
|
+
ctx.internalServerError('Failed to fetch attributes. Please try again.');
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
|
|
748
|
+
// Standard Strapi controller methods for content manager
|
|
749
|
+
async find(ctx) {
|
|
750
|
+
try {
|
|
751
|
+
const { product_category, tag, search, limit = 25, start = 0, sort = 'createdAt:desc' } = ctx.query;
|
|
752
|
+
|
|
753
|
+
const where = { publishedAt: { $notNull: true } }; // Only published products
|
|
754
|
+
|
|
755
|
+
if (product_category) {
|
|
756
|
+
where.product_categories = { id: product_category };
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (tag) {
|
|
760
|
+
where.tags = { id: tag };
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (search) {
|
|
764
|
+
where.name = { $containsi: search };
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const products = await strapi.db.query('plugin::webbycommerce.product').findMany({
|
|
768
|
+
where,
|
|
769
|
+
limit: parseInt(limit, 10),
|
|
770
|
+
start: parseInt(start, 10),
|
|
771
|
+
orderBy: sort.split(':').reduce((acc, val, i) => {
|
|
772
|
+
if (i === 0) acc[val] = 'asc';
|
|
773
|
+
else acc[val] = 'desc';
|
|
774
|
+
return acc;
|
|
775
|
+
}, {}),
|
|
776
|
+
populate: ['product_categories', 'tags', 'images', 'variations'],
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
const total = await strapi.db.query('plugin::webbycommerce.product').count({ where });
|
|
780
|
+
|
|
781
|
+
ctx.send({
|
|
782
|
+
data: products,
|
|
783
|
+
meta: {
|
|
784
|
+
pagination: {
|
|
785
|
+
page: Math.floor(parseInt(start, 10) / parseInt(limit, 10)) + 1,
|
|
786
|
+
pageSize: parseInt(limit, 10),
|
|
787
|
+
pageCount: Math.ceil(total / parseInt(limit, 10)),
|
|
788
|
+
total,
|
|
789
|
+
},
|
|
790
|
+
},
|
|
791
|
+
});
|
|
792
|
+
} catch (error) {
|
|
793
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in find:`, error);
|
|
794
|
+
ctx.internalServerError('Failed to fetch products.');
|
|
795
|
+
}
|
|
796
|
+
},
|
|
797
|
+
|
|
798
|
+
async findOne(ctx) {
|
|
799
|
+
try {
|
|
800
|
+
const { id } = ctx.params;
|
|
801
|
+
|
|
802
|
+
const product = await strapi.db.query('plugin::webbycommerce.product').findOne({
|
|
803
|
+
where: { id, publishedAt: { $notNull: true } },
|
|
804
|
+
populate: ['product_categories', 'tags', 'images', 'variations'],
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
if (!product) {
|
|
808
|
+
return ctx.notFound('Product not found.');
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
ctx.send({ data: product });
|
|
812
|
+
} catch (error) {
|
|
813
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in findOne:`, error);
|
|
814
|
+
ctx.internalServerError('Failed to fetch product.');
|
|
815
|
+
}
|
|
816
|
+
},
|
|
817
|
+
|
|
818
|
+
async create(ctx) {
|
|
819
|
+
try {
|
|
820
|
+
const data = ctx.request.body.data || ctx.request.body;
|
|
821
|
+
|
|
822
|
+
// Validate required fields
|
|
823
|
+
if (!data.name || data.price === undefined || data.price === null) {
|
|
824
|
+
return ctx.badRequest('Name and price are required.');
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Validate and parse numeric fields
|
|
828
|
+
const parsedPrice = parseFloat(data.price);
|
|
829
|
+
if (isNaN(parsedPrice) || parsedPrice < 0) {
|
|
830
|
+
return ctx.badRequest('Price must be a valid positive number.');
|
|
831
|
+
}
|
|
832
|
+
data.price = parsedPrice;
|
|
833
|
+
|
|
834
|
+
if (data.sale_price !== undefined && data.sale_price !== null) {
|
|
835
|
+
const parsedSalePrice = parseFloat(data.sale_price);
|
|
836
|
+
if (isNaN(parsedSalePrice) || parsedSalePrice < 0) {
|
|
837
|
+
return ctx.badRequest('Sale price must be a valid positive number.');
|
|
838
|
+
}
|
|
839
|
+
data.sale_price = parsedSalePrice;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (data.stock_quantity !== undefined && data.stock_quantity !== null) {
|
|
843
|
+
const parsedStockQuantity = parseInt(data.stock_quantity, 10);
|
|
844
|
+
if (isNaN(parsedStockQuantity) || parsedStockQuantity < 0) {
|
|
845
|
+
return ctx.badRequest('Stock quantity must be a valid non-negative integer.');
|
|
846
|
+
}
|
|
847
|
+
data.stock_quantity = parsedStockQuantity;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if (data.weight !== undefined && data.weight !== null) {
|
|
851
|
+
const parsedWeight = parseFloat(data.weight);
|
|
852
|
+
if (isNaN(parsedWeight) || parsedWeight < 0) {
|
|
853
|
+
return ctx.badRequest('Weight must be a valid positive number.');
|
|
854
|
+
}
|
|
855
|
+
data.weight = parsedWeight;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Set publishedAt if not provided
|
|
859
|
+
if (!data.publishedAt) {
|
|
860
|
+
data.publishedAt = new Date();
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Extract relations before creating
|
|
864
|
+
const relations = {
|
|
865
|
+
product_categories: data.product_categories || [],
|
|
866
|
+
tags: data.tags || [],
|
|
867
|
+
images: data.images || [],
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
// Remove relations from data object for initial creation
|
|
871
|
+
delete data.product_categories;
|
|
872
|
+
delete data.tags;
|
|
873
|
+
delete data.images;
|
|
874
|
+
|
|
875
|
+
// Normalize relation IDs
|
|
876
|
+
if (Array.isArray(relations.product_categories) && relations.product_categories.length > 0) {
|
|
877
|
+
relations.product_categories = relations.product_categories.map(id =>
|
|
878
|
+
typeof id === 'object' && id.id ? id.id : id
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
if (Array.isArray(relations.tags) && relations.tags.length > 0) {
|
|
882
|
+
relations.tags = relations.tags.map(id =>
|
|
883
|
+
typeof id === 'object' && id.id ? id.id : id
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
if (Array.isArray(relations.images) && relations.images.length > 0) {
|
|
887
|
+
relations.images = relations.images.map(id =>
|
|
888
|
+
typeof id === 'object' && id.id ? id.id : id
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Use entityService for Strapi v5 document model compatibility
|
|
893
|
+
const product = await strapi.entityService.create('plugin::webbycommerce.product', {
|
|
894
|
+
data,
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
// Update product with relations to ensure they're properly linked in document model
|
|
898
|
+
const updateData = {};
|
|
899
|
+
if (relations.product_categories.length > 0) {
|
|
900
|
+
updateData.product_categories = relations.product_categories;
|
|
901
|
+
}
|
|
902
|
+
if (relations.tags.length > 0) {
|
|
903
|
+
updateData.tags = relations.tags;
|
|
904
|
+
}
|
|
905
|
+
if (relations.images.length > 0) {
|
|
906
|
+
updateData.images = relations.images;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
let updatedProduct = product;
|
|
910
|
+
if (Object.keys(updateData).length > 0) {
|
|
911
|
+
updatedProduct = await strapi.entityService.update('plugin::webbycommerce.product', product.id, {
|
|
912
|
+
data: updateData,
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Populate relations for response
|
|
917
|
+
const populated = await strapi.entityService.findOne('plugin::webbycommerce.product', updatedProduct.id, {
|
|
918
|
+
populate: ['product_categories', 'tags', 'images', 'variations'],
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
ctx.send({ data: populated || updatedProduct });
|
|
922
|
+
} catch (error) {
|
|
923
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in create:`, error);
|
|
924
|
+
ctx.internalServerError('Failed to create product.');
|
|
925
|
+
}
|
|
926
|
+
},
|
|
927
|
+
|
|
928
|
+
async update(ctx) {
|
|
929
|
+
try {
|
|
930
|
+
const { id } = ctx.params;
|
|
931
|
+
const data = ctx.request.body.data || ctx.request.body;
|
|
932
|
+
|
|
933
|
+
// Validate numeric fields if they are being updated
|
|
934
|
+
if (data.price !== undefined && data.price !== null) {
|
|
935
|
+
const parsedPrice = parseFloat(data.price);
|
|
936
|
+
if (isNaN(parsedPrice) || parsedPrice < 0) {
|
|
937
|
+
return ctx.badRequest('Price must be a valid positive number.');
|
|
938
|
+
}
|
|
939
|
+
data.price = parsedPrice;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
if (data.sale_price !== undefined && data.sale_price !== null) {
|
|
943
|
+
const parsedSalePrice = parseFloat(data.sale_price);
|
|
944
|
+
if (isNaN(parsedSalePrice) || parsedSalePrice < 0) {
|
|
945
|
+
return ctx.badRequest('Sale price must be a valid positive number.');
|
|
946
|
+
}
|
|
947
|
+
data.sale_price = parsedSalePrice;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (data.stock_quantity !== undefined && data.stock_quantity !== null) {
|
|
951
|
+
const parsedStockQuantity = parseInt(data.stock_quantity, 10);
|
|
952
|
+
if (isNaN(parsedStockQuantity) || parsedStockQuantity < 0) {
|
|
953
|
+
return ctx.badRequest('Stock quantity must be a valid non-negative integer.');
|
|
954
|
+
}
|
|
955
|
+
data.stock_quantity = parsedStockQuantity;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (data.weight !== undefined && data.weight !== null) {
|
|
959
|
+
const parsedWeight = parseFloat(data.weight);
|
|
960
|
+
if (isNaN(parsedWeight) || parsedWeight < 0) {
|
|
961
|
+
return ctx.badRequest('Weight must be a valid positive number.');
|
|
962
|
+
}
|
|
963
|
+
data.weight = parsedWeight;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const product = await strapi.db.query('plugin::webbycommerce.product').update({
|
|
967
|
+
where: { id },
|
|
968
|
+
data,
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
if (!product) {
|
|
972
|
+
return ctx.notFound('Product not found.');
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Populate relations for response
|
|
976
|
+
const populated = await strapi.db.query('plugin::webbycommerce.product').findOne({
|
|
977
|
+
where: { id: product.id },
|
|
978
|
+
populate: ['product_categories', 'tags', 'images', 'variations'],
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
ctx.send({ data: populated || product });
|
|
982
|
+
} catch (error) {
|
|
983
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in update:`, error);
|
|
984
|
+
ctx.internalServerError('Failed to update product.');
|
|
985
|
+
}
|
|
986
|
+
},
|
|
987
|
+
|
|
988
|
+
async delete(ctx) {
|
|
989
|
+
try {
|
|
990
|
+
const { id } = ctx.params;
|
|
991
|
+
|
|
992
|
+
const product = await strapi.db.query('plugin::webbycommerce.product').delete({
|
|
993
|
+
where: { id },
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
if (!product) {
|
|
997
|
+
return ctx.notFound('Product not found.');
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
ctx.send({ data: product });
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in delete:`, error);
|
|
1003
|
+
ctx.internalServerError('Failed to delete product.');
|
|
1004
|
+
}
|
|
1005
|
+
},
|
|
1006
|
+
};
|