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.
- package/.husky/pre-commit +22 -0
- package/.prettierignore +7 -0
- package/.prettierrc.json +16 -0
- package/README.md +1 -0
- package/backend/automations-methods.js +13 -0
- package/backend/cms-data-methods.js +249 -0
- package/backend/consts.js +45 -0
- package/backend/contacts-methods.js +128 -0
- package/backend/daily-pull/bulk-process-methods.js +196 -0
- package/backend/daily-pull/consts.js +42 -0
- package/backend/daily-pull/index.js +5 -0
- package/backend/daily-pull/process-member-methods.js +304 -0
- package/backend/daily-pull/sync-to-cms-methods.js +124 -0
- package/backend/daily-pull/utils.js +124 -0
- package/backend/data-hooks.js +29 -0
- package/backend/dev-only-methods.js +18 -0
- package/backend/elevated-modules.js +18 -0
- package/backend/forms-methods.js +52 -0
- package/backend/http-functions/httpFunctions.js +86 -0
- package/backend/http-functions/index.js +3 -0
- package/backend/http-functions/interests.js +37 -0
- package/backend/index.js +19 -0
- package/backend/jobs.js +42 -0
- package/backend/login/index.js +7 -0
- package/backend/login/login-methods-factory.js +24 -0
- package/backend/login/qa-login-methods.js +72 -0
- package/backend/login/sso-methods.js +158 -0
- package/backend/members-area-methods.js +89 -0
- package/backend/members-data-methods.js +545 -0
- package/backend/pac-api-methods.js +34 -0
- package/backend/routers/index.js +3 -0
- package/backend/routers/methods.js +177 -0
- package/backend/routers/utils.js +121 -0
- package/backend/search-filters-methods.js +124 -0
- package/backend/tasks/consts.js +23 -0
- package/backend/tasks/index.js +7 -0
- package/backend/tasks/migration-methods.js +46 -0
- package/backend/tasks/tasks-configs.js +158 -0
- package/backend/tasks/tasks-helpers-methods.js +419 -0
- package/backend/tasks/tasks-process-methods.js +545 -0
- package/backend/tasks/url-migration-methods.js +378 -0
- package/backend/utils.js +221 -0
- package/dev-only-scripts/find-duplicate-urls.js +201 -0
- package/eslint.config.js +120 -0
- package/index.js +5 -5
- package/package.json +48 -3
- package/pages/ContactUs.js +129 -0
- package/pages/Home.js +770 -0
- package/pages/LearnMore.js +27 -0
- package/pages/LoadingPage.js +20 -0
- package/pages/Profile.js +350 -0
- package/pages/QAPage.js +39 -0
- package/pages/SaveAlerts.js +13 -0
- package/pages/SelectBannerImages.js +46 -0
- package/pages/deleteConfirm.js +19 -0
- package/pages/index.js +12 -0
- package/pages/personalDetails.js +2063 -0
- package/public/Utils/homePage.js +765 -0
- package/public/Utils/personalDetailsUtils.js +25 -0
- package/public/Utils/sharedUtils.js +172 -0
- package/public/consts.js +112 -0
- package/public/index.js +5 -0
- package/public/messages.js +16 -0
- 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
|
+
|
package/.prettierignore
ADDED
package/.prettierrc.json
ADDED
|
@@ -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 };
|