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