@webbycrown/webbycommerce 1.2.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/README.md +21 -3
  2. package/admin/app.js +3 -0
  3. package/admin/jsconfig.json +20 -0
  4. package/admin/src/components/ApiCollectionsContent.jsx +4626 -0
  5. package/admin/src/components/CompareContent.jsx +300 -0
  6. package/admin/src/components/ConfigureContent.jsx +407 -0
  7. package/admin/src/components/Initializer.jsx +64 -0
  8. package/admin/src/components/LoginRegisterContent.jsx +280 -0
  9. package/admin/src/components/PluginIcon.jsx +6 -0
  10. package/admin/src/components/ShippingTypeContent.jsx +230 -0
  11. package/admin/src/components/SmtpContent.jsx +316 -0
  12. package/admin/src/components/WishlistContent.jsx +273 -0
  13. package/admin/src/index.js +81 -0
  14. package/admin/src/pages/ApiCollections.jsx +169 -0
  15. package/admin/src/pages/Configure.jsx +55 -0
  16. package/admin/src/pages/Settings.jsx +93 -0
  17. package/admin/src/pluginId.js +4 -0
  18. package/{dist/_chunks/en-CiQ97iC8.js → admin/src/translations/en.json} +712 -574
  19. package/bin/setup.js +50 -3
  20. package/package.json +14 -13
  21. package/server/bootstrap.js +3 -0
  22. package/server/register.js +3 -0
  23. package/server/src/bootstrap.js +3826 -0
  24. package/server/src/components/content-block.json +37 -0
  25. package/server/src/components/shipping-zone-location.json +27 -0
  26. package/server/src/config/index.js +7 -0
  27. package/server/src/content-types/address/index.js +7 -0
  28. package/server/src/content-types/address/schema.json +74 -0
  29. package/server/src/content-types/cart/index.js +61 -0
  30. package/server/src/content-types/cart-item/index.js +79 -0
  31. package/server/src/content-types/compare.js +73 -0
  32. package/server/src/content-types/coupon/index.js +7 -0
  33. package/server/src/content-types/coupon/schema.json +67 -0
  34. package/server/src/content-types/index.js +42 -0
  35. package/server/src/content-types/order/index.js +7 -0
  36. package/server/src/content-types/order/schema.json +121 -0
  37. package/server/src/content-types/payment-transaction/index.js +7 -0
  38. package/server/src/content-types/payment-transaction/schema.json +73 -0
  39. package/server/src/content-types/product/index.js +7 -0
  40. package/server/src/content-types/product/schema.json +104 -0
  41. package/server/src/content-types/product-attribute/index.js +7 -0
  42. package/server/src/content-types/product-attribute/schema.json +80 -0
  43. package/server/src/content-types/product-attribute-value/index.js +7 -0
  44. package/server/src/content-types/product-attribute-value/schema.json +52 -0
  45. package/server/src/content-types/product-category/index.js +7 -0
  46. package/server/src/content-types/product-category/schema.json +54 -0
  47. package/server/src/content-types/product-tag/index.js +7 -0
  48. package/server/src/content-types/product-tag/schema.json +38 -0
  49. package/server/src/content-types/product-variation/index.js +7 -0
  50. package/server/src/content-types/product-variation/schema.json +74 -0
  51. package/server/src/content-types/shipping-method/index.js +7 -0
  52. package/server/src/content-types/shipping-method/schema.json +91 -0
  53. package/server/src/content-types/shipping-rate/index.js +7 -0
  54. package/server/src/content-types/shipping-rate/schema.json +73 -0
  55. package/server/src/content-types/shipping-rule/index.js +7 -0
  56. package/server/src/content-types/shipping-rule/schema.json +84 -0
  57. package/server/src/content-types/shipping-zone/index.js +7 -0
  58. package/server/src/content-types/shipping-zone/schema.json +57 -0
  59. package/server/src/content-types/wishlist.js +66 -0
  60. package/server/src/controllers/address.js +374 -0
  61. package/server/src/controllers/auth.js +1409 -0
  62. package/server/src/controllers/cart.js +337 -0
  63. package/server/src/controllers/category.js +388 -0
  64. package/server/src/controllers/compare.js +246 -0
  65. package/server/src/controllers/controller.js +168 -0
  66. package/server/src/controllers/ecommerce.js +20 -0
  67. package/server/src/controllers/index.js +34 -0
  68. package/server/src/controllers/order.js +1100 -0
  69. package/server/src/controllers/payment.js +243 -0
  70. package/server/src/controllers/product.js +1006 -0
  71. package/server/src/controllers/productTag.js +370 -0
  72. package/server/src/controllers/productVariation.js +181 -0
  73. package/server/src/controllers/shipping.js +1046 -0
  74. package/server/src/controllers/wishlist.js +332 -0
  75. package/server/src/destroy.js +6 -0
  76. package/server/src/index.js +26 -0
  77. package/server/src/middlewares/index.js +4 -0
  78. package/server/src/policies/index.js +4 -0
  79. package/server/src/register.js +67 -0
  80. package/server/src/routes/index.js +1130 -0
  81. package/server/src/services/cart.js +531 -0
  82. package/server/src/services/compare.js +300 -0
  83. package/server/src/services/index.js +16 -0
  84. package/server/src/services/service.js +19 -0
  85. package/server/src/services/shipping.js +513 -0
  86. package/server/src/services/wishlist.js +238 -0
  87. package/server/src/utils/check-ecommerce-permission.js +204 -0
  88. package/server/src/utils/extend-user-schema.js +161 -0
  89. package/server/src/utils/seed-data.js +639 -0
  90. package/server/src/utils/send-email.js +98 -0
  91. package/strapi-server.js +1 -6
  92. package/dist/_chunks/Settings-Bg2JyQ4c.js +0 -31518
  93. package/dist/_chunks/Settings-BonPzbwr.mjs +0 -31499
  94. package/dist/_chunks/en-DE15m4xZ.mjs +0 -574
  95. package/dist/_chunks/index-BWVy9o1d.mjs +0 -128
  96. package/dist/_chunks/index-NRuOdjd7.js +0 -127
  97. package/dist/admin/index.js +0 -3
  98. package/dist/admin/index.mjs +0 -4
  99. package/dist/robots.txt +0 -3
  100. package/dist/server/index.js +0 -27336
  101. package/dist/uploads/.gitkeep +0 -0
  102. package/dist/uploads/accessories_category_2a5631094b.jpeg +0 -0
  103. package/dist/uploads/beauty_personal_care_category_57f8a8f1e3.jpeg +0 -0
  104. package/dist/uploads/books_category_a9a253eada.jpeg +0 -0
  105. package/dist/uploads/classic_cotton_tshirt_1_cd713425f6.png +0 -0
  106. package/dist/uploads/clothing_category_d5c60ef07b.jpeg +0 -0
  107. package/dist/uploads/daviddoe_strapi_adbcd41787.jpeg +0 -0
  108. package/dist/uploads/electronics_category_fc3e5ef571.jpeg +0 -0
  109. package/dist/uploads/ergonomic_office_chair_1_c751cffb07.png +0 -0
  110. package/dist/uploads/home_garden_category_4f6eb3f8d6.jpeg +0 -0
  111. package/dist/uploads/istockphoto_1188462138_612x612_11f295b9c0.jpg +0 -0
  112. package/dist/uploads/istockphoto_1188462138_612x612_396fb272fd.jpg +0 -0
  113. package/dist/uploads/large_daviddoe_strapi_adbcd41787.jpeg +0 -0
  114. package/dist/uploads/leather_travel_backpack_1_238bc1ae4d.png +0 -0
  115. package/dist/uploads/mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
  116. package/dist/uploads/medium_classic_cotton_tshirt_1_cd713425f6.png +0 -0
  117. package/dist/uploads/medium_daviddoe_strapi_adbcd41787.jpeg +0 -0
  118. package/dist/uploads/medium_ergonomic_office_chair_1_c751cffb07.png +0 -0
  119. package/dist/uploads/medium_leather_travel_backpack_1_238bc1ae4d.png +0 -0
  120. package/dist/uploads/medium_mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
  121. package/dist/uploads/medium_smart_watch_series_5_1_cdc2511fb7.png +0 -0
  122. package/dist/uploads/medium_smartphone_x_pro_1_c3f0cbd080.png +0 -0
  123. package/dist/uploads/medium_the_great_gatsby_special_1_2e7c76d997.png +0 -0
  124. package/dist/uploads/medium_wireless_headphones_1_fa75cd50c3.png +0 -0
  125. package/dist/uploads/medium_yoga_mat_premium_1_01f9a3b5fa.png +0 -0
  126. package/dist/uploads/predictive_maintenance_icons_industry_automation_600nw_2685943461_e18a8aa3b0.webp +0 -0
  127. package/dist/uploads/small_classic_cotton_tshirt_1_cd713425f6.png +0 -0
  128. package/dist/uploads/small_daviddoe_strapi_adbcd41787.jpeg +0 -0
  129. package/dist/uploads/small_ergonomic_office_chair_1_c751cffb07.png +0 -0
  130. package/dist/uploads/small_leather_travel_backpack_1_238bc1ae4d.png +0 -0
  131. package/dist/uploads/small_mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
  132. package/dist/uploads/small_smart_watch_series_5_1_cdc2511fb7.png +0 -0
  133. package/dist/uploads/small_smartphone_x_pro_1_c3f0cbd080.png +0 -0
  134. package/dist/uploads/small_the_great_gatsby_special_1_2e7c76d997.png +0 -0
  135. package/dist/uploads/small_wireless_headphones_1_fa75cd50c3.png +0 -0
  136. package/dist/uploads/small_yoga_mat_premium_1_01f9a3b5fa.png +0 -0
  137. package/dist/uploads/smart_watch_series_5_1_cdc2511fb7.png +0 -0
  138. package/dist/uploads/smartphone_x_pro_1_c3f0cbd080.png +0 -0
  139. package/dist/uploads/the_great_gatsby_special_1_2e7c76d997.png +0 -0
  140. package/dist/uploads/thumbnail_accessories_category_2a5631094b.jpeg +0 -0
  141. package/dist/uploads/thumbnail_beauty_personal_care_category_57f8a8f1e3.jpeg +0 -0
  142. package/dist/uploads/thumbnail_books_category_a9a253eada.jpeg +0 -0
  143. package/dist/uploads/thumbnail_classic_cotton_tshirt_1_cd713425f6.png +0 -0
  144. package/dist/uploads/thumbnail_clothing_category_d5c60ef07b.jpeg +0 -0
  145. package/dist/uploads/thumbnail_daviddoe_strapi_adbcd41787.jpeg +0 -0
  146. package/dist/uploads/thumbnail_electronics_category_fc3e5ef571.jpeg +0 -0
  147. package/dist/uploads/thumbnail_ergonomic_office_chair_1_c751cffb07.png +0 -0
  148. package/dist/uploads/thumbnail_home_garden_category_4f6eb3f8d6.jpeg +0 -0
  149. package/dist/uploads/thumbnail_istockphoto_1188462138_612x612_11f295b9c0.jpg +0 -0
  150. package/dist/uploads/thumbnail_istockphoto_1188462138_612x612_396fb272fd.jpg +0 -0
  151. package/dist/uploads/thumbnail_leather_travel_backpack_1_238bc1ae4d.png +0 -0
  152. package/dist/uploads/thumbnail_mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
  153. package/dist/uploads/thumbnail_predictive_maintenance_icons_industry_automation_600nw_2685943461_e18a8aa3b0.webp +0 -0
  154. package/dist/uploads/thumbnail_smart_watch_series_5_1_cdc2511fb7.png +0 -0
  155. package/dist/uploads/thumbnail_smartphone_x_pro_1_c3f0cbd080.png +0 -0
  156. package/dist/uploads/thumbnail_the_great_gatsby_special_1_2e7c76d997.png +0 -0
  157. package/dist/uploads/thumbnail_wireless_headphones_1_fa75cd50c3.png +0 -0
  158. package/dist/uploads/thumbnail_yoga_mat_premium_1_01f9a3b5fa.png +0 -0
  159. package/dist/uploads/webby-commerce.png +0 -0
  160. package/dist/uploads/wireless_headphones_1_fa75cd50c3.png +0 -0
  161. package/dist/uploads/yoga_mat_premium_1_01f9a3b5fa.png +0 -0
  162. /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
+ };