@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,1046 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const PLUGIN_ID = 'webbycommerce';
|
|
4
|
+
const { ensureEcommercePermission } = require('../utils/check-ecommerce-permission');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Helper function to check if an address matches a shipping zone
|
|
8
|
+
*/
|
|
9
|
+
const parseTextList = (value) => {
|
|
10
|
+
if (!value) return [];
|
|
11
|
+
if (Array.isArray(value)) return value.map(String).map((s) => s.trim()).filter(Boolean);
|
|
12
|
+
if (typeof value !== 'string') return [];
|
|
13
|
+
return value
|
|
14
|
+
.split(/[\n,;]+/g)
|
|
15
|
+
.map((t) => t.trim())
|
|
16
|
+
.filter(Boolean);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const parsePostalCodes = (value) => {
|
|
20
|
+
if (!value) return [];
|
|
21
|
+
if (Array.isArray(value)) return value;
|
|
22
|
+
if (typeof value !== 'string') return [];
|
|
23
|
+
return value
|
|
24
|
+
.split(/[\n,;]+/g)
|
|
25
|
+
.map((t) => t.trim())
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
.map((token) => {
|
|
28
|
+
const m = token.match(/^\s*(\d+)\s*-\s*(\d+)\s*$/);
|
|
29
|
+
if (m) return { min: parseInt(m[1], 10), max: parseInt(m[2], 10) };
|
|
30
|
+
return token;
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const addressMatchesZone = (address, zone) => {
|
|
35
|
+
// Location is repeatable, so it's an array of location objects
|
|
36
|
+
const locations = Array.isArray(zone.location) ? zone.location : [zone.location].filter(Boolean);
|
|
37
|
+
|
|
38
|
+
// Zone matches if ANY location rule matches the address
|
|
39
|
+
for (const location of locations) {
|
|
40
|
+
const zoneCountries = parseTextList(location.countries);
|
|
41
|
+
const zoneStates = parseTextList(location.states);
|
|
42
|
+
const zonePostalCodes = parsePostalCodes(location.postal_codes);
|
|
43
|
+
|
|
44
|
+
// Check country match
|
|
45
|
+
if (zoneCountries.length > 0) {
|
|
46
|
+
if (!zoneCountries.includes(address.country)) {
|
|
47
|
+
continue; // Try next location rule
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check state match
|
|
52
|
+
if (zoneStates.length > 0) {
|
|
53
|
+
if (!zoneStates.includes(address.region)) {
|
|
54
|
+
continue; // Try next location rule
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check postal code match
|
|
59
|
+
if (zonePostalCodes.length > 0) {
|
|
60
|
+
const postcode = address.postcode;
|
|
61
|
+
let matches = false;
|
|
62
|
+
|
|
63
|
+
for (const pattern of zonePostalCodes) {
|
|
64
|
+
if (typeof pattern === 'string') {
|
|
65
|
+
// Check if postcode matches pattern (supports wildcards like "123*")
|
|
66
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
67
|
+
if (regex.test(postcode)) {
|
|
68
|
+
matches = true;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
} else if (typeof pattern === 'object' && pattern.min && pattern.max) {
|
|
72
|
+
// Check if postcode is within range
|
|
73
|
+
const numPostcode = parseInt(postcode, 10);
|
|
74
|
+
if (!isNaN(numPostcode) && numPostcode >= pattern.min && numPostcode <= pattern.max) {
|
|
75
|
+
matches = true;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!matches) {
|
|
82
|
+
continue; // Try next location rule
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// If we reach here, this location rule matches
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// No location rule matched
|
|
91
|
+
return false;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Calculate shipping cost based on cart items and shipping address
|
|
96
|
+
*/
|
|
97
|
+
const calculateShippingCost = async (cartItems, shippingAddress, method) => {
|
|
98
|
+
let totalCost = parseFloat(method.handling_fee || 0);
|
|
99
|
+
|
|
100
|
+
// Get active rates for this method
|
|
101
|
+
const rates = await strapi.db.query('plugin::webbycommerce.shipping-rate').findMany({
|
|
102
|
+
where: {
|
|
103
|
+
shippingMethod: method.id,
|
|
104
|
+
is_active: true,
|
|
105
|
+
},
|
|
106
|
+
orderBy: { sort_order: 'asc', min_value: 'asc' },
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Calculate based on condition type
|
|
110
|
+
for (const rate of rates) {
|
|
111
|
+
let conditionValue = 0;
|
|
112
|
+
|
|
113
|
+
switch (rate.condition_type) {
|
|
114
|
+
case 'weight':
|
|
115
|
+
// Calculate total weight
|
|
116
|
+
for (const item of cartItems) {
|
|
117
|
+
const weight = parseFloat(item.product?.weight || 0);
|
|
118
|
+
conditionValue += weight * item.quantity;
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
|
|
122
|
+
case 'price':
|
|
123
|
+
// Calculate subtotal
|
|
124
|
+
for (const item of cartItems) {
|
|
125
|
+
conditionValue += parseFloat(item.price) * item.quantity;
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
|
|
129
|
+
case 'quantity':
|
|
130
|
+
// Calculate total quantity
|
|
131
|
+
for (const item of cartItems) {
|
|
132
|
+
conditionValue += item.quantity;
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
|
|
136
|
+
default:
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check if condition value falls within rate range
|
|
141
|
+
const minValue = parseFloat(rate.min_value);
|
|
142
|
+
const maxValue = rate.max_value ? parseFloat(rate.max_value) : null;
|
|
143
|
+
|
|
144
|
+
if (conditionValue >= minValue && (maxValue === null || conditionValue <= maxValue)) {
|
|
145
|
+
totalCost += parseFloat(rate.rate);
|
|
146
|
+
break; // Use first matching rate
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return totalCost;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Normalize `applies_to_methods` which can be:
|
|
155
|
+
* - null/undefined (meaning: all methods)
|
|
156
|
+
* - legacy JSON array of IDs
|
|
157
|
+
* - populated relation array of entities ({ id, ... })
|
|
158
|
+
*/
|
|
159
|
+
const getAppliesToMethodIds = (appliesToMethods) => {
|
|
160
|
+
if (!Array.isArray(appliesToMethods)) return [];
|
|
161
|
+
return appliesToMethods
|
|
162
|
+
.map((m) => (m && typeof m === 'object' ? m.id : m))
|
|
163
|
+
.filter((id) => id !== null && id !== undefined)
|
|
164
|
+
.map((id) => String(id));
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Apply shipping rules to available methods
|
|
169
|
+
*/
|
|
170
|
+
const applyShippingRules = async (methods, cartItems, shippingAddress) => {
|
|
171
|
+
const rules = await strapi.db.query('plugin::webbycommerce.shipping-rule').findMany({
|
|
172
|
+
where: { is_active: true },
|
|
173
|
+
orderBy: { priority: 'desc' }, // Higher priority first
|
|
174
|
+
populate: { applies_to_methods: true },
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const filteredMethods = [];
|
|
178
|
+
|
|
179
|
+
for (const method of methods) {
|
|
180
|
+
let isEligible = true;
|
|
181
|
+
let modifiedCost = method.calculated_cost || 0;
|
|
182
|
+
let messages = [];
|
|
183
|
+
|
|
184
|
+
// Check if rule applies to this method
|
|
185
|
+
const methodId = String(method.id);
|
|
186
|
+
const applicableRules = rules.filter((rule) => {
|
|
187
|
+
const ids = getAppliesToMethodIds(rule.applies_to_methods);
|
|
188
|
+
return ids.length === 0 || ids.includes(methodId);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
for (const rule of applicableRules) {
|
|
192
|
+
let conditionMet = false;
|
|
193
|
+
let conditionValue = null;
|
|
194
|
+
|
|
195
|
+
// Evaluate condition
|
|
196
|
+
switch (rule.condition_type) {
|
|
197
|
+
case 'product_category':
|
|
198
|
+
for (const item of cartItems) {
|
|
199
|
+
const categories = item.product?.categories || [];
|
|
200
|
+
const categoryIds = categories.map(cat => cat.id);
|
|
201
|
+
conditionMet = evaluateCondition(categoryIds, rule.condition_operator, rule.condition_value);
|
|
202
|
+
if (conditionMet) break;
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
|
|
206
|
+
case 'product_tag':
|
|
207
|
+
for (const item of cartItems) {
|
|
208
|
+
const tags = item.product?.tags || [];
|
|
209
|
+
const tagIds = tags.map(tag => tag.id);
|
|
210
|
+
conditionMet = evaluateCondition(tagIds, rule.condition_operator, rule.condition_value);
|
|
211
|
+
if (conditionMet) break;
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
|
|
215
|
+
case 'order_total':
|
|
216
|
+
conditionValue = cartItems.reduce((total, item) =>
|
|
217
|
+
total + (parseFloat(item.price) * item.quantity), 0
|
|
218
|
+
);
|
|
219
|
+
conditionMet = evaluateCondition(conditionValue, rule.condition_operator, rule.condition_value);
|
|
220
|
+
break;
|
|
221
|
+
|
|
222
|
+
case 'cart_quantity':
|
|
223
|
+
conditionValue = cartItems.reduce((total, item) => total + item.quantity, 0);
|
|
224
|
+
conditionMet = evaluateCondition(conditionValue, rule.condition_operator, rule.condition_value);
|
|
225
|
+
break;
|
|
226
|
+
|
|
227
|
+
case 'shipping_address':
|
|
228
|
+
conditionValue = shippingAddress[rule.condition_value];
|
|
229
|
+
conditionMet = evaluateCondition(conditionValue, rule.condition_operator, rule.condition_value);
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (conditionMet) {
|
|
234
|
+
// Apply rule action
|
|
235
|
+
switch (rule.action_type) {
|
|
236
|
+
case 'hide_method':
|
|
237
|
+
isEligible = false;
|
|
238
|
+
break;
|
|
239
|
+
|
|
240
|
+
case 'add_fee':
|
|
241
|
+
modifiedCost += parseFloat(rule.action_value || 0);
|
|
242
|
+
if (rule.action_message) messages.push(rule.action_message);
|
|
243
|
+
break;
|
|
244
|
+
|
|
245
|
+
case 'subtract_fee':
|
|
246
|
+
modifiedCost -= parseFloat(rule.action_value || 0);
|
|
247
|
+
if (rule.action_message) messages.push(rule.action_message);
|
|
248
|
+
break;
|
|
249
|
+
|
|
250
|
+
case 'set_rate':
|
|
251
|
+
modifiedCost = parseFloat(rule.action_value || 0);
|
|
252
|
+
if (rule.action_message) messages.push(rule.action_message);
|
|
253
|
+
break;
|
|
254
|
+
|
|
255
|
+
case 'multiply_rate':
|
|
256
|
+
modifiedCost *= parseFloat(rule.action_value || 1);
|
|
257
|
+
if (rule.action_message) messages.push(rule.action_message);
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (isEligible) {
|
|
264
|
+
filteredMethods.push({
|
|
265
|
+
...method,
|
|
266
|
+
calculated_cost: Math.max(0, modifiedCost), // Ensure cost is not negative
|
|
267
|
+
rule_messages: messages,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return filteredMethods;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Evaluate condition based on operator
|
|
277
|
+
*/
|
|
278
|
+
const evaluateCondition = (value, operator, conditionValue) => {
|
|
279
|
+
switch (operator) {
|
|
280
|
+
case 'equals':
|
|
281
|
+
return value === conditionValue;
|
|
282
|
+
case 'not_equals':
|
|
283
|
+
return value !== conditionValue;
|
|
284
|
+
case 'greater_than':
|
|
285
|
+
return parseFloat(value) > parseFloat(conditionValue);
|
|
286
|
+
case 'less_than':
|
|
287
|
+
return parseFloat(value) < parseFloat(conditionValue);
|
|
288
|
+
case 'contains':
|
|
289
|
+
if (Array.isArray(value)) {
|
|
290
|
+
return value.includes(conditionValue);
|
|
291
|
+
}
|
|
292
|
+
return String(value).includes(String(conditionValue));
|
|
293
|
+
case 'not_contains':
|
|
294
|
+
if (Array.isArray(value)) {
|
|
295
|
+
return !value.includes(conditionValue);
|
|
296
|
+
}
|
|
297
|
+
return !String(value).includes(String(conditionValue));
|
|
298
|
+
case 'in':
|
|
299
|
+
return Array.isArray(conditionValue) && conditionValue.includes(value);
|
|
300
|
+
case 'not_in':
|
|
301
|
+
return !Array.isArray(conditionValue) || !conditionValue.includes(value);
|
|
302
|
+
default:
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
module.exports = {
|
|
308
|
+
/**
|
|
309
|
+
* Get available shipping methods for cart and address
|
|
310
|
+
*/
|
|
311
|
+
async getShippingMethods(ctx) {
|
|
312
|
+
try {
|
|
313
|
+
const hasPermission = await ensureEcommercePermission(ctx);
|
|
314
|
+
if (!hasPermission) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const user = ctx.state.user;
|
|
319
|
+
if (!user) {
|
|
320
|
+
return ctx.unauthorized('Authentication required. Please provide a valid JWT token.');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const { cart_items, shipping_address } = ctx.request.body;
|
|
324
|
+
|
|
325
|
+
if (!cart_items || !Array.isArray(cart_items) || cart_items.length === 0) {
|
|
326
|
+
return ctx.badRequest('Cart items are required.');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!shipping_address || typeof shipping_address !== 'object') {
|
|
330
|
+
return ctx.badRequest('Shipping address is required.');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Validate shipping address has required fields
|
|
334
|
+
const requiredFields = ['country', 'city', 'street_address', 'postcode'];
|
|
335
|
+
for (const field of requiredFields) {
|
|
336
|
+
if (!shipping_address[field]) {
|
|
337
|
+
return ctx.badRequest(`Shipping address ${field} is required.`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Get all active shipping zones
|
|
342
|
+
const zones = await strapi.db.query('plugin::webbycommerce.shipping-zone').findMany({
|
|
343
|
+
where: { is_active: true },
|
|
344
|
+
orderBy: { sort_order: 'asc' },
|
|
345
|
+
populate: ['shippingMethods'],
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Find matching zones for the shipping address
|
|
349
|
+
const matchingZones = zones.filter(zone => addressMatchesZone(shipping_address, zone));
|
|
350
|
+
|
|
351
|
+
if (matchingZones.length === 0) {
|
|
352
|
+
return ctx.send({ data: [], message: 'No shipping methods available for this address.' });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Get all shipping methods from matching zones
|
|
356
|
+
let availableMethods = [];
|
|
357
|
+
for (const zone of matchingZones) {
|
|
358
|
+
const methods = await strapi.db.query('plugin::webbycommerce.shipping-method').findMany({
|
|
359
|
+
where: {
|
|
360
|
+
shippingZone: zone.id,
|
|
361
|
+
is_active: true,
|
|
362
|
+
},
|
|
363
|
+
orderBy: { sort_order: 'asc' },
|
|
364
|
+
populate: ['shippingRates', 'shippingZone'],
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
availableMethods = availableMethods.concat(methods);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Remove duplicates (method might be in multiple zones)
|
|
371
|
+
const uniqueMethods = availableMethods.filter((method, index, self) =>
|
|
372
|
+
index === self.findIndex(m => String(m.id) === String(method.id))
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
// Calculate costs for each method
|
|
376
|
+
const methodsWithCosts = [];
|
|
377
|
+
for (const method of uniqueMethods) {
|
|
378
|
+
try {
|
|
379
|
+
const cost = await calculateShippingCost(cart_items, shipping_address, method);
|
|
380
|
+
|
|
381
|
+
// Check free shipping
|
|
382
|
+
let finalCost = cost;
|
|
383
|
+
if (method.is_free_shipping && method.free_shipping_threshold) {
|
|
384
|
+
const cartTotal = cart_items.reduce((total, item) =>
|
|
385
|
+
total + (parseFloat(item.price) * item.quantity), 0
|
|
386
|
+
);
|
|
387
|
+
if (cartTotal >= parseFloat(method.free_shipping_threshold)) {
|
|
388
|
+
finalCost = 0;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
methodsWithCosts.push({
|
|
393
|
+
id: method.id,
|
|
394
|
+
name: method.name,
|
|
395
|
+
description: method.description,
|
|
396
|
+
carrier: method.carrier,
|
|
397
|
+
service_type: method.service_type,
|
|
398
|
+
transit_time: method.transit_time,
|
|
399
|
+
cost: finalCost,
|
|
400
|
+
calculated_cost: finalCost,
|
|
401
|
+
currency: method.shippingRates?.[0]?.currency || 'USD',
|
|
402
|
+
zone: {
|
|
403
|
+
id: method.shippingZone.id,
|
|
404
|
+
name: method.shippingZone.name,
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
} catch (error) {
|
|
408
|
+
strapi.log.error(`Error calculating cost for method ${method.id}:`, error);
|
|
409
|
+
// Skip this method if calculation fails
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Apply shipping rules
|
|
414
|
+
const finalMethods = await applyShippingRules(methodsWithCosts, cart_items, shipping_address);
|
|
415
|
+
const uniqueFinalMethods = finalMethods.filter((method, index, self) =>
|
|
416
|
+
index === self.findIndex(m => String(m.id) === String(method.id))
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
ctx.send({
|
|
420
|
+
data: uniqueFinalMethods,
|
|
421
|
+
meta: {
|
|
422
|
+
total: uniqueFinalMethods.length,
|
|
423
|
+
address: shipping_address,
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
} catch (error) {
|
|
427
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in getShippingMethods:`, error);
|
|
428
|
+
ctx.internalServerError('Failed to calculate shipping methods. Please try again.');
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Get all shipping zones (admin only)
|
|
434
|
+
*/
|
|
435
|
+
async getShippingZones(ctx) {
|
|
436
|
+
try {
|
|
437
|
+
// This endpoint requires admin permissions
|
|
438
|
+
const user = ctx.state.user;
|
|
439
|
+
const userRole = user?.role?.type;
|
|
440
|
+
|
|
441
|
+
if (!user || (userRole !== 'admin' && userRole !== 'super_admin')) {
|
|
442
|
+
return ctx.forbidden('Admin access required.');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const zones = await strapi.db.query('plugin::webbycommerce.shipping-zone').findMany({
|
|
446
|
+
orderBy: { sort_order: 'asc' },
|
|
447
|
+
populate: ['shippingMethods', 'location'],
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
ctx.send({ data: zones });
|
|
451
|
+
} catch (error) {
|
|
452
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in getShippingZones:`, error);
|
|
453
|
+
ctx.internalServerError('Failed to fetch shipping zones. Please try again.');
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Create shipping zone (admin only)
|
|
459
|
+
*/
|
|
460
|
+
async createShippingZone(ctx) {
|
|
461
|
+
try {
|
|
462
|
+
const user = ctx.state.user;
|
|
463
|
+
const userRole = user?.role?.type;
|
|
464
|
+
|
|
465
|
+
if (!user || (userRole !== 'admin' && userRole !== 'super_admin')) {
|
|
466
|
+
return ctx.forbidden('Admin access required.');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const {
|
|
470
|
+
name,
|
|
471
|
+
description,
|
|
472
|
+
// New shape (preferred):
|
|
473
|
+
location,
|
|
474
|
+
// Legacy shape (backwards compatible):
|
|
475
|
+
countries,
|
|
476
|
+
states,
|
|
477
|
+
postal_codes,
|
|
478
|
+
is_active,
|
|
479
|
+
sort_order,
|
|
480
|
+
} = ctx.request.body;
|
|
481
|
+
|
|
482
|
+
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
|
483
|
+
return ctx.badRequest('Zone name is required.');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
let finalLocation;
|
|
487
|
+
if (Array.isArray(location) && location.length > 0) {
|
|
488
|
+
// Location is already an array of objects
|
|
489
|
+
finalLocation = location;
|
|
490
|
+
} else if (location && typeof location === 'object') {
|
|
491
|
+
// Single location object - wrap in array since field is repeatable
|
|
492
|
+
finalLocation = [location];
|
|
493
|
+
} else {
|
|
494
|
+
// Fallback to legacy fields
|
|
495
|
+
finalLocation = [{
|
|
496
|
+
countries: Array.isArray(countries) ? countries.join(',') : countries,
|
|
497
|
+
states: Array.isArray(states) ? states.join(',') : states,
|
|
498
|
+
postal_codes: Array.isArray(postal_codes) ? postal_codes.map((p) => (typeof p === 'string' ? p : JSON.stringify(p))).join('\n') : postal_codes,
|
|
499
|
+
}];
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const zone = await strapi.db.query('plugin::webbycommerce.shipping-zone').create({
|
|
503
|
+
data: {
|
|
504
|
+
name: name.trim(),
|
|
505
|
+
description: description ? description.trim() : null,
|
|
506
|
+
location: finalLocation,
|
|
507
|
+
is_active: is_active !== undefined ? Boolean(is_active) : true,
|
|
508
|
+
sort_order: sort_order !== undefined ? parseInt(sort_order, 10) : 0,
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
ctx.send({ data: zone });
|
|
513
|
+
} catch (error) {
|
|
514
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in createShippingZone:`, error);
|
|
515
|
+
ctx.internalServerError('Failed to create shipping zone. Please try again.');
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Update shipping zone (admin only)
|
|
521
|
+
*/
|
|
522
|
+
async updateShippingZone(ctx) {
|
|
523
|
+
try {
|
|
524
|
+
const user = ctx.state.user;
|
|
525
|
+
const userRole = user?.role?.type;
|
|
526
|
+
|
|
527
|
+
if (!user || (userRole !== 'admin' && userRole !== 'super_admin')) {
|
|
528
|
+
return ctx.forbidden('Admin access required.');
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const { id } = ctx.params;
|
|
532
|
+
const {
|
|
533
|
+
name,
|
|
534
|
+
description,
|
|
535
|
+
// New shape (preferred):
|
|
536
|
+
location,
|
|
537
|
+
// Legacy shape (backwards compatible):
|
|
538
|
+
countries,
|
|
539
|
+
states,
|
|
540
|
+
postal_codes,
|
|
541
|
+
is_active,
|
|
542
|
+
sort_order,
|
|
543
|
+
} = ctx.request.body;
|
|
544
|
+
|
|
545
|
+
const existingZone = await strapi.db.query('plugin::webbycommerce.shipping-zone').findOne({
|
|
546
|
+
where: { id },
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
if (!existingZone) {
|
|
550
|
+
return ctx.notFound('Shipping zone not found.');
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const updateData = {};
|
|
554
|
+
if (name !== undefined) updateData.name = name.trim();
|
|
555
|
+
if (description !== undefined) updateData.description = description ? description.trim() : null;
|
|
556
|
+
if (location !== undefined) {
|
|
557
|
+
// Location field is repeatable, ensure it's an array
|
|
558
|
+
if (Array.isArray(location)) {
|
|
559
|
+
updateData.location = location;
|
|
560
|
+
} else if (typeof location === 'object') {
|
|
561
|
+
updateData.location = [location];
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
// allow legacy updates
|
|
565
|
+
if (location === undefined && (countries !== undefined || states !== undefined || postal_codes !== undefined)) {
|
|
566
|
+
const existingLocations = Array.isArray(existingZone.location) ? existingZone.location : [existingZone.location].filter(Boolean);
|
|
567
|
+
const existingLocation = existingLocations.length > 0 ? existingLocations[0] : {};
|
|
568
|
+
updateData.location = [{
|
|
569
|
+
...existingLocation,
|
|
570
|
+
...(countries !== undefined ? { countries: Array.isArray(countries) ? countries.join(',') : countries } : {}),
|
|
571
|
+
...(states !== undefined ? { states: Array.isArray(states) ? states.join(',') : states } : {}),
|
|
572
|
+
...(postal_codes !== undefined ? { postal_codes: Array.isArray(postal_codes) ? postal_codes.map((p) => (typeof p === 'string' ? p : JSON.stringify(p))).join('\n') : postal_codes } : {}),
|
|
573
|
+
}];
|
|
574
|
+
}
|
|
575
|
+
if (is_active !== undefined) updateData.is_active = Boolean(is_active);
|
|
576
|
+
if (sort_order !== undefined) updateData.sort_order = parseInt(sort_order, 10);
|
|
577
|
+
|
|
578
|
+
const updatedZone = await strapi.db.query('plugin::webbycommerce.shipping-zone').update({
|
|
579
|
+
where: { id },
|
|
580
|
+
data: updateData,
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
ctx.send({ data: updatedZone });
|
|
584
|
+
} catch (error) {
|
|
585
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in updateShippingZone:`, error);
|
|
586
|
+
ctx.internalServerError('Failed to update shipping zone. Please try again.');
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Delete shipping zone (admin only)
|
|
592
|
+
*/
|
|
593
|
+
async deleteShippingZone(ctx) {
|
|
594
|
+
try {
|
|
595
|
+
const user = ctx.state.user;
|
|
596
|
+
const userRole = user?.role?.type;
|
|
597
|
+
|
|
598
|
+
if (!user || (userRole !== 'admin' && userRole !== 'super_admin')) {
|
|
599
|
+
return ctx.forbidden('Admin access required.');
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const { id } = ctx.params;
|
|
603
|
+
|
|
604
|
+
const zone = await strapi.db.query('plugin::webbycommerce.shipping-zone').findOne({
|
|
605
|
+
where: { id },
|
|
606
|
+
populate: ['shippingMethods'],
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
if (!zone) {
|
|
610
|
+
return ctx.notFound('Shipping zone not found.');
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (zone.shippingMethods && zone.shippingMethods.length > 0) {
|
|
614
|
+
return ctx.badRequest('Cannot delete zone with associated shipping methods. Please remove methods first.');
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
await strapi.db.query('plugin::webbycommerce.shipping-zone').delete({
|
|
618
|
+
where: { id },
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
ctx.send({ data: { id } });
|
|
622
|
+
} catch (error) {
|
|
623
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in deleteShippingZone:`, error);
|
|
624
|
+
ctx.internalServerError('Failed to delete shipping zone. Please try again.');
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Get all shipping methods (admin only)
|
|
630
|
+
*/
|
|
631
|
+
async getShippingMethodsAdmin(ctx) {
|
|
632
|
+
try {
|
|
633
|
+
const user = ctx.state.user;
|
|
634
|
+
const userRole = user?.role?.type;
|
|
635
|
+
|
|
636
|
+
if (!user || (userRole !== 'admin' && userRole !== 'super_admin')) {
|
|
637
|
+
return ctx.forbidden('Admin access required.');
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const methods = await strapi.db.query('plugin::webbycommerce.shipping-method').findMany({
|
|
641
|
+
orderBy: { sort_order: 'asc' },
|
|
642
|
+
populate: ['shippingZone', 'shippingRates'],
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
ctx.send({ data: methods });
|
|
646
|
+
} catch (error) {
|
|
647
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in getShippingMethodsAdmin:`, error);
|
|
648
|
+
ctx.internalServerError('Failed to fetch shipping methods. Please try again.');
|
|
649
|
+
}
|
|
650
|
+
},
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Create shipping method (admin only)
|
|
654
|
+
*/
|
|
655
|
+
async createShippingMethod(ctx) {
|
|
656
|
+
try {
|
|
657
|
+
const user = ctx.state.user;
|
|
658
|
+
const userRole = user?.role?.type;
|
|
659
|
+
|
|
660
|
+
if (!user || (userRole !== 'admin' && userRole !== 'super_admin')) {
|
|
661
|
+
return ctx.forbidden('Admin access required.');
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const {
|
|
665
|
+
name,
|
|
666
|
+
description,
|
|
667
|
+
carrier,
|
|
668
|
+
service_type,
|
|
669
|
+
carrier_service_code,
|
|
670
|
+
transit_time,
|
|
671
|
+
is_active,
|
|
672
|
+
is_free_shipping,
|
|
673
|
+
free_shipping_threshold,
|
|
674
|
+
handling_fee,
|
|
675
|
+
zone,
|
|
676
|
+
sort_order,
|
|
677
|
+
} = ctx.request.body;
|
|
678
|
+
|
|
679
|
+
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
|
680
|
+
return ctx.badRequest('Method name is required.');
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (!carrier || typeof carrier !== 'string' || carrier.trim().length === 0) {
|
|
684
|
+
return ctx.badRequest('Carrier is required.');
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (!service_type || typeof service_type !== 'string' || service_type.trim().length === 0) {
|
|
688
|
+
return ctx.badRequest('Service type is required.');
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (!zone || !zone.id) {
|
|
692
|
+
return ctx.badRequest('Shipping zone is required.');
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Verify zone exists
|
|
696
|
+
const existingZone = await strapi.db.query('plugin::webbycommerce.shipping-zone').findOne({
|
|
697
|
+
where: { id: zone.id },
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
if (!existingZone) {
|
|
701
|
+
return ctx.notFound('Shipping zone not found.');
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const method = await strapi.db.query('plugin::webbycommerce.shipping-method').create({
|
|
705
|
+
data: {
|
|
706
|
+
name: name.trim(),
|
|
707
|
+
description: description ? description.trim() : null,
|
|
708
|
+
carrier: carrier.trim(),
|
|
709
|
+
service_type: service_type.trim(),
|
|
710
|
+
carrier_service_code: carrier_service_code ? carrier_service_code.trim() : null,
|
|
711
|
+
transit_time: transit_time ? transit_time.trim() : null,
|
|
712
|
+
is_active: is_active !== undefined ? Boolean(is_active) : true,
|
|
713
|
+
is_free_shipping: Boolean(is_free_shipping),
|
|
714
|
+
free_shipping_threshold: free_shipping_threshold ? parseFloat(free_shipping_threshold) : null,
|
|
715
|
+
handling_fee: handling_fee !== undefined ? parseFloat(handling_fee) : 0,
|
|
716
|
+
shippingZone: zone.id,
|
|
717
|
+
sort_order: sort_order !== undefined ? parseInt(sort_order, 10) : 0,
|
|
718
|
+
},
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
ctx.send({ data: method });
|
|
722
|
+
} catch (error) {
|
|
723
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in createShippingMethod:`, error);
|
|
724
|
+
ctx.internalServerError('Failed to create shipping method. Please try again.');
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Update shipping method (admin only)
|
|
730
|
+
*/
|
|
731
|
+
async updateShippingMethod(ctx) {
|
|
732
|
+
try {
|
|
733
|
+
const user = ctx.state.user;
|
|
734
|
+
const userRole = user?.role?.type;
|
|
735
|
+
|
|
736
|
+
if (!user || (userRole !== 'admin' && userRole !== 'super_admin')) {
|
|
737
|
+
return ctx.forbidden('Admin access required.');
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const { id } = ctx.params;
|
|
741
|
+
const {
|
|
742
|
+
name,
|
|
743
|
+
description,
|
|
744
|
+
carrier,
|
|
745
|
+
service_type,
|
|
746
|
+
carrier_service_code,
|
|
747
|
+
transit_time,
|
|
748
|
+
is_active,
|
|
749
|
+
is_free_shipping,
|
|
750
|
+
free_shipping_threshold,
|
|
751
|
+
handling_fee,
|
|
752
|
+
zone,
|
|
753
|
+
sort_order,
|
|
754
|
+
} = ctx.request.body;
|
|
755
|
+
|
|
756
|
+
const existingMethod = await strapi.db.query('plugin::webbycommerce.shipping-method').findOne({
|
|
757
|
+
where: { id },
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
if (!existingMethod) {
|
|
761
|
+
return ctx.notFound('Shipping method not found.');
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Verify zone exists if provided
|
|
765
|
+
if (zone && zone.id) {
|
|
766
|
+
const existingZone = await strapi.db.query('plugin::webbycommerce.shipping-zone').findOne({
|
|
767
|
+
where: { id: zone.id },
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
if (!existingZone) {
|
|
771
|
+
return ctx.notFound('Shipping zone not found.');
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const updateData = {};
|
|
776
|
+
if (name !== undefined) updateData.name = name.trim();
|
|
777
|
+
if (description !== undefined) updateData.description = description ? description.trim() : null;
|
|
778
|
+
if (carrier !== undefined) updateData.carrier = carrier.trim();
|
|
779
|
+
if (service_type !== undefined) updateData.service_type = service_type.trim();
|
|
780
|
+
if (carrier_service_code !== undefined) updateData.carrier_service_code = carrier_service_code ? carrier_service_code.trim() : null;
|
|
781
|
+
if (transit_time !== undefined) updateData.transit_time = transit_time ? transit_time.trim() : null;
|
|
782
|
+
if (is_active !== undefined) updateData.is_active = Boolean(is_active);
|
|
783
|
+
if (is_free_shipping !== undefined) updateData.is_free_shipping = Boolean(is_free_shipping);
|
|
784
|
+
if (free_shipping_threshold !== undefined) updateData.free_shipping_threshold = free_shipping_threshold ? parseFloat(free_shipping_threshold) : null;
|
|
785
|
+
if (handling_fee !== undefined) updateData.handling_fee = parseFloat(handling_fee);
|
|
786
|
+
if (zone !== undefined && zone.id) updateData.shippingZone = zone.id;
|
|
787
|
+
if (sort_order !== undefined) updateData.sort_order = parseInt(sort_order, 10);
|
|
788
|
+
|
|
789
|
+
const updatedMethod = await strapi.db.query('plugin::webbycommerce.shipping-method').update({
|
|
790
|
+
where: { id },
|
|
791
|
+
data: updateData,
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
ctx.send({ data: updatedMethod });
|
|
795
|
+
} catch (error) {
|
|
796
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in updateShippingMethod:`, error);
|
|
797
|
+
ctx.internalServerError('Failed to update shipping method. Please try again.');
|
|
798
|
+
}
|
|
799
|
+
},
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Delete shipping method (admin only)
|
|
803
|
+
*/
|
|
804
|
+
async deleteShippingMethod(ctx) {
|
|
805
|
+
try {
|
|
806
|
+
const user = ctx.state.user;
|
|
807
|
+
const userRole = user?.role?.type;
|
|
808
|
+
|
|
809
|
+
if (!user || (userRole !== 'admin' && userRole !== 'super_admin')) {
|
|
810
|
+
return ctx.forbidden('Admin access required.');
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const { id } = ctx.params;
|
|
814
|
+
|
|
815
|
+
const method = await strapi.db.query('plugin::webbycommerce.shipping-method').findOne({
|
|
816
|
+
where: { id },
|
|
817
|
+
populate: ['shippingRates'],
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
if (!method) {
|
|
821
|
+
return ctx.notFound('Shipping method not found.');
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
if (method.shippingRates && method.shippingRates.length > 0) {
|
|
825
|
+
return ctx.badRequest('Cannot delete method with associated rates. Please remove rates first.');
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
await strapi.db.query('plugin::webbycommerce.shipping-method').delete({
|
|
829
|
+
where: { id },
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
ctx.send({ data: { id } });
|
|
833
|
+
} catch (error) {
|
|
834
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in deleteShippingMethod:`, error);
|
|
835
|
+
ctx.internalServerError('Failed to delete shipping method. Please try again.');
|
|
836
|
+
}
|
|
837
|
+
},
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Get shipping rates for a method (admin only)
|
|
841
|
+
*/
|
|
842
|
+
async getShippingRates(ctx) {
|
|
843
|
+
try {
|
|
844
|
+
const user = ctx.state.user;
|
|
845
|
+
const userRole = user?.role?.type;
|
|
846
|
+
|
|
847
|
+
if (!user || (userRole !== 'admin' && userRole !== 'super_admin')) {
|
|
848
|
+
return ctx.forbidden('Admin access required.');
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const { methodId } = ctx.params;
|
|
852
|
+
|
|
853
|
+
const rates = await strapi.db.query('plugin::webbycommerce.shipping-rate').findMany({
|
|
854
|
+
where: { shippingMethod: methodId },
|
|
855
|
+
orderBy: { sort_order: 'asc', min_value: 'asc' },
|
|
856
|
+
populate: ['shippingMethod'],
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
ctx.send({ data: rates });
|
|
860
|
+
} catch (error) {
|
|
861
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in getShippingRates:`, error);
|
|
862
|
+
ctx.internalServerError('Failed to fetch shipping rates. Please try again.');
|
|
863
|
+
}
|
|
864
|
+
},
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Create shipping rate (admin only)
|
|
868
|
+
*/
|
|
869
|
+
async createShippingRate(ctx) {
|
|
870
|
+
try {
|
|
871
|
+
const user = ctx.state.user;
|
|
872
|
+
const userRole = user?.role?.type;
|
|
873
|
+
|
|
874
|
+
if (!user || (userRole !== 'admin' && userRole !== 'super_admin')) {
|
|
875
|
+
return ctx.forbidden('Admin access required.');
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const {
|
|
879
|
+
name,
|
|
880
|
+
condition_type,
|
|
881
|
+
min_value,
|
|
882
|
+
max_value,
|
|
883
|
+
rate,
|
|
884
|
+
currency,
|
|
885
|
+
method,
|
|
886
|
+
is_active,
|
|
887
|
+
sort_order,
|
|
888
|
+
} = ctx.request.body;
|
|
889
|
+
|
|
890
|
+
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
|
891
|
+
return ctx.badRequest('Rate name is required.');
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const validConditionTypes = ['weight', 'price', 'quantity', 'volume', 'dimension'];
|
|
895
|
+
if (!condition_type || !validConditionTypes.includes(condition_type)) {
|
|
896
|
+
return ctx.badRequest('Valid condition type is required.');
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
if (min_value === undefined || min_value === null) {
|
|
900
|
+
return ctx.badRequest('Minimum value is required.');
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (rate === undefined || rate === null) {
|
|
904
|
+
return ctx.badRequest('Rate is required.');
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
if (!method || !method.id) {
|
|
908
|
+
return ctx.badRequest('Shipping method is required.');
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Verify method exists
|
|
912
|
+
const existingMethod = await strapi.db.query('plugin::webbycommerce.shipping-method').findOne({
|
|
913
|
+
where: { id: method.id },
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
if (!existingMethod) {
|
|
917
|
+
return ctx.notFound('Shipping method not found.');
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
const shippingRate = await strapi.db.query('plugin::webbycommerce.shipping-rate').create({
|
|
921
|
+
data: {
|
|
922
|
+
name: name.trim(),
|
|
923
|
+
condition_type,
|
|
924
|
+
min_value: parseFloat(min_value),
|
|
925
|
+
max_value: max_value !== undefined ? parseFloat(max_value) : null,
|
|
926
|
+
rate: parseFloat(rate),
|
|
927
|
+
currency: currency || 'USD',
|
|
928
|
+
shippingMethod: method.id,
|
|
929
|
+
is_active: is_active !== undefined ? Boolean(is_active) : true,
|
|
930
|
+
sort_order: sort_order !== undefined ? parseInt(sort_order, 10) : 0,
|
|
931
|
+
},
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
ctx.send({ data: shippingRate });
|
|
935
|
+
} catch (error) {
|
|
936
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in createShippingRate:`, error);
|
|
937
|
+
ctx.internalServerError('Failed to create shipping rate. Please try again.');
|
|
938
|
+
}
|
|
939
|
+
},
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Update shipping rate (admin only)
|
|
943
|
+
*/
|
|
944
|
+
async updateShippingRate(ctx) {
|
|
945
|
+
try {
|
|
946
|
+
const user = ctx.state.user;
|
|
947
|
+
const userRole = user?.role?.type;
|
|
948
|
+
|
|
949
|
+
if (!user || (userRole !== 'admin' && userRole !== 'super_admin')) {
|
|
950
|
+
return ctx.forbidden('Admin access required.');
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const { id } = ctx.params;
|
|
954
|
+
const {
|
|
955
|
+
name,
|
|
956
|
+
condition_type,
|
|
957
|
+
min_value,
|
|
958
|
+
max_value,
|
|
959
|
+
rate,
|
|
960
|
+
currency,
|
|
961
|
+
method,
|
|
962
|
+
is_active,
|
|
963
|
+
sort_order,
|
|
964
|
+
} = ctx.request.body;
|
|
965
|
+
|
|
966
|
+
const existingRate = await strapi.db.query('plugin::webbycommerce.shipping-rate').findOne({
|
|
967
|
+
where: { id },
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
if (!existingRate) {
|
|
971
|
+
return ctx.notFound('Shipping rate not found.');
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Verify method exists if provided
|
|
975
|
+
if (method && method.id) {
|
|
976
|
+
const existingMethod = await strapi.db.query('plugin::webbycommerce.shipping-method').findOne({
|
|
977
|
+
where: { id: method.id },
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
if (!existingMethod) {
|
|
981
|
+
return ctx.notFound('Shipping method not found.');
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const updateData = {};
|
|
986
|
+
if (name !== undefined) updateData.name = name.trim();
|
|
987
|
+
if (condition_type !== undefined) {
|
|
988
|
+
const validConditionTypes = ['weight', 'price', 'quantity', 'volume', 'dimension'];
|
|
989
|
+
if (!validConditionTypes.includes(condition_type)) {
|
|
990
|
+
return ctx.badRequest('Invalid condition type.');
|
|
991
|
+
}
|
|
992
|
+
updateData.condition_type = condition_type;
|
|
993
|
+
}
|
|
994
|
+
if (min_value !== undefined) updateData.min_value = parseFloat(min_value);
|
|
995
|
+
if (max_value !== undefined) updateData.max_value = max_value ? parseFloat(max_value) : null;
|
|
996
|
+
if (rate !== undefined) updateData.rate = parseFloat(rate);
|
|
997
|
+
if (currency !== undefined) updateData.currency = currency;
|
|
998
|
+
if (method !== undefined && method.id) updateData.shippingMethod = method.id;
|
|
999
|
+
if (is_active !== undefined) updateData.is_active = Boolean(is_active);
|
|
1000
|
+
if (sort_order !== undefined) updateData.sort_order = parseInt(sort_order, 10);
|
|
1001
|
+
|
|
1002
|
+
const updatedRate = await strapi.db.query('plugin::webbycommerce.shipping-rate').update({
|
|
1003
|
+
where: { id },
|
|
1004
|
+
data: updateData,
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
ctx.send({ data: updatedRate });
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in updateShippingRate:`, error);
|
|
1010
|
+
ctx.internalServerError('Failed to update shipping rate. Please try again.');
|
|
1011
|
+
}
|
|
1012
|
+
},
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Delete shipping rate (admin only)
|
|
1016
|
+
*/
|
|
1017
|
+
async deleteShippingRate(ctx) {
|
|
1018
|
+
try {
|
|
1019
|
+
const user = ctx.state.user;
|
|
1020
|
+
const userRole = user?.role?.type;
|
|
1021
|
+
|
|
1022
|
+
if (!user || (userRole !== 'admin' && userRole !== 'super_admin')) {
|
|
1023
|
+
return ctx.forbidden('Admin access required.');
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
const { id } = ctx.params;
|
|
1027
|
+
|
|
1028
|
+
const rate = await strapi.db.query('plugin::webbycommerce.shipping-rate').findOne({
|
|
1029
|
+
where: { id },
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
if (!rate) {
|
|
1033
|
+
return ctx.notFound('Shipping rate not found.');
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
await strapi.db.query('plugin::webbycommerce.shipping-rate').delete({
|
|
1037
|
+
where: { id },
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
ctx.send({ data: { id } });
|
|
1041
|
+
} catch (error) {
|
|
1042
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in deleteShippingRate:`, error);
|
|
1043
|
+
ctx.internalServerError('Failed to delete shipping rate. Please try again.');
|
|
1044
|
+
}
|
|
1045
|
+
},
|
|
1046
|
+
};
|