abmp-npm 1.0.0 → 1.0.105

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 (64) hide show
  1. package/.husky/pre-commit +22 -0
  2. package/.prettierignore +7 -0
  3. package/.prettierrc.json +16 -0
  4. package/README.md +1 -0
  5. package/backend/automations-methods.js +13 -0
  6. package/backend/cms-data-methods.js +249 -0
  7. package/backend/consts.js +45 -0
  8. package/backend/contacts-methods.js +128 -0
  9. package/backend/daily-pull/bulk-process-methods.js +196 -0
  10. package/backend/daily-pull/consts.js +42 -0
  11. package/backend/daily-pull/index.js +5 -0
  12. package/backend/daily-pull/process-member-methods.js +304 -0
  13. package/backend/daily-pull/sync-to-cms-methods.js +124 -0
  14. package/backend/daily-pull/utils.js +124 -0
  15. package/backend/data-hooks.js +29 -0
  16. package/backend/dev-only-methods.js +18 -0
  17. package/backend/elevated-modules.js +18 -0
  18. package/backend/forms-methods.js +52 -0
  19. package/backend/http-functions/httpFunctions.js +86 -0
  20. package/backend/http-functions/index.js +3 -0
  21. package/backend/http-functions/interests.js +37 -0
  22. package/backend/index.js +19 -0
  23. package/backend/jobs.js +42 -0
  24. package/backend/login/index.js +7 -0
  25. package/backend/login/login-methods-factory.js +24 -0
  26. package/backend/login/qa-login-methods.js +72 -0
  27. package/backend/login/sso-methods.js +158 -0
  28. package/backend/members-area-methods.js +89 -0
  29. package/backend/members-data-methods.js +545 -0
  30. package/backend/pac-api-methods.js +34 -0
  31. package/backend/routers/index.js +3 -0
  32. package/backend/routers/methods.js +177 -0
  33. package/backend/routers/utils.js +121 -0
  34. package/backend/search-filters-methods.js +124 -0
  35. package/backend/tasks/consts.js +23 -0
  36. package/backend/tasks/index.js +7 -0
  37. package/backend/tasks/migration-methods.js +46 -0
  38. package/backend/tasks/tasks-configs.js +158 -0
  39. package/backend/tasks/tasks-helpers-methods.js +419 -0
  40. package/backend/tasks/tasks-process-methods.js +545 -0
  41. package/backend/tasks/url-migration-methods.js +378 -0
  42. package/backend/utils.js +221 -0
  43. package/dev-only-scripts/find-duplicate-urls.js +201 -0
  44. package/eslint.config.js +120 -0
  45. package/index.js +5 -5
  46. package/package.json +48 -3
  47. package/pages/ContactUs.js +129 -0
  48. package/pages/Home.js +770 -0
  49. package/pages/LearnMore.js +27 -0
  50. package/pages/LoadingPage.js +20 -0
  51. package/pages/Profile.js +350 -0
  52. package/pages/QAPage.js +39 -0
  53. package/pages/SaveAlerts.js +13 -0
  54. package/pages/SelectBannerImages.js +46 -0
  55. package/pages/deleteConfirm.js +19 -0
  56. package/pages/index.js +12 -0
  57. package/pages/personalDetails.js +2063 -0
  58. package/public/Utils/homePage.js +765 -0
  59. package/public/Utils/personalDetailsUtils.js +25 -0
  60. package/public/Utils/sharedUtils.js +172 -0
  61. package/public/consts.js +112 -0
  62. package/public/index.js +5 -0
  63. package/public/messages.js +16 -0
  64. package/public/sso-auth-methods.js +43 -0
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env sh
2
+ . "$(dirname -- "$0")/_/husky.sh"
3
+
4
+ echo "Running ESLint check..."
5
+ npm run lint
6
+
7
+ if [ $? -ne 0 ]; then
8
+ echo "❌ ESLint check failed. Please fix the linting errors before committing."
9
+ exit 1
10
+ fi
11
+
12
+ echo "Running Prettier check..."
13
+ npm run format:check
14
+
15
+ if [ $? -ne 0 ]; then
16
+ echo "❌ Prettier check failed. Please format your code before committing."
17
+ echo "Run 'npm run format' to automatically format your files."
18
+ exit 1
19
+ fi
20
+
21
+ echo "✅ All checks passed!"
22
+
@@ -0,0 +1,7 @@
1
+ node_modules
2
+ dist
3
+ build
4
+ coverage
5
+ *.min.js
6
+ package-lock.json
7
+
@@ -0,0 +1,16 @@
1
+ {
2
+ "singleQuote": true,
3
+ "trailingComma": "es5",
4
+ "printWidth": 100,
5
+ "tabWidth": 2,
6
+ "semi": true,
7
+ "bracketSpacing": true,
8
+ "bracketSameLine": false,
9
+ "arrowParens": "avoid",
10
+ "endOfLine": "lf",
11
+ "quoteProps": "as-needed",
12
+ "jsxSingleQuote": false,
13
+ "requirePragma": false,
14
+ "insertPragma": false,
15
+ "proseWrap": "preserve"
16
+ }
package/README.md ADDED
@@ -0,0 +1 @@
1
+ # abmp-npm
@@ -0,0 +1,13 @@
1
+ const { customTrigger } = require('@wix/automations');
2
+ const { auth } = require('@wix/essentials');
3
+ const triggerMethod = auth.elevate(customTrigger.runTrigger);
4
+
5
+ const triggerAutomation = async (triggerId, payload) =>
6
+ await triggerMethod({
7
+ triggerId,
8
+ payload,
9
+ });
10
+
11
+ module.exports = {
12
+ triggerAutomation,
13
+ };
@@ -0,0 +1,249 @@
1
+ const geohash = require('ngeohash');
2
+
3
+ const { COLLECTIONS, MEMBERS_FIELDS } = require('../public/consts.js');
4
+ const { findMainAddress } = require('../public/Utils/sharedUtils.js');
5
+ const { calculateDistance, shuffleArray } = require('../public/Utils/sharedUtils.js');
6
+
7
+ const {
8
+ GEO_HASH_PRECISION,
9
+ MAX__MEMBERS_SEARCH_RESULTS,
10
+ WIX_QUERY_MAX_LIMIT,
11
+ MEMBERSHIPS_TYPES,
12
+ } = require('./consts.js');
13
+ const { wixData } = require('./elevated-modules');
14
+
15
+ function buildMembersSearchQuery(data) {
16
+ console.log('data: ', JSON.stringify(data));
17
+ const { filter, isSearchingNearby, includeStudents = false } = data;
18
+ const isUserLocationEnabled = filter.latitude !== 0 || filter.longitude !== 0;
19
+ filter.searchText = filter.searchText || '';
20
+ filter.stateSearch = filter.stateSearch || '';
21
+ filter.practiceAreasSearch = filter.practiceAreasSearch || '';
22
+ filter.practiceAreas = filter.practiceAreas || [];
23
+ filter.state = filter.state || [];
24
+ filter.citySearch = filter.citySearch || '';
25
+ filter.city = filter.city || [];
26
+ filter.latitude = filter.latitude || 0;
27
+ filter.longitude = filter.longitude || 0;
28
+ filter.postalcode = filter.postalcode || '';
29
+ return {
30
+ get: () => {
31
+ let query = wixData
32
+ .query(COLLECTIONS.MEMBERS_DATA)
33
+ .ne('optOut', true)
34
+ .ne('action', 'drop')
35
+ .ne('memberships.membertype', MEMBERSHIPS_TYPES.PAC_STAFF)
36
+ .eq('isVisible', true);
37
+ let filterConfig = [
38
+ {
39
+ filterKey: 'practiceAreas',
40
+ queryMethod: 'hasSome',
41
+ queryField: 'areasOfPractices',
42
+ condition: value => value && value.length > 0,
43
+ fallback: {
44
+ filterKey: 'practiceAreasSearch',
45
+ queryMethod: 'contains',
46
+ queryField: 'areasOfPractices',
47
+ condition: value => value && value.trim() !== '',
48
+ },
49
+ },
50
+ {
51
+ filterKey: 'postalcode',
52
+ queryMethod: 'contains',
53
+ queryField: 'addresses.postalcode',
54
+ condition: value => value && value.trim() !== '',
55
+ },
56
+ {
57
+ filterKey: 'state',
58
+ queryMethod: 'hasSome',
59
+ queryField: 'addresses.state',
60
+ condition: value => value && value.length > 0,
61
+ fallback: {
62
+ filterKey: 'stateSearch',
63
+ queryMethod: 'contains',
64
+ queryField: 'addresses.state',
65
+ condition: value => value && value.trim() !== '',
66
+ },
67
+ },
68
+ {
69
+ filterKey: 'city',
70
+ queryMethod: 'hasSome',
71
+ queryField: 'addresses.city',
72
+ condition: value => value && value.length > 0,
73
+ fallback: {
74
+ filterKey: 'citySearch',
75
+ queryMethod: 'contains',
76
+ queryField: 'addresses.city',
77
+ condition: value => value && value.trim() !== '',
78
+ },
79
+ },
80
+ ];
81
+ //Ignore state, city and postal code when isSearchingNearby is true
82
+ if (isSearchingNearby) {
83
+ filterConfig = filterConfig.filter(
84
+ config => !['state', 'city', 'postalcode'].includes(config.filterKey)
85
+ );
86
+ }
87
+ const applyFilterToQuery = (query, config, filter) => {
88
+ const filterValue = filter[config.filterKey];
89
+ if (config.condition(filterValue)) {
90
+ return query[config.queryMethod](config.queryField, filterValue);
91
+ } else if (config.fallback) {
92
+ return applyFilterToQuery(query, config.fallback, filter);
93
+ }
94
+ return query;
95
+ };
96
+ // Apply filters using the configuration
97
+ filterConfig.forEach(config => {
98
+ query = applyFilterToQuery(query, config, filter);
99
+ });
100
+ if (isUserLocationEnabled && isSearchingNearby) {
101
+ const userGeohash = geohash.encode(filter.latitude, filter.longitude, GEO_HASH_PRECISION);
102
+ const neighborGeohashes = geohash.neighbors(userGeohash);
103
+ const geohashList = [userGeohash, ...neighborGeohashes];
104
+ query = query.hasSome('locHash', geohashList);
105
+ }
106
+ if (filter.searchText.trim() !== '') {
107
+ query = query.contains('fullName', filter.searchText);
108
+ }
109
+ if (!includeStudents) {
110
+ query = query.ne('memberships.membertype', MEMBERSHIPS_TYPES.STUDENT);
111
+ }
112
+ return query;
113
+ },
114
+ run: async query => {
115
+ const baseQuery = query.ascending('firstName').fields(...Object.values(MEMBERS_FIELDS));
116
+ const getRandomSkip = totalCount => {
117
+ let randomSkip = 0;
118
+ if (totalCount > MAX__MEMBERS_SEARCH_RESULTS) {
119
+ const maxSkip = totalCount - MAX__MEMBERS_SEARCH_RESULTS;
120
+ randomSkip = Math.floor(Math.random() * (maxSkip + 1));
121
+ }
122
+ return randomSkip;
123
+ };
124
+ const getResult = async query => {
125
+ if (isSearchingNearby) {
126
+ return fetchAllItemsInParallel(baseQuery);
127
+ }
128
+ const totalCount = await query.count();
129
+ const randomSkip = getRandomSkip(totalCount);
130
+ const result = await query
131
+ .skip(randomSkip)
132
+ .limit(MAX__MEMBERS_SEARCH_RESULTS)
133
+ .find({ omitTotalCount: true });
134
+
135
+ // Shuffle the result items for additional randomization
136
+ return {
137
+ ...result,
138
+ items: shuffleArray(result.items),
139
+ };
140
+ };
141
+
142
+ const result = await getResult(baseQuery);
143
+ if (isUserLocationEnabled) {
144
+ const withDistances = result.items.map(item => ({
145
+ ...item,
146
+ distance: calculateDistance(
147
+ {
148
+ latitude: filter.latitude,
149
+ longitude: filter.longitude,
150
+ },
151
+ findMainAddress(item.addressDisplayOption, item.addresses)
152
+ ),
153
+ }));
154
+ const resultWithDistances = {
155
+ ...result,
156
+ items: withDistances,
157
+ };
158
+ if (isSearchingNearby) {
159
+ return {
160
+ ...resultWithDistances,
161
+ items: withDistances
162
+ .filter(item => item.distance !== null)
163
+ .sort((a, b) => a.distance - b.distance)
164
+ .slice(0, MAX__MEMBERS_SEARCH_RESULTS),
165
+ };
166
+ }
167
+ return resultWithDistances;
168
+ }
169
+ return result;
170
+ },
171
+ };
172
+ }
173
+
174
+ // Generic parallel fetch function for large datasets
175
+ async function fetchAllItemsInParallel(query) {
176
+ const batchSize = WIX_QUERY_MAX_LIMIT;
177
+ const allItems = [];
178
+
179
+ const firstResult = await query.skip(0).limit(batchSize).find();
180
+
181
+ const totalBatches = firstResult.totalPages;
182
+ allItems.push(...firstResult.items);
183
+
184
+ if (totalBatches > 1) {
185
+ // Create parallel promises for all remaining batches
186
+ const batchPromises = [];
187
+ for (let i = 1; i < totalBatches; i++) {
188
+ const skip = i * batchSize;
189
+ const promise = query
190
+ .skip(skip)
191
+ .limit(batchSize)
192
+ .find()
193
+ .then(result => result.items);
194
+ batchPromises.push(promise);
195
+ }
196
+
197
+ // Execute all batches in parallel
198
+ const batchResults = await Promise.all(batchPromises);
199
+ for (const items of batchResults) {
200
+ if (items.length > 0) {
201
+ allItems.push(...items);
202
+ }
203
+ }
204
+ }
205
+
206
+ return {
207
+ ...firstResult,
208
+ items: allItems,
209
+ };
210
+ }
211
+
212
+ /**
213
+ * Get all interests from the database
214
+ * @returns {Promise<Array<string>>} Array of interest titles sorted alphabetically
215
+ */
216
+ async function getInterestAll() {
217
+ try {
218
+ let res = await wixData.query(COLLECTIONS.INTERESTS).limit(1000).find();
219
+
220
+ let interests = res.items.map(x => x.title);
221
+
222
+ while (res.hasNext()) {
223
+ res = await res.next();
224
+ interests.push(...res.items.map(x => x.title));
225
+ }
226
+
227
+ // Sort the interests alphabetically (case-insensitive)
228
+ interests = interests.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
229
+
230
+ return interests;
231
+ } catch (e) {
232
+ console.error('Error in getInterestAll:', e);
233
+ throw e;
234
+ }
235
+ }
236
+ async function clearCollection(collectionName) {
237
+ try {
238
+ await wixData.truncate(collectionName);
239
+ } catch (err) {
240
+ throw new Error(`Failed to clearCollection ${collectionName} with error: ${err.message}`);
241
+ }
242
+ }
243
+
244
+ module.exports = {
245
+ buildMembersSearchQuery,
246
+ fetchAllItemsInParallel,
247
+ getInterestAll,
248
+ clearCollection,
249
+ };
@@ -0,0 +1,45 @@
1
+ const PAC_API_URL = 'https://members.abmp.com/eweb/api/Wix';
2
+ const BACKUP_API_URL = 'https://psdevteamenterpris.wixstudio.com/abmp-backup/_functions';
3
+ const SSO_TOKEN_AUTH_API_URL = 'https://members.professionalassistcorp.com/';
4
+
5
+ /**
6
+ * Valid configuration keys for getSiteConfigs function
7
+ * @readonly
8
+ * @enum {string}
9
+ */
10
+ const CONFIG_KEYS = {
11
+ AUTOMATION_EMAIL_TRIGGER_ID: 'AUTOMATION_EMAIL_TRIGGER_ID',
12
+ SITE_ASSOCIATION: 'SITE_ASSOCIATION',
13
+ DEFAULT_PROFILE_SEO_DESCRIPTION: 'DEFAULT_PROFILE_SEO_DESCRIPTION',
14
+ INTERESTS_API_URL: 'INTERESTS_API_URL',
15
+ SITE_LOGO_URL: 'SITE_LOGO_URL',
16
+ MEMBERS_EXTERNAL_PORTAL_URL: 'MEMBERS_EXTERNAL_PORTAL_URL',
17
+ DEFAULT_PROFILE_IMAGE: 'DEFAULT_PROFILE_IMAGE',
18
+ };
19
+
20
+ const MAX__MEMBERS_SEARCH_RESULTS = 120;
21
+ const WIX_QUERY_MAX_LIMIT = 1000;
22
+
23
+ const GEO_HASH_PRECISION = 3;
24
+
25
+ const COMPILED_FILTERS_FIELDS = {
26
+ COMPILED_STATE_LIST: 'COMPILED_STATE_LIST',
27
+ COMPILED_AREAS_OF_PRACTICES: 'COMPILED_AREAS_OF_PRACTICES',
28
+ COMPILED_STATE_CITY_MAP: 'COMPILED_STATE_CITY_MAP',
29
+ };
30
+ const MEMBERSHIPS_TYPES = {
31
+ STUDENT: 'Student',
32
+ PAC_STAFF: 'PAC STAFF',
33
+ };
34
+
35
+ module.exports = {
36
+ CONFIG_KEYS,
37
+ MAX__MEMBERS_SEARCH_RESULTS,
38
+ WIX_QUERY_MAX_LIMIT,
39
+ GEO_HASH_PRECISION,
40
+ PAC_API_URL,
41
+ COMPILED_FILTERS_FIELDS,
42
+ MEMBERSHIPS_TYPES,
43
+ SSO_TOKEN_AUTH_API_URL,
44
+ BACKUP_API_URL,
45
+ };
@@ -0,0 +1,128 @@
1
+ const { contacts } = require('@wix/crm');
2
+ const { auth } = require('@wix/essentials');
3
+
4
+ const elevatedGetContact = auth.elevate(contacts.getContact);
5
+ const elevatedUpdateContact = auth.elevate(contacts.updateContact);
6
+
7
+ /**
8
+ * Generic contact update helper function
9
+ * @param {string} contactId - The contact ID in Wix CRM
10
+ * @param {function} updateInfoCallback - Function that returns the updated info object
11
+ * @param {string} operationName - Name of the operation for logging
12
+ */
13
+ async function updateContactInfo(contactId, updateInfoCallback, operationName) {
14
+ if (!contactId) {
15
+ throw new Error('Contact ID is required');
16
+ }
17
+
18
+ try {
19
+ const contact = await elevatedGetContact(contactId);
20
+ const currentInfo = contact.info;
21
+ const updatedInfo = updateInfoCallback(currentInfo);
22
+
23
+ await elevatedUpdateContact(contactId, updatedInfo, contact.revision);
24
+ } catch (error) {
25
+ console.error(`Error in ${operationName}:`, error);
26
+ throw new Error(`Failed to ${operationName}: ${error.message}`);
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Updates contact email in Wix CRM
32
+ * @param {string} contactId - The contact ID in Wix CRM
33
+ * @param {string} newEmail - The new email address
34
+ */
35
+ async function updateContactEmail(contactId, newEmail) {
36
+ if (!newEmail) {
37
+ throw new Error('New email is required');
38
+ }
39
+
40
+ return await updateContactInfo(
41
+ contactId,
42
+ currentInfo => ({
43
+ ...currentInfo,
44
+ emails: {
45
+ items: [
46
+ {
47
+ email: newEmail,
48
+ primary: true,
49
+ },
50
+ ],
51
+ },
52
+ }),
53
+ 'update contact email'
54
+ );
55
+ }
56
+
57
+ /**
58
+ * Updates contact names in Wix CRM
59
+ * @param {string} contactId - The contact ID in Wix CRM
60
+ * @param {string} firstName - The new first name
61
+ * @param {string} lastName - The new last name
62
+ */
63
+ async function updateContactNames(contactId, firstName, lastName) {
64
+ if (!firstName && !lastName) {
65
+ throw new Error('At least one name field is required');
66
+ }
67
+
68
+ return await updateContactInfo(
69
+ contactId,
70
+ currentInfo => ({
71
+ ...currentInfo,
72
+ name: {
73
+ first: firstName || currentInfo?.name?.first || '',
74
+ last: lastName || currentInfo?.name?.last || '',
75
+ },
76
+ }),
77
+ 'update contact names'
78
+ );
79
+ }
80
+
81
+ /**
82
+ * Update fields if they have changed
83
+ * @param {Array} existingValues - Current values for comparison
84
+ * @param {Array} newValues - New values to compare against
85
+ * @param {Function} updater - Function to call if values changed
86
+ * @param {Function} argsBuilder - Function to build arguments for updater
87
+ */
88
+ const updateIfChanged = (existingValues, newValues, updater, argsBuilder) => {
89
+ const hasChanged = existingValues.some((val, idx) => val !== newValues[idx]);
90
+ if (!hasChanged) return null;
91
+ return updater(...argsBuilder(newValues));
92
+ };
93
+
94
+ /**
95
+ * Updates member contact information in CRM if fields have changed
96
+ * @param {Object} data - New member data
97
+ * @param {Object} existingMemberData - Existing member data
98
+ */
99
+ const updateMemberContactInfo = async (data, existingMemberData) => {
100
+ const { contactId } = existingMemberData;
101
+
102
+ const updateConfig = [
103
+ {
104
+ fields: ['contactFormEmail'],
105
+ updater: updateContactEmail,
106
+ args: ([email]) => [contactId, email],
107
+ },
108
+ {
109
+ fields: ['firstName', 'lastName'],
110
+ updater: updateContactNames,
111
+ args: ([firstName, lastName]) => [contactId, firstName, lastName],
112
+ },
113
+ ];
114
+
115
+ const updatePromises = updateConfig
116
+ .map(({ fields, updater, args }) => {
117
+ const existingValues = fields.map(field => existingMemberData[field]);
118
+ const newValues = fields.map(field => data[field]);
119
+ return updateIfChanged(existingValues, newValues, updater, args);
120
+ })
121
+ .filter(Boolean);
122
+
123
+ await Promise.all(updatePromises);
124
+ };
125
+
126
+ module.exports = {
127
+ updateMemberContactInfo,
128
+ };
@@ -0,0 +1,196 @@
1
+ const { bulkSaveMembers, getMemberBySlug } = require('../members-data-methods');
2
+
3
+ const { generateUpdatedMemberData } = require('./process-member-methods');
4
+ const {
5
+ changeWixMembersEmails,
6
+ extractUrlCounter,
7
+ incrementUrlCounter,
8
+ extractBaseUrl,
9
+ } = require('./utils');
10
+
11
+ /**
12
+ * Ensures unique URLs within a batch of members by deduplicating URLs
13
+ * Groups members by their base URL (normalized) and assigns unique counters
14
+ * Also checks database to handle cross-page conflicts
15
+ * @param {Array} memberDataList - Array of processed member data
16
+ * @returns {Promise<Array>} - Array of members with unique URLs assigned
17
+ */
18
+ async function ensureUniqueUrlsInBatch(memberDataList) {
19
+ if (!Array.isArray(memberDataList) || memberDataList.length === 0) {
20
+ return memberDataList;
21
+ }
22
+
23
+ // Group members by their normalized base URL
24
+ const urlGroups = new Map();
25
+
26
+ memberDataList.forEach(member => {
27
+ if (!member || !member.url) {
28
+ return;
29
+ }
30
+
31
+ // Extract the base URL (without any counter) for grouping
32
+ const baseUrl = extractBaseUrl(member.url);
33
+ if (!urlGroups.has(baseUrl)) {
34
+ urlGroups.set(baseUrl, []);
35
+ }
36
+ urlGroups.get(baseUrl).push(member);
37
+ });
38
+
39
+ // For each group, check database and assign unique URLs sequentially
40
+ for (const [baseUrl, members] of urlGroups.entries()) {
41
+ if (members.length <= 1) {
42
+ // Single member - still check DB to ensure it doesn't conflict with other pages
43
+ const member = members[0];
44
+ if (member) {
45
+ const dbMember = await getMemberBySlug({
46
+ slug: baseUrl,
47
+ excludeDropped: false,
48
+ normalizeSlugForComparison: true,
49
+ });
50
+
51
+ if (dbMember && dbMember.url) {
52
+ // Conflict found in DB, need to add counter
53
+ member.url = incrementUrlCounter(dbMember.url, baseUrl);
54
+ console.log(
55
+ `Found DB conflict for single member with base URL "${baseUrl}", assigned: ${member.url}`
56
+ );
57
+ }
58
+ }
59
+ continue;
60
+ }
61
+
62
+ // Sort members to ensure consistent ordering
63
+ members.sort((a, b) => {
64
+ if (a.url && b.url) {
65
+ return String(a.url).localeCompare(String(b.url));
66
+ }
67
+ return 0;
68
+ });
69
+
70
+ // Check database for existing members with this base URL to find highest counter
71
+ const dbMember = await getMemberBySlug({
72
+ slug: baseUrl,
73
+ excludeDropped: false,
74
+ normalizeSlugForComparison: true,
75
+ });
76
+
77
+ const dbMaxCounter = extractUrlCounter(dbMember?.url);
78
+
79
+ // Find the highest existing counter among all members in this batch group
80
+ let batchMaxCounter = -1;
81
+ members.forEach(member => {
82
+ const originalUrl = member.url;
83
+ const urlParts = originalUrl.split('-');
84
+ const lastSegment = urlParts[urlParts.length - 1];
85
+ const isNumeric = /^\d+$/.test(lastSegment);
86
+ if (isNumeric) {
87
+ const counter = parseInt(lastSegment, 10);
88
+ if (counter > batchMaxCounter) {
89
+ batchMaxCounter = counter;
90
+ }
91
+ }
92
+ });
93
+
94
+ // Start index from the maximum of DB counter and batch counter + 1
95
+ const maxCounter = Math.max(dbMaxCounter, batchMaxCounter);
96
+ const startIndex = maxCounter + 1;
97
+
98
+ // Assign unique URLs: start from the appropriate index
99
+ members.forEach((member, index) => {
100
+ const assignedIndex = startIndex + index;
101
+ if (assignedIndex === 0) {
102
+ // Index 0 means no counter, use baseUrl
103
+ member.url = baseUrl;
104
+ } else {
105
+ // Index > 0 means add counter
106
+ member.url = `${baseUrl}-${assignedIndex}`;
107
+ }
108
+ });
109
+
110
+ console.log(
111
+ `Deduplicated ${
112
+ members.length
113
+ } members with base URL "${baseUrl}" (DB max: ${dbMaxCounter}, batch max: ${batchMaxCounter}, start: ${startIndex}): ${members
114
+ .map(m => m.url)
115
+ .join(', ')}`
116
+ );
117
+ }
118
+
119
+ return memberDataList;
120
+ }
121
+
122
+ /**
123
+ * Processes and saves multiple member records in bulk
124
+ * @param {Object} options - The options object
125
+ * @param {Array} options.memberDataList - Array of member data from API
126
+ * @param {number} options.currentPageNumber - Current page number being processed
127
+ * @param {boolean} [options.addInterests=true] - Whether to add interests to the member data
128
+ * @param {Array} memberDataList - Array of member data from API
129
+ * @returns {Promise<Object>} - Bulk save operation result with statistics
130
+ */
131
+ const bulkProcessAndSaveMemberData = async ({
132
+ memberDataList,
133
+ currentPageNumber,
134
+ addInterests = true,
135
+ }) => {
136
+ if (!Array.isArray(memberDataList) || memberDataList.length === 0) {
137
+ throw new Error('Invalid member data list provided');
138
+ }
139
+
140
+ const startTime = Date.now();
141
+
142
+ try {
143
+ const processedMemberDataPromises = memberDataList.map(memberData =>
144
+ generateUpdatedMemberData({
145
+ inputMemberData: memberData,
146
+ currentPageNumber,
147
+ addInterests,
148
+ })
149
+ );
150
+
151
+ const processedMemberDataList = await Promise.all(processedMemberDataPromises);
152
+ const validMemberData = processedMemberDataList.filter(
153
+ data => data !== null && data !== undefined
154
+ );
155
+ if (validMemberData.length === 0) {
156
+ return {
157
+ totalProcessed: memberDataList.length,
158
+ totalSaved: 0,
159
+ totalFailed: memberDataList.length,
160
+ processingTime: Date.now() - startTime,
161
+ };
162
+ }
163
+ const newMembers = validMemberData.filter(data => data.isNewToDb);
164
+ const existingMembers = validMemberData.filter(data => !data.isNewToDb);
165
+ // Ensure unique URLs within the batch to prevent duplicates (also checks DB for cross-page conflicts)
166
+ const uniqueUrlsNewToDBMembersList = await ensureUniqueUrlsInBatch(newMembers);
167
+ const uniqueUrlsMembersData = [...uniqueUrlsNewToDBMembersList, ...existingMembers];
168
+ const toChangeWixMembersEmails = [];
169
+ const toSaveMembersData = uniqueUrlsMembersData.map(member => {
170
+ const { isLoginEmailChanged, isNewToDb: _isNewToDb, ...restMemberData } = member;
171
+ if (member.contactId && isLoginEmailChanged) {
172
+ toChangeWixMembersEmails.push(member);
173
+ }
174
+ return restMemberData; //we don't want to store the isLoginEmailChanged in the database, it's just a flag to know if we need to change the login email in Members area
175
+ });
176
+ const saveResult = await bulkSaveMembers(toSaveMembersData);
177
+ // Change login emails for users who was dropped but now are back to system as new members and have different loginEmail for users with action DROP
178
+ if (toChangeWixMembersEmails.length > 0) {
179
+ await changeWixMembersEmails(toChangeWixMembersEmails);
180
+ }
181
+ const totalFailed = memberDataList.length - validMemberData.length;
182
+ const processingTime = Date.now() - startTime;
183
+
184
+ return {
185
+ ...saveResult,
186
+ totalProcessed: memberDataList.length,
187
+ totalSaved: validMemberData.length,
188
+ totalFailed: totalFailed,
189
+ processingTime: processingTime,
190
+ };
191
+ } catch (error) {
192
+ throw new Error(`Bulk operation failed: ${error.message}`);
193
+ }
194
+ };
195
+
196
+ module.exports = { bulkProcessAndSaveMemberData, ensureUniqueUrlsInBatch };