@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,73 @@
1
+ {
2
+ "kind": "collectionType",
3
+ "collectionName": "shipping_rates",
4
+ "info": {
5
+ "singularName": "shipping-rate",
6
+ "pluralName": "shipping-rates",
7
+ "displayName": "Shipping Rate",
8
+ "description": "Shipping rates based on weight, price, or quantity"
9
+ },
10
+ "options": {
11
+ "draftAndPublish": false
12
+ },
13
+ "pluginOptions": {
14
+ "content-manager": {
15
+ "visible": true
16
+ },
17
+ "content-api": {
18
+ "visible": true
19
+ }
20
+ },
21
+ "attributes": {
22
+ "name": {
23
+ "type": "string",
24
+ "required": true,
25
+ "description": "Rate name for admin reference"
26
+ },
27
+ "condition_type": {
28
+ "type": "enumeration",
29
+ "enum": ["weight", "price", "quantity", "volume", "dimension"],
30
+ "required": true,
31
+ "description": "What the rate is based on"
32
+ },
33
+ "min_value": {
34
+ "type": "decimal",
35
+ "required": true,
36
+ "min": 0,
37
+ "description": "Minimum value for this rate tier"
38
+ },
39
+ "max_value": {
40
+ "type": "decimal",
41
+ "required": false,
42
+ "min": 0,
43
+ "description": "Maximum value for this rate tier (null for unlimited)"
44
+ },
45
+ "rate": {
46
+ "type": "decimal",
47
+ "required": true,
48
+ "min": 0,
49
+ "description": "Shipping cost for this tier"
50
+ },
51
+ "currency": {
52
+ "type": "string",
53
+ "required": true,
54
+ "default": "USD"
55
+ },
56
+ "shippingMethod": {
57
+ "type": "relation",
58
+ "relation": "manyToOne",
59
+ "target": "plugin::webbycommerce.shipping-method",
60
+ "required": false
61
+ },
62
+ "is_active": {
63
+ "type": "boolean",
64
+ "default": true,
65
+ "required": true
66
+ },
67
+ "sort_order": {
68
+ "type": "integer",
69
+ "default": 0,
70
+ "required": false
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ const schema = require('./schema.json');
4
+
5
+ module.exports = {
6
+ schema,
7
+ };
@@ -0,0 +1,84 @@
1
+ {
2
+ "kind": "collectionType",
3
+ "collectionName": "shipping_rules",
4
+ "info": {
5
+ "singularName": "shipping-rule",
6
+ "pluralName": "shipping-rules",
7
+ "displayName": "Shipping Rule",
8
+ "description": "Rules and conditions for shipping eligibility"
9
+ },
10
+ "options": {
11
+ "draftAndPublish": false
12
+ },
13
+ "pluginOptions": {
14
+ "content-manager": {
15
+ "visible": true
16
+ },
17
+ "content-api": {
18
+ "visible": true
19
+ }
20
+ },
21
+ "attributes": {
22
+ "name": {
23
+ "type": "string",
24
+ "required": true
25
+ },
26
+ "description": {
27
+ "type": "text",
28
+ "required": false
29
+ },
30
+ "rule_type": {
31
+ "type": "enumeration",
32
+ "enum": ["restriction", "surcharge", "discount", "requirement"],
33
+ "required": true
34
+ },
35
+ "condition_type": {
36
+ "type": "enumeration",
37
+ "enum": ["product_category", "product_tag", "product_weight", "order_total", "customer_group", "shipping_address", "cart_quantity"],
38
+ "required": true
39
+ },
40
+ "condition_operator": {
41
+ "type": "enumeration",
42
+ "enum": ["equals", "not_equals", "greater_than", "less_than", "contains", "not_contains", "in", "not_in"],
43
+ "required": true
44
+ },
45
+ "condition_value": {
46
+ "type": "json",
47
+ "required": true,
48
+ "description": "Value(s) to compare against (string, number, or array)"
49
+ },
50
+ "action_type": {
51
+ "type": "enumeration",
52
+ "enum": ["hide_method", "add_fee", "subtract_fee", "set_rate", "multiply_rate"],
53
+ "required": true
54
+ },
55
+ "action_value": {
56
+ "type": "decimal",
57
+ "required": false,
58
+ "description": "Value for the action (fee amount, multiplier, etc.)"
59
+ },
60
+ "action_message": {
61
+ "type": "string",
62
+ "required": false,
63
+ "description": "Message to display when rule applies"
64
+ },
65
+ "is_active": {
66
+ "type": "boolean",
67
+ "default": true,
68
+ "required": true
69
+ },
70
+ "priority": {
71
+ "type": "integer",
72
+ "default": 0,
73
+ "required": false,
74
+ "description": "Higher priority rules are evaluated first"
75
+ },
76
+ "applies_to_methods": {
77
+ "type": "relation",
78
+ "relation": "manyToMany",
79
+ "target": "plugin::webbycommerce.shipping-method",
80
+ "required": false,
81
+ "description": "Shipping methods this rule applies to (empty/null for all)"
82
+ }
83
+ }
84
+ }
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ const schema = require('./schema.json');
4
+
5
+ module.exports = {
6
+ schema,
7
+ };
@@ -0,0 +1,57 @@
1
+ {
2
+ "kind": "collectionType",
3
+ "collectionName": "shipping_zones",
4
+ "info": {
5
+ "singularName": "shipping-zone",
6
+ "pluralName": "shipping-zones",
7
+ "displayName": "Shipping Zone",
8
+ "description": "Geographical shipping zones for rate calculation"
9
+ },
10
+ "options": {
11
+ "draftAndPublish": false
12
+ },
13
+ "pluginOptions": {
14
+ "content-manager": {
15
+ "visible": true
16
+ },
17
+ "content-api": {
18
+ "visible": true
19
+ }
20
+ },
21
+ "attributes": {
22
+ "name": {
23
+ "type": "string",
24
+ "required": true,
25
+ "unique": true
26
+ },
27
+ "description": {
28
+ "type": "text",
29
+ "required": false
30
+ },
31
+ "location": {
32
+ "type": "component",
33
+ "repeatable": true,
34
+ "component": "plugin::webbycommerce.shipping-zone-location",
35
+ "required": false,
36
+ "description": "Location rules for matching shipping addresses"
37
+ },
38
+ "is_active": {
39
+ "type": "boolean",
40
+ "default": true,
41
+ "required": true
42
+ },
43
+ "sort_order": {
44
+ "type": "integer",
45
+ "default": 0,
46
+ "required": false
47
+ },
48
+ "shippingMethods": {
49
+ "type": "relation",
50
+ "relation": "oneToMany",
51
+ "target": "plugin::webbycommerce.shipping-method",
52
+ "required": false
53
+
54
+
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * wishlist.js
5
+ *
6
+ * Wishlist content type lifecycle
7
+ */
8
+
9
+ const schema = {
10
+ kind: 'collectionType',
11
+ collectionName: 'wishlists',
12
+ info: {
13
+ singularName: 'wishlist',
14
+ pluralName: 'wishlists',
15
+ displayName: 'Wishlist',
16
+ description: 'User wishlist for storing favorite products',
17
+ },
18
+ options: {
19
+ draftAndPublish: false,
20
+ timestamps: true,
21
+ },
22
+ pluginOptions: {
23
+ 'content-manager': {
24
+ visible: true,
25
+ },
26
+ 'content-type-builder': {
27
+ visible: true,
28
+ },
29
+ },
30
+ attributes: {
31
+ userId: {
32
+ type: 'string',
33
+ required: true,
34
+ configurable: false,
35
+ },
36
+ userEmail: {
37
+ type: 'email',
38
+ required: true,
39
+ configurable: false,
40
+ },
41
+ products: {
42
+ type: 'relation',
43
+ relation: 'manyToMany',
44
+ target: 'plugin::webbycommerce.product',
45
+ mappedBy: 'wishlists',
46
+ },
47
+ isPublic: {
48
+ type: 'boolean',
49
+ default: false,
50
+ required: false,
51
+ },
52
+ name: {
53
+ type: 'string',
54
+ maxLength: 100,
55
+ required: false,
56
+ },
57
+ description: {
58
+ type: 'text',
59
+ required: false,
60
+ },
61
+ },
62
+ };
63
+
64
+ module.exports = {
65
+ schema,
66
+ };
@@ -0,0 +1,374 @@
1
+ 'use strict';
2
+
3
+ const PLUGIN_ID = 'webbycommerce';
4
+ const { ensureEcommercePermission } = require('../utils/check-ecommerce-permission');
5
+
6
+ const getStore = () => {
7
+ return strapi.store({ type: 'plugin', name: PLUGIN_ID });
8
+ };
9
+
10
+ const getShippingType = async () => {
11
+ const store = getStore();
12
+ const value = (await store.get({ key: 'settings' })) || {};
13
+ return value.shippingType || 'single'; // 'single' or 'multiple'
14
+ };
15
+
16
+ module.exports = {
17
+ /**
18
+ * Get all addresses for authenticated user
19
+ */
20
+ async getAddresses(ctx) {
21
+ try {
22
+ // Check ecommerce permission
23
+ const hasPermission = await ensureEcommercePermission(ctx);
24
+ if (!hasPermission) {
25
+ return;
26
+ }
27
+
28
+ const user = ctx.state.user;
29
+ if (!user) {
30
+ return ctx.unauthorized('Authentication required. Please provide a valid JWT token.');
31
+ }
32
+
33
+ const { type } = ctx.query; // Optional filter: type=0 (billing) or type=1 (shipping)
34
+
35
+ const where = { user: user.id };
36
+ if (type !== undefined) {
37
+ where.type = parseInt(type, 10);
38
+ }
39
+
40
+ const addresses = await strapi.db.query('plugin::webbycommerce.address').findMany({
41
+ where,
42
+ orderBy: { createdAt: 'desc' },
43
+ });
44
+
45
+ ctx.send({ data: addresses });
46
+ } catch (error) {
47
+ strapi.log.error(`[${PLUGIN_ID}] Error in getAddresses:`, error);
48
+ ctx.internalServerError('Failed to fetch addresses. Please try again.');
49
+ }
50
+ },
51
+
52
+ /**
53
+ * Get single address by ID
54
+ */
55
+ async getAddress(ctx) {
56
+ try {
57
+ const hasPermission = await ensureEcommercePermission(ctx);
58
+ if (!hasPermission) {
59
+ return;
60
+ }
61
+
62
+ const user = ctx.state.user;
63
+ if (!user) {
64
+ return ctx.unauthorized('Authentication required. Please provide a valid JWT token.');
65
+ }
66
+
67
+ const { id } = ctx.params;
68
+
69
+ const address = await strapi.db.query('plugin::webbycommerce.address').findOne({
70
+ where: { id, user: user.id },
71
+ });
72
+
73
+ if (!address) {
74
+ return ctx.notFound('Address not found.');
75
+ }
76
+
77
+ ctx.send({ data: address });
78
+ } catch (error) {
79
+ strapi.log.error(`[${PLUGIN_ID}] Error in getAddress:`, error);
80
+ ctx.internalServerError('Failed to fetch address. Please try again.');
81
+ }
82
+ },
83
+
84
+ /**
85
+ * Create new address
86
+ */
87
+ async createAddress(ctx) {
88
+ try {
89
+ const hasPermission = await ensureEcommercePermission(ctx);
90
+ if (!hasPermission) {
91
+ return;
92
+ }
93
+
94
+ const user = ctx.state.user;
95
+ if (!user) {
96
+ return ctx.unauthorized('Authentication required. Please provide a valid JWT token.');
97
+ }
98
+
99
+ const shippingType = await getShippingType();
100
+ const {
101
+ type,
102
+ first_name,
103
+ last_name,
104
+ company_name,
105
+ country,
106
+ region,
107
+ city,
108
+ street_address,
109
+ postcode,
110
+ phone,
111
+ email_address,
112
+ } = ctx.request.body || {};
113
+
114
+ // Validate required fields - convert type to integer for validation
115
+ // Check if type exists in request body
116
+ if (type === undefined || type === null) {
117
+ return ctx.badRequest('Type is required and must be 0 (billing) or 1 (shipping).');
118
+ }
119
+
120
+ // Convert to number - handle both string and number inputs
121
+ // Accept: 0, 1, "0", "1", 0.0, 1.0
122
+ let typeInt;
123
+ if (type === 0 || type === '0' || type === 0.0) {
124
+ typeInt = 0;
125
+ } else if (type === 1 || type === '1' || type === 1.0) {
126
+ typeInt = 1;
127
+ } else {
128
+ // Try parsing as number
129
+ typeInt = Number(type);
130
+ if (isNaN(typeInt) || (typeInt !== 0 && typeInt !== 1)) {
131
+ return ctx.badRequest('Type is required and must be 0 (billing) or 1 (shipping).');
132
+ }
133
+ }
134
+
135
+ if (!first_name || typeof first_name !== 'string' || first_name.trim().length === 0) {
136
+ return ctx.badRequest('First name is required.');
137
+ }
138
+
139
+ if (!last_name || typeof last_name !== 'string' || last_name.trim().length === 0) {
140
+ return ctx.badRequest('Last name is required.');
141
+ }
142
+
143
+ if (!country || typeof country !== 'string' || country.trim().length === 0) {
144
+ return ctx.badRequest('Country is required.');
145
+ }
146
+
147
+ if (!city || typeof city !== 'string' || city.trim().length === 0) {
148
+ return ctx.badRequest('City is required.');
149
+ }
150
+
151
+ if (!street_address || typeof street_address !== 'string' || street_address.trim().length === 0) {
152
+ return ctx.badRequest('Street address is required.');
153
+ }
154
+
155
+ if (!postcode || typeof postcode !== 'string' || postcode.trim().length === 0) {
156
+ return ctx.badRequest('Postcode is required.');
157
+ }
158
+
159
+ if (!phone || typeof phone !== 'string' || phone.trim().length === 0) {
160
+ return ctx.badRequest('Phone is required.');
161
+ }
162
+
163
+ // Email is required for billing addresses
164
+ if (typeInt === 0 && (!email_address || typeof email_address !== 'string' || email_address.trim().length === 0)) {
165
+ return ctx.badRequest('Email address is required for billing addresses.');
166
+ }
167
+
168
+ // Validate email format if provided
169
+ if (email_address) {
170
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
171
+ if (!emailRegex.test(email_address.trim())) {
172
+ return ctx.badRequest('Invalid email format.');
173
+ }
174
+ }
175
+
176
+ // Check if single mode and address already exists
177
+ if (shippingType === 'single') {
178
+ const existingAddress = await strapi.db.query('plugin::webbycommerce.address').findOne({
179
+ where: { user: user.id, type: typeInt },
180
+ });
181
+
182
+ if (existingAddress) {
183
+ return ctx.forbidden(
184
+ `A ${typeInt === 0 ? 'billing' : 'shipping'} address already exists. Please update the existing address or enable multiple address mode in settings.`
185
+ );
186
+ }
187
+ }
188
+
189
+ // Create address
190
+ const address = await strapi.db.query('plugin::webbycommerce.address').create({
191
+ data: {
192
+ type: typeInt,
193
+ first_name: first_name.trim(),
194
+ last_name: last_name.trim(),
195
+ company_name: company_name ? company_name.trim() : null,
196
+ country: country.trim(),
197
+ region: region ? region.trim() : null,
198
+ city: city.trim(),
199
+ street_address: street_address.trim(),
200
+ postcode: postcode.trim(),
201
+ phone: phone.trim(),
202
+ email_address: email_address ? email_address.trim().toLowerCase() : null,
203
+ user: user.id,
204
+ },
205
+ });
206
+
207
+ ctx.send({ data: address });
208
+ } catch (error) {
209
+ strapi.log.error(`[${PLUGIN_ID}] Error in createAddress:`, error);
210
+ ctx.internalServerError('Failed to create address. Please try again.');
211
+ }
212
+ },
213
+
214
+ /**
215
+ * Update address
216
+ */
217
+ async updateAddress(ctx) {
218
+ try {
219
+ const hasPermission = await ensureEcommercePermission(ctx);
220
+ if (!hasPermission) {
221
+ return;
222
+ }
223
+
224
+ const user = ctx.state.user;
225
+ if (!user) {
226
+ return ctx.unauthorized('Authentication required. Please provide a valid JWT token.');
227
+ }
228
+
229
+ const { id } = ctx.params;
230
+ const {
231
+ type,
232
+ first_name,
233
+ last_name,
234
+ company_name,
235
+ country,
236
+ region,
237
+ city,
238
+ street_address,
239
+ postcode,
240
+ phone,
241
+ email_address,
242
+ } = ctx.request.body;
243
+
244
+ // Verify address belongs to user
245
+ const existingAddress = await strapi.db.query('plugin::webbycommerce.address').findOne({
246
+ where: { id, user: user.id },
247
+ });
248
+
249
+ if (!existingAddress) {
250
+ return ctx.notFound('Address not found.');
251
+ }
252
+
253
+ // Validate type if provided
254
+ if (type !== undefined) {
255
+ const typeInt = parseInt(type, 10);
256
+ if (isNaN(typeInt) || (typeInt !== 0 && typeInt !== 1)) {
257
+ return ctx.badRequest('Type must be 0 (billing) or 1 (shipping).');
258
+ }
259
+ }
260
+
261
+ // Validate required fields if provided
262
+ if (first_name !== undefined && (!first_name || typeof first_name !== 'string' || first_name.trim().length === 0)) {
263
+ return ctx.badRequest('First name is required.');
264
+ }
265
+
266
+ if (last_name !== undefined && (!last_name || typeof last_name !== 'string' || last_name.trim().length === 0)) {
267
+ return ctx.badRequest('Last name is required.');
268
+ }
269
+
270
+ if (country !== undefined && (!country || typeof country !== 'string' || country.trim().length === 0)) {
271
+ return ctx.badRequest('Country is required.');
272
+ }
273
+
274
+ if (city !== undefined && (!city || typeof city !== 'string' || city.trim().length === 0)) {
275
+ return ctx.badRequest('City is required.');
276
+ }
277
+
278
+ if (street_address !== undefined && (!street_address || typeof street_address !== 'string' || street_address.trim().length === 0)) {
279
+ return ctx.badRequest('Street address is required.');
280
+ }
281
+
282
+ if (postcode !== undefined && (!postcode || typeof postcode !== 'string' || postcode.trim().length === 0)) {
283
+ return ctx.badRequest('Postcode is required.');
284
+ }
285
+
286
+ if (phone !== undefined && (!phone || typeof phone !== 'string' || phone.trim().length === 0)) {
287
+ return ctx.badRequest('Phone is required.');
288
+ }
289
+
290
+ // Validate email if provided
291
+ if (email_address !== undefined) {
292
+ const addressType = type !== undefined ? parseInt(type, 10) : existingAddress.type;
293
+ if (addressType === 0 && (!email_address || typeof email_address !== 'string' || email_address.trim().length === 0)) {
294
+ return ctx.badRequest('Email address is required for billing addresses.');
295
+ }
296
+
297
+ if (email_address) {
298
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
299
+ if (!emailRegex.test(email_address.trim())) {
300
+ return ctx.badRequest('Invalid email format.');
301
+ }
302
+ }
303
+ }
304
+
305
+ // Prepare update data
306
+ const updateData = {};
307
+ if (type !== undefined) {
308
+ const typeInt = parseInt(type, 10);
309
+ if (!isNaN(typeInt) && (typeInt === 0 || typeInt === 1)) {
310
+ updateData.type = typeInt;
311
+ }
312
+ }
313
+ if (first_name !== undefined) updateData.first_name = first_name.trim();
314
+ if (last_name !== undefined) updateData.last_name = last_name.trim();
315
+ if (company_name !== undefined) updateData.company_name = company_name ? company_name.trim() : null;
316
+ if (country !== undefined) updateData.country = country.trim();
317
+ if (region !== undefined) updateData.region = region ? region.trim() : null;
318
+ if (city !== undefined) updateData.city = city.trim();
319
+ if (street_address !== undefined) updateData.street_address = street_address.trim();
320
+ if (postcode !== undefined) updateData.postcode = postcode.trim();
321
+ if (phone !== undefined) updateData.phone = phone.trim();
322
+ if (email_address !== undefined) updateData.email_address = email_address ? email_address.trim().toLowerCase() : null;
323
+
324
+ // Update address
325
+ const updatedAddress = await strapi.db.query('plugin::webbycommerce.address').update({
326
+ where: { id },
327
+ data: updateData,
328
+ });
329
+
330
+ ctx.send({ data: updatedAddress });
331
+ } catch (error) {
332
+ strapi.log.error(`[${PLUGIN_ID}] Error in updateAddress:`, error);
333
+ ctx.internalServerError('Failed to update address. Please try again.');
334
+ }
335
+ },
336
+
337
+ /**
338
+ * Delete address
339
+ */
340
+ async deleteAddress(ctx) {
341
+ try {
342
+ const hasPermission = await ensureEcommercePermission(ctx);
343
+ if (!hasPermission) {
344
+ return;
345
+ }
346
+
347
+ const user = ctx.state.user;
348
+ if (!user) {
349
+ return ctx.unauthorized('Authentication required. Please provide a valid JWT token.');
350
+ }
351
+
352
+ const { id } = ctx.params;
353
+
354
+ // Verify address belongs to user
355
+ const address = await strapi.db.query('plugin::webbycommerce.address').findOne({
356
+ where: { id, user: user.id },
357
+ });
358
+
359
+ if (!address) {
360
+ return ctx.notFound('Address not found.');
361
+ }
362
+
363
+ await strapi.db.query('plugin::webbycommerce.address').delete({
364
+ where: { id },
365
+ });
366
+
367
+ ctx.send({ data: { id } });
368
+ } catch (error) {
369
+ strapi.log.error(`[${PLUGIN_ID}] Error in deleteAddress:`, error);
370
+ ctx.internalServerError('Failed to delete address. Please try again.');
371
+ }
372
+ },
373
+ };
374
+