@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,513 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* shipping service
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { createCoreService } = require('@strapi/strapi').factories;
|
|
8
|
+
|
|
9
|
+
module.exports = createCoreService('plugin::webbycommerce.shipping', ({ strapi }) => ({
|
|
10
|
+
/**
|
|
11
|
+
* Parse comma/newline separated text into array tokens.
|
|
12
|
+
* Also supports legacy arrays (backwards compatible).
|
|
13
|
+
*/
|
|
14
|
+
parseTextList(value) {
|
|
15
|
+
if (!value) return [];
|
|
16
|
+
if (Array.isArray(value)) return value.map(String).map((s) => s.trim()).filter(Boolean);
|
|
17
|
+
if (typeof value !== 'string') return [];
|
|
18
|
+
return value
|
|
19
|
+
.split(/[\n,;]+/g)
|
|
20
|
+
.map((t) => t.trim())
|
|
21
|
+
.filter(Boolean);
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parse postal codes text into patterns/ranges.
|
|
26
|
+
* Accepts:
|
|
27
|
+
* - string: lines/tokens like "123*" or "1000-2000"
|
|
28
|
+
* - legacy array: ["123*", {min:1000,max:2000}]
|
|
29
|
+
*/
|
|
30
|
+
parsePostalCodes(value) {
|
|
31
|
+
if (!value) return [];
|
|
32
|
+
if (Array.isArray(value)) return value;
|
|
33
|
+
if (typeof value !== 'string') return [];
|
|
34
|
+
return value
|
|
35
|
+
.split(/[\n,;]+/g)
|
|
36
|
+
.map((t) => t.trim())
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.map((token) => {
|
|
39
|
+
const m = token.match(/^\s*(\d+)\s*-\s*(\d+)\s*$/);
|
|
40
|
+
if (m) return { min: parseInt(m[1], 10), max: parseInt(m[2], 10) };
|
|
41
|
+
return token;
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Calculate shipping costs for cart items and address
|
|
47
|
+
*/
|
|
48
|
+
async calculateShipping(cartItems, shippingAddress) {
|
|
49
|
+
try {
|
|
50
|
+
// Get all active shipping zones
|
|
51
|
+
const zones = await strapi.db.query('plugin::webbycommerce.shipping-zone').findMany({
|
|
52
|
+
where: { is_active: true },
|
|
53
|
+
orderBy: { sort_order: 'asc' },
|
|
54
|
+
populate: ['shippingMethods', 'location'],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Find matching zones for the shipping address
|
|
58
|
+
const matchingZones = zones.filter(zone => this.addressMatchesZone(shippingAddress, zone));
|
|
59
|
+
|
|
60
|
+
if (matchingZones.length === 0) {
|
|
61
|
+
return { methods: [], message: 'No shipping methods available for this address.' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Get all shipping methods from matching zones
|
|
65
|
+
let availableMethods = [];
|
|
66
|
+
for (const zone of matchingZones) {
|
|
67
|
+
const methods = await strapi.db.query('plugin::webbycommerce.shipping-method').findMany({
|
|
68
|
+
where: {
|
|
69
|
+
shippingZone: zone.id,
|
|
70
|
+
is_active: true,
|
|
71
|
+
},
|
|
72
|
+
orderBy: { sort_order: 'asc' },
|
|
73
|
+
populate: ['shippingRates', 'shippingZone'],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
availableMethods = availableMethods.concat(methods);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Remove duplicates (method might be in multiple zones)
|
|
80
|
+
const uniqueMethods = availableMethods.filter((method, index, self) =>
|
|
81
|
+
index === self.findIndex(m => String(m.id) === String(method.id))
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Calculate costs for each method
|
|
85
|
+
const methodsWithCosts = [];
|
|
86
|
+
for (const method of uniqueMethods) {
|
|
87
|
+
try {
|
|
88
|
+
const cost = await this.calculateShippingCost(cartItems, shippingAddress, method);
|
|
89
|
+
|
|
90
|
+
// Check free shipping
|
|
91
|
+
let finalCost = cost;
|
|
92
|
+
if (method.is_free_shipping && method.free_shipping_threshold) {
|
|
93
|
+
const cartTotal = cartItems.reduce((total, item) =>
|
|
94
|
+
total + (parseFloat(item.price) * item.quantity), 0
|
|
95
|
+
);
|
|
96
|
+
if (cartTotal >= parseFloat(method.free_shipping_threshold)) {
|
|
97
|
+
finalCost = 0;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
methodsWithCosts.push({
|
|
102
|
+
id: method.id,
|
|
103
|
+
name: method.name,
|
|
104
|
+
description: method.description,
|
|
105
|
+
carrier: method.carrier,
|
|
106
|
+
service_type: method.service_type,
|
|
107
|
+
transit_time: method.transit_time,
|
|
108
|
+
cost: finalCost,
|
|
109
|
+
calculated_cost: finalCost,
|
|
110
|
+
currency: method.shippingRates?.[0]?.currency || 'USD',
|
|
111
|
+
zone: {
|
|
112
|
+
id: method.shippingZone.id,
|
|
113
|
+
name: method.shippingZone.name,
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
} catch (error) {
|
|
117
|
+
strapi.log.error(`Error calculating cost for method ${method.id}:`, error);
|
|
118
|
+
// Skip this method if calculation fails
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Apply shipping rules
|
|
123
|
+
const finalMethods = await this.applyShippingRules(methodsWithCosts, cartItems, shippingAddress);
|
|
124
|
+
const uniqueFinalMethods = finalMethods.filter((method, index, self) =>
|
|
125
|
+
index === self.findIndex(m => String(m.id) === String(method.id))
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
methods: uniqueFinalMethods,
|
|
130
|
+
address: shippingAddress,
|
|
131
|
+
};
|
|
132
|
+
} catch (error) {
|
|
133
|
+
throw new Error(`Failed to calculate shipping: ${error.message}`);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check if an address matches a shipping zone
|
|
139
|
+
*/
|
|
140
|
+
addressMatchesZone(address, zone) {
|
|
141
|
+
const location = zone.location || {};
|
|
142
|
+
const zoneCountries = this.parseTextList(location.countries);
|
|
143
|
+
const zoneStates = this.parseTextList(location.states);
|
|
144
|
+
const zonePostalCodes = this.parsePostalCodes(location.postal_codes);
|
|
145
|
+
|
|
146
|
+
// Check country match
|
|
147
|
+
if (zoneCountries.length > 0) {
|
|
148
|
+
if (!zoneCountries.includes(address.country)) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check state match
|
|
154
|
+
if (zoneStates.length > 0) {
|
|
155
|
+
if (!zoneStates.includes(address.region)) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check postal code match
|
|
161
|
+
if (zonePostalCodes.length > 0) {
|
|
162
|
+
const postcode = address.postcode;
|
|
163
|
+
let matches = false;
|
|
164
|
+
|
|
165
|
+
for (const pattern of zonePostalCodes) {
|
|
166
|
+
if (typeof pattern === 'string') {
|
|
167
|
+
// Check if postcode matches pattern (supports wildcards like "123*")
|
|
168
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
169
|
+
if (regex.test(postcode)) {
|
|
170
|
+
matches = true;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
} else if (typeof pattern === 'object' && pattern.min && pattern.max) {
|
|
174
|
+
// Check if postcode is within range
|
|
175
|
+
const numPostcode = parseInt(postcode, 10);
|
|
176
|
+
if (!isNaN(numPostcode) && numPostcode >= pattern.min && numPostcode <= pattern.max) {
|
|
177
|
+
matches = true;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!matches) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return true;
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Calculate shipping cost based on cart items and method
|
|
193
|
+
*/
|
|
194
|
+
async calculateShippingCost(cartItems, shippingAddress, method) {
|
|
195
|
+
let totalCost = parseFloat(method.handling_fee || 0);
|
|
196
|
+
|
|
197
|
+
// Get active rates for this method
|
|
198
|
+
const rates = await strapi.db.query('plugin::webbycommerce.shipping-rate').findMany({
|
|
199
|
+
where: {
|
|
200
|
+
shippingMethod: method.id,
|
|
201
|
+
is_active: true,
|
|
202
|
+
},
|
|
203
|
+
orderBy: { sort_order: 'asc', min_value: 'asc' },
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Calculate based on condition type
|
|
207
|
+
for (const rate of rates) {
|
|
208
|
+
let conditionValue = 0;
|
|
209
|
+
|
|
210
|
+
switch (rate.condition_type) {
|
|
211
|
+
case 'weight':
|
|
212
|
+
// Calculate total weight
|
|
213
|
+
for (const item of cartItems) {
|
|
214
|
+
const weight = parseFloat(item.product?.weight || 0);
|
|
215
|
+
conditionValue += weight * item.quantity;
|
|
216
|
+
}
|
|
217
|
+
break;
|
|
218
|
+
|
|
219
|
+
case 'price':
|
|
220
|
+
// Calculate subtotal
|
|
221
|
+
for (const item of cartItems) {
|
|
222
|
+
conditionValue += parseFloat(item.price) * item.quantity;
|
|
223
|
+
}
|
|
224
|
+
break;
|
|
225
|
+
|
|
226
|
+
case 'quantity':
|
|
227
|
+
// Calculate total quantity
|
|
228
|
+
for (const item of cartItems) {
|
|
229
|
+
conditionValue += item.quantity;
|
|
230
|
+
}
|
|
231
|
+
break;
|
|
232
|
+
|
|
233
|
+
default:
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Check if condition value falls within rate range
|
|
238
|
+
const minValue = parseFloat(rate.min_value);
|
|
239
|
+
const maxValue = rate.max_value ? parseFloat(rate.max_value) : null;
|
|
240
|
+
|
|
241
|
+
if (conditionValue >= minValue && (maxValue === null || conditionValue <= maxValue)) {
|
|
242
|
+
totalCost += parseFloat(rate.rate);
|
|
243
|
+
break; // Use first matching rate
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return totalCost;
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Normalize `applies_to_methods` which can be:
|
|
252
|
+
* - null/undefined (meaning: all methods)
|
|
253
|
+
* - legacy JSON array of IDs
|
|
254
|
+
* - populated relation array of entities ({ id, ... })
|
|
255
|
+
*/
|
|
256
|
+
getAppliesToMethodIds(appliesToMethods) {
|
|
257
|
+
if (!Array.isArray(appliesToMethods)) return [];
|
|
258
|
+
return appliesToMethods
|
|
259
|
+
.map((m) => (m && typeof m === 'object' ? m.id : m))
|
|
260
|
+
.filter((id) => id !== null && id !== undefined)
|
|
261
|
+
.map((id) => String(id));
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Apply shipping rules to available methods
|
|
266
|
+
*/
|
|
267
|
+
async applyShippingRules(methods, cartItems, shippingAddress) {
|
|
268
|
+
const rules = await strapi.db.query('plugin::webbycommerce.shipping-rule').findMany({
|
|
269
|
+
where: { is_active: true },
|
|
270
|
+
orderBy: { priority: 'desc' }, // Higher priority first
|
|
271
|
+
populate: { applies_to_methods: true },
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const filteredMethods = [];
|
|
275
|
+
|
|
276
|
+
for (const method of methods) {
|
|
277
|
+
let isEligible = true;
|
|
278
|
+
let modifiedCost = method.calculated_cost || 0;
|
|
279
|
+
let messages = [];
|
|
280
|
+
|
|
281
|
+
// Check if rule applies to this method
|
|
282
|
+
const methodId = String(method.id);
|
|
283
|
+
const applicableRules = rules.filter((rule) => {
|
|
284
|
+
const ids = this.getAppliesToMethodIds(rule.applies_to_methods);
|
|
285
|
+
return ids.length === 0 || ids.includes(methodId);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
for (const rule of applicableRules) {
|
|
289
|
+
let conditionMet = false;
|
|
290
|
+
let conditionValue = null;
|
|
291
|
+
|
|
292
|
+
// Evaluate condition
|
|
293
|
+
switch (rule.condition_type) {
|
|
294
|
+
case 'product_category':
|
|
295
|
+
for (const item of cartItems) {
|
|
296
|
+
const categories = item.product?.categories || [];
|
|
297
|
+
const categoryIds = categories.map(cat => cat.id);
|
|
298
|
+
conditionMet = this.evaluateCondition(categoryIds, rule.condition_operator, rule.condition_value);
|
|
299
|
+
if (conditionMet) break;
|
|
300
|
+
}
|
|
301
|
+
break;
|
|
302
|
+
|
|
303
|
+
case 'product_tag':
|
|
304
|
+
for (const item of cartItems) {
|
|
305
|
+
const tags = item.product?.tags || [];
|
|
306
|
+
const tagIds = tags.map(tag => tag.id);
|
|
307
|
+
conditionMet = this.evaluateCondition(tagIds, rule.condition_operator, rule.condition_value);
|
|
308
|
+
if (conditionMet) break;
|
|
309
|
+
}
|
|
310
|
+
break;
|
|
311
|
+
|
|
312
|
+
case 'order_total':
|
|
313
|
+
conditionValue = cartItems.reduce((total, item) =>
|
|
314
|
+
total + (parseFloat(item.price) * item.quantity), 0
|
|
315
|
+
);
|
|
316
|
+
conditionMet = this.evaluateCondition(conditionValue, rule.condition_operator, rule.condition_value);
|
|
317
|
+
break;
|
|
318
|
+
|
|
319
|
+
case 'cart_quantity':
|
|
320
|
+
conditionValue = cartItems.reduce((total, item) => total + item.quantity, 0);
|
|
321
|
+
conditionMet = this.evaluateCondition(conditionValue, rule.condition_operator, rule.condition_value);
|
|
322
|
+
break;
|
|
323
|
+
|
|
324
|
+
case 'shipping_address':
|
|
325
|
+
conditionValue = shippingAddress[rule.condition_value];
|
|
326
|
+
conditionMet = this.evaluateCondition(conditionValue, rule.condition_operator, rule.condition_value);
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (conditionMet) {
|
|
331
|
+
// Apply rule action
|
|
332
|
+
switch (rule.action_type) {
|
|
333
|
+
case 'hide_method':
|
|
334
|
+
isEligible = false;
|
|
335
|
+
break;
|
|
336
|
+
|
|
337
|
+
case 'add_fee':
|
|
338
|
+
modifiedCost += parseFloat(rule.action_value || 0);
|
|
339
|
+
if (rule.action_message) messages.push(rule.action_message);
|
|
340
|
+
break;
|
|
341
|
+
|
|
342
|
+
case 'subtract_fee':
|
|
343
|
+
modifiedCost -= parseFloat(rule.action_value || 0);
|
|
344
|
+
if (rule.action_message) messages.push(rule.action_message);
|
|
345
|
+
break;
|
|
346
|
+
|
|
347
|
+
case 'set_rate':
|
|
348
|
+
modifiedCost = parseFloat(rule.action_value || 0);
|
|
349
|
+
if (rule.action_message) messages.push(rule.action_message);
|
|
350
|
+
break;
|
|
351
|
+
|
|
352
|
+
case 'multiply_rate':
|
|
353
|
+
modifiedCost *= parseFloat(rule.action_value || 1);
|
|
354
|
+
if (rule.action_message) messages.push(rule.action_message);
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (isEligible) {
|
|
361
|
+
filteredMethods.push({
|
|
362
|
+
...method,
|
|
363
|
+
calculated_cost: Math.max(0, modifiedCost), // Ensure cost is not negative
|
|
364
|
+
rule_messages: messages,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return filteredMethods;
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Evaluate condition based on operator
|
|
374
|
+
*/
|
|
375
|
+
evaluateCondition(value, operator, conditionValue) {
|
|
376
|
+
switch (operator) {
|
|
377
|
+
case 'equals':
|
|
378
|
+
return value === conditionValue;
|
|
379
|
+
case 'not_equals':
|
|
380
|
+
return value !== conditionValue;
|
|
381
|
+
case 'greater_than':
|
|
382
|
+
return parseFloat(value) > parseFloat(conditionValue);
|
|
383
|
+
case 'less_than':
|
|
384
|
+
return parseFloat(value) < parseFloat(conditionValue);
|
|
385
|
+
case 'contains':
|
|
386
|
+
if (Array.isArray(value)) {
|
|
387
|
+
return value.includes(conditionValue);
|
|
388
|
+
}
|
|
389
|
+
return String(value).includes(String(conditionValue));
|
|
390
|
+
case 'not_contains':
|
|
391
|
+
if (Array.isArray(value)) {
|
|
392
|
+
return !value.includes(conditionValue);
|
|
393
|
+
}
|
|
394
|
+
return !String(value).includes(String(conditionValue));
|
|
395
|
+
case 'in':
|
|
396
|
+
return Array.isArray(conditionValue) && conditionValue.includes(value);
|
|
397
|
+
case 'not_in':
|
|
398
|
+
return !Array.isArray(conditionValue) || !conditionValue.includes(value);
|
|
399
|
+
default:
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Validate shipping zone data
|
|
406
|
+
*/
|
|
407
|
+
validateShippingZone(data) {
|
|
408
|
+
const errors = [];
|
|
409
|
+
|
|
410
|
+
if (!data.name || typeof data.name !== 'string' || data.name.trim().length === 0) {
|
|
411
|
+
errors.push('Zone name is required.');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Now stored under `location` component as text; accept legacy array inputs too.
|
|
415
|
+
if (data.location && typeof data.location !== 'object') {
|
|
416
|
+
errors.push('Location must be an object.');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return errors;
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Validate shipping method data
|
|
424
|
+
*/
|
|
425
|
+
validateShippingMethod(data) {
|
|
426
|
+
const errors = [];
|
|
427
|
+
|
|
428
|
+
if (!data.name || typeof data.name !== 'string' || data.name.trim().length === 0) {
|
|
429
|
+
errors.push('Method name is required.');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (!data.carrier || typeof data.carrier !== 'string' || data.carrier.trim().length === 0) {
|
|
433
|
+
errors.push('Carrier is required.');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (!data.service_type || typeof data.service_type !== 'string' || data.service_type.trim().length === 0) {
|
|
437
|
+
errors.push('Service type is required.');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (!data.shippingZone) {
|
|
441
|
+
errors.push('Shipping zone is required.');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (data.handling_fee !== undefined && (isNaN(parseFloat(data.handling_fee)) || parseFloat(data.handling_fee) < 0)) {
|
|
445
|
+
errors.push('Handling fee must be a valid positive number.');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return errors;
|
|
449
|
+
},
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Validate shipping rate data
|
|
453
|
+
*/
|
|
454
|
+
validateShippingRate(data) {
|
|
455
|
+
const errors = [];
|
|
456
|
+
|
|
457
|
+
if (!data.name || typeof data.name !== 'string' || data.name.trim().length === 0) {
|
|
458
|
+
errors.push('Rate name is required.');
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const validConditionTypes = ['weight', 'price', 'quantity', 'volume', 'dimension'];
|
|
462
|
+
if (!data.condition_type || !validConditionTypes.includes(data.condition_type)) {
|
|
463
|
+
errors.push('Valid condition type is required.');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (data.min_value === undefined || data.min_value === null) {
|
|
467
|
+
errors.push('Minimum value is required.');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (data.rate === undefined || data.rate === null) {
|
|
471
|
+
errors.push('Rate is required.');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (!data.shippingMethod) {
|
|
475
|
+
errors.push('Shipping method is required.');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return errors;
|
|
479
|
+
},
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Validate shipping rule data
|
|
483
|
+
*/
|
|
484
|
+
validateShippingRule(data) {
|
|
485
|
+
const errors = [];
|
|
486
|
+
|
|
487
|
+
if (!data.name || typeof data.name !== 'string' || data.name.trim().length === 0) {
|
|
488
|
+
errors.push('Rule name is required.');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const validRuleTypes = ['restriction', 'surcharge', 'discount', 'requirement'];
|
|
492
|
+
if (!data.rule_type || !validRuleTypes.includes(data.rule_type)) {
|
|
493
|
+
errors.push('Valid rule type is required.');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const validConditionTypes = ['product_category', 'product_tag', 'product_weight', 'order_total', 'customer_group', 'shipping_address', 'cart_quantity'];
|
|
497
|
+
if (!data.condition_type || !validConditionTypes.includes(data.condition_type)) {
|
|
498
|
+
errors.push('Valid condition type is required.');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const validOperators = ['equals', 'not_equals', 'greater_than', 'less_than', 'contains', 'not_contains', 'in', 'not_in'];
|
|
502
|
+
if (!data.condition_operator || !validOperators.includes(data.condition_operator)) {
|
|
503
|
+
errors.push('Valid condition operator is required.');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const validActionTypes = ['hide_method', 'add_fee', 'subtract_fee', 'set_rate', 'multiply_rate'];
|
|
507
|
+
if (!data.action_type || !validActionTypes.includes(data.action_type)) {
|
|
508
|
+
errors.push('Valid action type is required.');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return errors;
|
|
512
|
+
},
|
|
513
|
+
}));
|