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