abmp-npm 2.0.68 → 2.0.70
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/backend/__tests__/login-email-sync.test.js +49 -0
- package/backend/consts.js +10 -0
- package/backend/contacts-methods.js +3 -0
- package/backend/daily-pull/bulk-process-methods.js +42 -10
- package/backend/daily-pull/process-member-methods.js +3 -1
- package/backend/daily-pull/utils.js +41 -3
- package/backend/jobs.js +15 -0
- package/backend/login/generate-member-session-token.js +27 -0
- package/backend/login/index.js +4 -3
- package/backend/login/qa-login-methods.js +4 -3
- package/backend/login/sso-methods.js +4 -3
- package/backend/member-contact-orchestration.js +6 -1
- package/backend/members-area-methods.js +33 -34
- package/backend/members-data-methods.js +102 -8
- package/backend/tasks/consts.js +2 -0
- package/backend/tasks/email-normalize-methods.js +134 -0
- package/backend/tasks/tasks-configs.js +18 -0
- package/backend/tasks/tasks-process-methods.js +33 -17
- package/package.json +3 -3
- package/public/Utils/sharedUtils.js +24 -0
- package/backend/login/login-methods-factory.js +0 -24
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const { LOGIN_EMAIL_SYNC_STATUS } = require('../consts');
|
|
2
|
+
const { summarizeLoginEmailOutcomes } = require('../daily-pull/utils');
|
|
3
|
+
|
|
4
|
+
const { UPDATED, FAILED, SKIPPED } = LOGIN_EMAIL_SYNC_STATUS;
|
|
5
|
+
|
|
6
|
+
describe('summarizeLoginEmailOutcomes', () => {
|
|
7
|
+
test('collects only FAILED outcomes as failures and failed ids', () => {
|
|
8
|
+
const outcomes = [
|
|
9
|
+
{ memberId: 1, wixMemberId: 'w1', desiredEmail: 'a@x.com', status: UPDATED },
|
|
10
|
+
{
|
|
11
|
+
memberId: 2,
|
|
12
|
+
wixMemberId: 'w2',
|
|
13
|
+
desiredEmail: 'b@x.com',
|
|
14
|
+
status: FAILED,
|
|
15
|
+
error: 'duplicate',
|
|
16
|
+
},
|
|
17
|
+
{ memberId: 3, status: SKIPPED, desiredEmail: 'c@x.com' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const { failedMemberIds, failures } = summarizeLoginEmailOutcomes(outcomes);
|
|
21
|
+
|
|
22
|
+
expect([...failedMemberIds]).toEqual([2]);
|
|
23
|
+
expect(failures).toEqual([
|
|
24
|
+
{ memberId: 2, wixMemberId: 'w2', desiredEmail: 'b@x.com', error: 'duplicate' },
|
|
25
|
+
]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('UPDATED and SKIPPED never produce failures (CMS email is left to advance/no-op)', () => {
|
|
29
|
+
const outcomes = [
|
|
30
|
+
{ memberId: 1, status: UPDATED, desiredEmail: 'a@x.com' },
|
|
31
|
+
{ memberId: 2, status: SKIPPED, desiredEmail: 'b@x.com' },
|
|
32
|
+
];
|
|
33
|
+
const { failedMemberIds, failures } = summarizeLoginEmailOutcomes(outcomes);
|
|
34
|
+
expect(failedMemberIds.size).toBe(0);
|
|
35
|
+
expect(failures).toEqual([]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('defaults a missing error message', () => {
|
|
39
|
+
const { failures } = summarizeLoginEmailOutcomes([
|
|
40
|
+
{ memberId: 9, wixMemberId: 'w9', desiredEmail: 'z@x.com', status: FAILED },
|
|
41
|
+
]);
|
|
42
|
+
expect(failures[0].error).toBe('unknown error');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('handles empty / missing input', () => {
|
|
46
|
+
expect(summarizeLoginEmailOutcomes([]).failures).toEqual([]);
|
|
47
|
+
expect(summarizeLoginEmailOutcomes().failures).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
});
|
package/backend/consts.js
CHANGED
|
@@ -34,6 +34,15 @@ const MEMBERSHIPS_TYPES = {
|
|
|
34
34
|
PAC_STAFF: 'PAC STAFF',
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Possible outcomes of attempting to change a Wix member's login email during the sync.
|
|
39
|
+
*/
|
|
40
|
+
const LOGIN_EMAIL_SYNC_STATUS = {
|
|
41
|
+
UPDATED: 'updated', // Wix login email successfully changed to the desired email
|
|
42
|
+
FAILED: 'failed', // change failed -> keep the CMS login email unchanged and report for manual handling
|
|
43
|
+
SKIPPED: 'skipped', // member has no wixMemberId, nothing to change
|
|
44
|
+
};
|
|
45
|
+
|
|
37
46
|
module.exports = {
|
|
38
47
|
CONFIG_KEYS,
|
|
39
48
|
MAX__MEMBERS_SEARCH_RESULTS,
|
|
@@ -45,4 +54,5 @@ module.exports = {
|
|
|
45
54
|
MEMBERSHIPS_TYPES,
|
|
46
55
|
SSO_TOKEN_AUTH_API_URL,
|
|
47
56
|
BACKUP_API_URL,
|
|
57
|
+
LOGIN_EMAIL_SYNC_STATUS,
|
|
48
58
|
};
|
|
@@ -28,6 +28,9 @@ async function createSiteContact(contactData) {
|
|
|
28
28
|
},
|
|
29
29
|
};
|
|
30
30
|
console.log('[createSiteContact]contactInfo', JSON.stringify(contactInfo, null, 2));
|
|
31
|
+
// Intentionally NOT passing allowDuplicates: Wix CRM enforces email uniqueness
|
|
32
|
+
// case-insensitively, which is the guard we want against two different members sharing an
|
|
33
|
+
// email. Same-member cases never reach here — they collapse to a single entity upstream.
|
|
31
34
|
const createContactResponse = await elevatedCreateContact(contactInfo);
|
|
32
35
|
console.log(
|
|
33
36
|
'[createSiteContact]createContactResponse',
|
|
@@ -2,7 +2,12 @@ const { bulkSaveMembers, getMemberBySlug } = require('../members-data-methods');
|
|
|
2
2
|
const { extractUrlCounter } = require('../utils');
|
|
3
3
|
|
|
4
4
|
const { generateUpdatedMemberData } = require('./process-member-methods');
|
|
5
|
-
const {
|
|
5
|
+
const {
|
|
6
|
+
changeWixMembersEmails,
|
|
7
|
+
summarizeLoginEmailOutcomes,
|
|
8
|
+
incrementUrlCounter,
|
|
9
|
+
extractBaseUrl,
|
|
10
|
+
} = require('./utils');
|
|
6
11
|
|
|
7
12
|
/**
|
|
8
13
|
* Ensures unique URLs within a batch of members by deduplicating URLs
|
|
@@ -151,28 +156,55 @@ const bulkProcessAndSaveMemberData = async ({ memberDataList, currentPageNumber
|
|
|
151
156
|
// Ensure unique URLs within the batch to prevent duplicates (also checks DB for cross-page conflicts)
|
|
152
157
|
const uniqueUrlsNewToDBMembersList = await ensureUniqueUrlsInBatch(newMembers);
|
|
153
158
|
const uniqueUrlsMembersData = [...uniqueUrlsNewToDBMembersList, ...existingMembers];
|
|
154
|
-
|
|
159
|
+
|
|
160
|
+
// Change Wix login emails FIRST so we know which succeeded before saving the CMS records.
|
|
161
|
+
const toChangeWixMembersEmails = uniqueUrlsMembersData.filter(
|
|
162
|
+
member => member.wixMemberId && member.isLoginEmailChanged
|
|
163
|
+
);
|
|
164
|
+
let failedLoginEmailIds = new Set();
|
|
165
|
+
let loginEmailFailures = [];
|
|
166
|
+
if (toChangeWixMembersEmails.length > 0) {
|
|
167
|
+
const outcomes = await changeWixMembersEmails(toChangeWixMembersEmails);
|
|
168
|
+
({ failedMemberIds: failedLoginEmailIds, failures: loginEmailFailures } =
|
|
169
|
+
summarizeLoginEmailOutcomes(outcomes));
|
|
170
|
+
}
|
|
171
|
+
|
|
155
172
|
const toSaveMembersData = uniqueUrlsMembersData.map(member => {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
173
|
+
// Transient flags only drive the sync above; never persist them.
|
|
174
|
+
const {
|
|
175
|
+
isLoginEmailChanged: _isLoginEmailChanged,
|
|
176
|
+
isNewToDb: _isNewToDb,
|
|
177
|
+
previousLoginEmail,
|
|
178
|
+
...restMemberData
|
|
179
|
+
} = member;
|
|
180
|
+
// If the Wix login-email change failed, keep the CMS login email unchanged (revert to the
|
|
181
|
+
// previous value) so the CMS stays consistent with Wix; the failure is reported below for
|
|
182
|
+
// manual handling.
|
|
183
|
+
if (failedLoginEmailIds.has(member.memberId) && previousLoginEmail !== undefined) {
|
|
184
|
+
return { ...restMemberData, email: previousLoginEmail };
|
|
159
185
|
}
|
|
160
|
-
return restMemberData;
|
|
186
|
+
return restMemberData;
|
|
161
187
|
});
|
|
162
188
|
const saveResult = await bulkSaveMembers(toSaveMembersData);
|
|
163
|
-
// 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
|
|
164
|
-
if (toChangeWixMembersEmails.length > 0) {
|
|
165
|
-
await changeWixMembersEmails(toChangeWixMembersEmails);
|
|
166
|
-
}
|
|
167
189
|
const totalFailed = memberDataList.length - validMemberData.length;
|
|
168
190
|
const processingTime = Date.now() - startTime;
|
|
169
191
|
|
|
192
|
+
if (loginEmailFailures.length > 0) {
|
|
193
|
+
console.error(
|
|
194
|
+
`[loginEmailSync] ${loginEmailFailures.length} login-email change(s) failed on page ${currentPageNumber}; CMS login email left unchanged for memberIds: [${loginEmailFailures
|
|
195
|
+
.map(failure => failure.memberId)
|
|
196
|
+
.join(', ')}]`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
170
200
|
return {
|
|
171
201
|
...saveResult,
|
|
172
202
|
totalProcessed: memberDataList.length,
|
|
173
203
|
totalSaved: validMemberData.length,
|
|
174
204
|
totalFailed: totalFailed,
|
|
175
205
|
processingTime: processingTime,
|
|
206
|
+
loginEmailFailedCount: loginEmailFailures.length,
|
|
207
|
+
loginEmailFailures,
|
|
176
208
|
};
|
|
177
209
|
} catch (error) {
|
|
178
210
|
throw new Error(`Bulk operation failed: ${error.message}`);
|
|
@@ -165,10 +165,12 @@ async function createCoreMemberData(inputMemberData, existingDbMember, currentPa
|
|
|
165
165
|
newEmail &&
|
|
166
166
|
existingDbMember.email !== newEmail;
|
|
167
167
|
if (isMemberReinstatedWithNewEmail) {
|
|
168
|
-
// If exists in DB, and email was changed means this user was dropped before that's why it exists in DB, then only update loginEmail not contactFormEmail
|
|
168
|
+
// If exists in DB, and email was changed means this user was dropped before that's why it exists in DB, then only update loginEmail not contactFormEmail.
|
|
169
|
+
// previousLoginEmail lets the caller keep the old email if the Wix login-email change fails.
|
|
169
170
|
return {
|
|
170
171
|
email: newEmail,
|
|
171
172
|
isLoginEmailChanged: true,
|
|
173
|
+
previousLoginEmail: existingDbMember.email,
|
|
172
174
|
};
|
|
173
175
|
}
|
|
174
176
|
//If exists in DB but not reinstated with new email, then don't update emails
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const { LOGIN_EMAIL_SYNC_STATUS } = require('../consts');
|
|
1
2
|
const { updateWixMemberLoginEmail } = require('../members-area-methods');
|
|
2
3
|
const { extractUrlCounter } = require('../utils');
|
|
3
4
|
|
|
@@ -7,13 +8,49 @@ const isUpdatedMember = member => member.action !== MEMBER_ACTIONS.NONE;
|
|
|
7
8
|
const isSiteAssociatedMember = (member, siteAssociation) =>
|
|
8
9
|
member.memberships.some(membership => membership.association === siteAssociation);
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Attempts to change Wix login emails for the given members and returns one structured
|
|
13
|
+
* outcome per member (see updateWixMemberLoginEmail). Never throws for an individual member.
|
|
14
|
+
* @param {Array} toChangeWixMembersEmails
|
|
15
|
+
* @returns {Promise<Array>} outcomes
|
|
16
|
+
*/
|
|
10
17
|
const changeWixMembersEmails = async toChangeWixMembersEmails => {
|
|
11
18
|
console.log(
|
|
12
|
-
`
|
|
19
|
+
`[loginEmailSync] changing login emails for ${toChangeWixMembersEmails.length} members with ids: [${toChangeWixMembersEmails.map(member => member.memberId).join(', ')}]`
|
|
13
20
|
);
|
|
14
|
-
|
|
15
|
-
toChangeWixMembersEmails.map(member => updateWixMemberLoginEmail(member
|
|
21
|
+
const outcomes = await Promise.all(
|
|
22
|
+
toChangeWixMembersEmails.map(member => updateWixMemberLoginEmail(member))
|
|
16
23
|
);
|
|
24
|
+
const summary = outcomes.reduce((acc, outcome) => {
|
|
25
|
+
acc[outcome.status] = (acc[outcome.status] || 0) + 1;
|
|
26
|
+
return acc;
|
|
27
|
+
}, {});
|
|
28
|
+
console.log(`[loginEmailSync] results summary: ${JSON.stringify(summary)}`);
|
|
29
|
+
return outcomes;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Summarizes login-email sync outcomes for manual handling via the task result.
|
|
34
|
+
* Pure function so it can be unit-tested without Wix.
|
|
35
|
+
* @param {Array} outcomes - from updateWixMemberLoginEmail / changeWixMembersEmails
|
|
36
|
+
* @returns {{ failedMemberIds: Set, failures: Array }} set of failed memberIds (whose CMS login
|
|
37
|
+
* email must be left unchanged) and the failure records to surface in the task result
|
|
38
|
+
*/
|
|
39
|
+
const summarizeLoginEmailOutcomes = (outcomes = []) => {
|
|
40
|
+
const failedMemberIds = new Set();
|
|
41
|
+
const failures = [];
|
|
42
|
+
outcomes.forEach(outcome => {
|
|
43
|
+
if (outcome.status === LOGIN_EMAIL_SYNC_STATUS.FAILED) {
|
|
44
|
+
failedMemberIds.add(outcome.memberId);
|
|
45
|
+
failures.push({
|
|
46
|
+
memberId: outcome.memberId,
|
|
47
|
+
wixMemberId: outcome.wixMemberId,
|
|
48
|
+
desiredEmail: outcome.desiredEmail,
|
|
49
|
+
error: outcome.error || 'unknown error',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return { failedMemberIds, failures };
|
|
17
54
|
};
|
|
18
55
|
|
|
19
56
|
const extractBaseUrl = url => {
|
|
@@ -105,6 +142,7 @@ module.exports = {
|
|
|
105
142
|
isUpdatedMember,
|
|
106
143
|
isSiteAssociatedMember,
|
|
107
144
|
changeWixMembersEmails,
|
|
145
|
+
summarizeLoginEmailOutcomes,
|
|
108
146
|
validateCoreMemberData,
|
|
109
147
|
containsNonEnglish,
|
|
110
148
|
createFullName,
|
package/backend/jobs.js
CHANGED
|
@@ -97,6 +97,20 @@ async function scheduleFixUrlsWithSpacesTask() {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
async function scheduleNormalizeMemberEmailsTask() {
|
|
101
|
+
try {
|
|
102
|
+
console.log('scheduleNormalizeMemberEmails started!');
|
|
103
|
+
return await taskManager().schedule({
|
|
104
|
+
name: TASKS_NAMES.scheduleNormalizeMemberEmails,
|
|
105
|
+
data: {},
|
|
106
|
+
type: 'scheduled',
|
|
107
|
+
});
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error(`Failed to scheduleNormalizeMemberEmails: ${error.message}`);
|
|
110
|
+
throw new Error(`Failed to scheduleNormalizeMemberEmails: ${error.message}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
100
114
|
async function runDailyPullExecutionCheck() {
|
|
101
115
|
try {
|
|
102
116
|
console.log('runDailyPullExecutionCheck started!');
|
|
@@ -126,5 +140,6 @@ module.exports = {
|
|
|
126
140
|
scheduleCreateContactsFromMembersTask,
|
|
127
141
|
scheduleFixPrimaryAddressForMembersTask,
|
|
128
142
|
scheduleFixUrlsWithSpacesTask,
|
|
143
|
+
scheduleNormalizeMemberEmailsTask,
|
|
129
144
|
runDailyPullExecutionCheck,
|
|
130
145
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const { auth } = require('@wix/essentials');
|
|
2
|
+
const { authentication } = require('@wix/identity');
|
|
3
|
+
|
|
4
|
+
const elevatedSignOn = auth.elevate(authentication.signOn);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates a Wix member session token for SSO / QA login using @wix/identity signOn (elevated).
|
|
8
|
+
* @param {string} email - Member login email
|
|
9
|
+
* @returns {Promise<string>} Session token for authentication.applySessionToken on the client
|
|
10
|
+
*/
|
|
11
|
+
async function generateMemberSessionToken(email) {
|
|
12
|
+
const trimmedEmail = (email || '').trim();
|
|
13
|
+
if (!trimmedEmail) {
|
|
14
|
+
throw new Error('Email is required to generate a session token');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const response = await elevatedSignOn({ email: trimmedEmail });
|
|
18
|
+
const sessionToken = response?.sessionToken;
|
|
19
|
+
if (!sessionToken) {
|
|
20
|
+
throw new Error('Failed to generate session token: empty response from signOn');
|
|
21
|
+
}
|
|
22
|
+
return sessionToken;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
generateMemberSessionToken,
|
|
27
|
+
};
|
package/backend/login/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
const {
|
|
2
|
-
const { validateMemberToken } = require('./sso-methods');
|
|
1
|
+
const { loginQAMember } = require('./qa-login-methods');
|
|
2
|
+
const { validateMemberToken, authenticateSSOToken } = require('./sso-methods');
|
|
3
3
|
|
|
4
4
|
module.exports = {
|
|
5
|
-
|
|
5
|
+
loginQAMember,
|
|
6
6
|
validateMemberToken,
|
|
7
|
+
authenticateSSOToken,
|
|
7
8
|
};
|
|
@@ -2,6 +2,8 @@ const { CONFIG_KEYS } = require('../consts');
|
|
|
2
2
|
const { prepareMemberForQALogin, getQAUsers } = require('../members-data-methods');
|
|
3
3
|
const { getSecret, getSiteConfigs } = require('../utils');
|
|
4
4
|
|
|
5
|
+
const { generateMemberSessionToken } = require('./generate-member-session-token');
|
|
6
|
+
|
|
5
7
|
const validateQAUser = async userEmail => {
|
|
6
8
|
const qaUsers = await getQAUsers();
|
|
7
9
|
const matchingUserEmail = qaUsers.find(user => user.email === userEmail)?.email;
|
|
@@ -16,10 +18,9 @@ const validateQAUser = async userEmail => {
|
|
|
16
18
|
* @param {Object} params - The parameters for the login
|
|
17
19
|
* @param {string} params.userEmail - The email of the user to login
|
|
18
20
|
* @param {string} params.secret - The secret of the user to login
|
|
19
|
-
* @param {Function} generateSessionToken - a dependency of the method, injected by the createLoginMethods function
|
|
20
21
|
* @returns {Promise<Object>} The result of the login
|
|
21
22
|
*/
|
|
22
|
-
const loginQAMember = async ({ userEmail, secret }
|
|
23
|
+
const loginQAMember = async ({ userEmail, secret }) => {
|
|
23
24
|
try {
|
|
24
25
|
const [qaSecret, allowAnyMember] = await Promise.all([
|
|
25
26
|
getSecret('ABMP_QA_SECRET'),
|
|
@@ -36,7 +37,7 @@ const loginQAMember = async ({ userEmail, secret }, generateSessionToken) => {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
const memberData = await prepareMemberForQALogin(userEmail);
|
|
39
|
-
const token = await
|
|
40
|
+
const token = await generateMemberSessionToken(memberData.email);
|
|
40
41
|
return {
|
|
41
42
|
success: true,
|
|
42
43
|
token,
|
|
@@ -15,6 +15,8 @@ const {
|
|
|
15
15
|
getSecret,
|
|
16
16
|
} = require('../utils');
|
|
17
17
|
|
|
18
|
+
const { generateMemberSessionToken } = require('./generate-member-session-token');
|
|
19
|
+
|
|
18
20
|
/**
|
|
19
21
|
* Validates member token and retrieves member data
|
|
20
22
|
* @param {string} memberIdInput - The member ID to validate
|
|
@@ -112,10 +114,9 @@ async function checkAndFetchSSO(token) {
|
|
|
112
114
|
* Authenticate an SSO token
|
|
113
115
|
* @param {Object} params - The parameters for the authentication
|
|
114
116
|
* @param {string} params.token - The token to authenticate
|
|
115
|
-
* @param {Function} generateSessionToken - a dependency of the method, injected by the createLoginMethods function
|
|
116
117
|
* @returns {Promise<Object>} The result of the authentication
|
|
117
118
|
*/
|
|
118
|
-
const authenticateSSOToken = async ({ token }
|
|
119
|
+
const authenticateSSOToken = async ({ token }) => {
|
|
119
120
|
const responseToken = await checkAndFetchSSO(token);
|
|
120
121
|
const isValidToken = Boolean(
|
|
121
122
|
responseToken && typeof responseToken === 'string' && responseToken?.trim()
|
|
@@ -135,7 +136,7 @@ const authenticateSSOToken = async ({ token }, generateSessionToken) => {
|
|
|
135
136
|
const payload = jwt.payload;
|
|
136
137
|
const memberData = await prepareMemberForSSOLogin(payload);
|
|
137
138
|
console.log('memberDataCollectionId', memberData._id);
|
|
138
|
-
const sessionToken = await
|
|
139
|
+
const sessionToken = await generateMemberSessionToken(memberData.email);
|
|
139
140
|
const authObj = {
|
|
140
141
|
type: 'success',
|
|
141
142
|
memberId: memberData._id,
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Orchestrates syncing between Wix Members and CRM Contacts.
|
|
3
3
|
* Returns member data to save; caller does a single updateMember to avoid double-write.
|
|
4
4
|
*/
|
|
5
|
+
const { emailsMatch } = require('../public/Utils/sharedUtils');
|
|
6
|
+
|
|
5
7
|
const { createSiteContact, updateContactInfo, deleteSiteContact } = require('./contacts-methods');
|
|
6
8
|
|
|
7
9
|
/**
|
|
@@ -18,7 +20,10 @@ async function updateContactEmail(newContactEmail, existingMemberData) {
|
|
|
18
20
|
|
|
19
21
|
const { wixContactId, wixMemberId, email: loginEmail } = existingMemberData;
|
|
20
22
|
const isSingleEntity = wixContactId === wixMemberId;
|
|
21
|
-
|
|
23
|
+
// Compare case-insensitively: Wix CRM enforces email uniqueness without regard to case,
|
|
24
|
+
// so a contact email that equals the login email (e.g. different casing) must collapse to
|
|
25
|
+
// a single entity rather than attempt a duplicate contact write.
|
|
26
|
+
const contactEmailDiffersFromLogin = !emailsMatch(loginEmail, newContactEmail);
|
|
22
27
|
|
|
23
28
|
if (!contactEmailDiffersFromLogin) {
|
|
24
29
|
if (isSingleEntity) {
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
const { auth } = require('@wix/essentials');
|
|
2
2
|
const { members, authentication } = require('@wix/members');
|
|
3
|
+
|
|
4
|
+
const { LOGIN_EMAIL_SYNC_STATUS } = require('./consts');
|
|
5
|
+
|
|
3
6
|
const elevatedCreateMember = auth.elevate(members.createMember);
|
|
4
7
|
const elevatedChangeLoginEmail = auth.elevate(authentication.changeLoginEmail);
|
|
5
8
|
|
|
9
|
+
const LOG = '[loginEmailSync]';
|
|
10
|
+
|
|
6
11
|
function prepareMemberData(partner) {
|
|
7
12
|
const options = {
|
|
8
13
|
member: {
|
|
@@ -37,48 +42,42 @@ const getCurrentMember = async () => {
|
|
|
37
42
|
};
|
|
38
43
|
|
|
39
44
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
45
|
+
* Attempts to change a Wix member's login email to `member.email` and reports a structured
|
|
46
|
+
* outcome instead of swallowing failures. Never throws.
|
|
47
|
+
*
|
|
48
|
+
* Outcomes:
|
|
49
|
+
* - SKIPPED: member has no wixMemberId, nothing to change.
|
|
50
|
+
* - UPDATED: Wix login email changed successfully.
|
|
51
|
+
* - FAILED: change failed. The caller keeps the CMS login email unchanged (so it stays
|
|
52
|
+
* consistent with Wix) and reports the member in the task result for manual handling.
|
|
53
|
+
*
|
|
54
|
+
* @param {Object} member - Member with { memberId, wixMemberId, email }
|
|
55
|
+
* @returns {Promise<Object>} outcome
|
|
43
56
|
*/
|
|
44
|
-
async function updateWixMemberLoginEmail(member
|
|
57
|
+
async function updateWixMemberLoginEmail(member) {
|
|
58
|
+
const desiredEmail = member.email;
|
|
59
|
+
const base = { memberId: member.memberId, wixMemberId: member.wixMemberId, desiredEmail };
|
|
60
|
+
|
|
45
61
|
if (!member.wixMemberId) {
|
|
46
|
-
console.log(
|
|
47
|
-
return;
|
|
62
|
+
console.log(`${LOG} member ${member.memberId} has no wixMemberId - skipping`);
|
|
63
|
+
return { ...base, status: LOGIN_EMAIL_SYNC_STATUS.SKIPPED };
|
|
48
64
|
}
|
|
49
65
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
const updatedWixMember = await elevatedChangeLoginEmail(member.wixMemberId, member.email);
|
|
66
|
+
console.log(
|
|
67
|
+
`${LOG} attempting login-email change for member ${member.memberId} (wixMemberId: ${member.wixMemberId}) -> ${desiredEmail}`
|
|
68
|
+
);
|
|
56
69
|
|
|
70
|
+
try {
|
|
71
|
+
const updatedWixMember = await elevatedChangeLoginEmail(member.wixMemberId, desiredEmail);
|
|
57
72
|
console.log(
|
|
58
|
-
|
|
73
|
+
`${LOG} ✅ updated member ${member.memberId} (wixMemberId: ${member.wixMemberId}) -> ${updatedWixMember.loginEmail}`
|
|
59
74
|
);
|
|
60
|
-
|
|
61
|
-
if (!result.wixMemberUpdates) {
|
|
62
|
-
result.wixMemberUpdates = { successful: 0, failed: 0 };
|
|
63
|
-
}
|
|
64
|
-
result.wixMemberUpdates.successful++;
|
|
75
|
+
return { ...base, status: LOGIN_EMAIL_SYNC_STATUS.UPDATED };
|
|
65
76
|
} catch (error) {
|
|
66
|
-
console.error(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
result.wixMemberUpdates.failed++;
|
|
72
|
-
|
|
73
|
-
if (!result.wixMemberErrors) {
|
|
74
|
-
result.wixMemberErrors = [];
|
|
75
|
-
}
|
|
76
|
-
result.wixMemberErrors.push({
|
|
77
|
-
memberId: member.memberId,
|
|
78
|
-
wixMemberId: member.wixMemberId,
|
|
79
|
-
email: member.email,
|
|
80
|
-
error: error.message,
|
|
81
|
-
});
|
|
77
|
+
console.error(
|
|
78
|
+
`${LOG} ❌ login-email change failed for member ${member.memberId} (wixMemberId: ${member.wixMemberId}) -> ${desiredEmail}: ${error.message}`
|
|
79
|
+
);
|
|
80
|
+
return { ...base, status: LOGIN_EMAIL_SYNC_STATUS.FAILED, error: error.message };
|
|
82
81
|
}
|
|
83
82
|
}
|
|
84
83
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const { COLLECTIONS } = require('../public/consts');
|
|
2
|
-
const { isWixHostedImage } = require('../public/Utils/sharedUtils');
|
|
2
|
+
const { isWixHostedImage, emailsMatch, normalizeEmail } = require('../public/Utils/sharedUtils');
|
|
3
3
|
|
|
4
4
|
const { MEMBERSHIPS_TYPES } = require('./consts');
|
|
5
5
|
const { createSiteContact } = require('./contacts-methods');
|
|
@@ -35,7 +35,42 @@ async function findMemberByWixDataId(memberId) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
const hasDifferentEmails = memberData =>
|
|
38
|
-
memberData.contactFormEmail &&
|
|
38
|
+
Boolean(memberData.contactFormEmail) &&
|
|
39
|
+
!emailsMatch(memberData.contactFormEmail, memberData.email);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Returns a shallow copy of a member record with its email fields normalized
|
|
43
|
+
* (lowercased + trimmed). Wix CRM and our uniqueness checks treat emails
|
|
44
|
+
* case-insensitively, so we persist them in canonical form to keep `.eq` lookups
|
|
45
|
+
* reliable. Only rewrites string values that are actually present.
|
|
46
|
+
* @param {Object} memberData
|
|
47
|
+
* @returns {Object}
|
|
48
|
+
*/
|
|
49
|
+
const normalizeMemberEmailFields = memberData => {
|
|
50
|
+
if (!memberData || typeof memberData !== 'object') return memberData;
|
|
51
|
+
const normalized = { ...memberData };
|
|
52
|
+
if (typeof normalized.email === 'string') {
|
|
53
|
+
normalized.email = normalizeEmail(normalized.email);
|
|
54
|
+
}
|
|
55
|
+
if (typeof normalized.contactFormEmail === 'string') {
|
|
56
|
+
normalized.contactFormEmail = normalizeEmail(normalized.contactFormEmail);
|
|
57
|
+
}
|
|
58
|
+
return normalized;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Whether a member's stored email fields are not already in canonical form
|
|
63
|
+
* (lowercased + trimmed) and therefore need the normalization backfill.
|
|
64
|
+
* @param {Object} member
|
|
65
|
+
* @returns {boolean}
|
|
66
|
+
*/
|
|
67
|
+
const memberNeedsEmailNormalization = member =>
|
|
68
|
+
(typeof member.email === 'string' &&
|
|
69
|
+
member.email.length > 0 &&
|
|
70
|
+
member.email !== normalizeEmail(member.email)) ||
|
|
71
|
+
(typeof member.contactFormEmail === 'string' &&
|
|
72
|
+
member.contactFormEmail.length > 0 &&
|
|
73
|
+
member.contactFormEmail !== normalizeEmail(member.contactFormEmail));
|
|
39
74
|
|
|
40
75
|
async function createContactAndMemberIfNew(memberData) {
|
|
41
76
|
if (!memberData) {
|
|
@@ -97,8 +132,14 @@ async function bulkSaveMembers(memberDataList, collectionName = COLLECTIONS.MEMB
|
|
|
97
132
|
}
|
|
98
133
|
|
|
99
134
|
try {
|
|
135
|
+
// Normalize email fields only for the members collection; other collections passed here
|
|
136
|
+
// (e.g. staging copies) don't have these fields and must be saved untouched.
|
|
137
|
+
const listToSave =
|
|
138
|
+
collectionName === COLLECTIONS.MEMBERS_DATA
|
|
139
|
+
? memberDataList.map(normalizeMemberEmailFields)
|
|
140
|
+
: memberDataList;
|
|
100
141
|
// bulkSave all with batches of 1000 items as this is the Velo limit for bulkSave
|
|
101
|
-
const batches = chunkArray(
|
|
142
|
+
const batches = chunkArray(listToSave, 1000);
|
|
102
143
|
return await Promise.all(batches.map(batch => wixData.bulkSave(collectionName, batch)));
|
|
103
144
|
} catch (error) {
|
|
104
145
|
console.error('Error bulk saving members:', error);
|
|
@@ -244,24 +285,48 @@ const getAllEmptyAboutYouMembers = async () => {
|
|
|
244
285
|
*/
|
|
245
286
|
async function updateMember(memberToUpdate) {
|
|
246
287
|
try {
|
|
247
|
-
const updatedMember = await wixData.update(
|
|
288
|
+
const updatedMember = await wixData.update(
|
|
289
|
+
COLLECTIONS.MEMBERS_DATA,
|
|
290
|
+
normalizeMemberEmailFields(memberToUpdate)
|
|
291
|
+
);
|
|
248
292
|
return updatedMember;
|
|
249
293
|
} catch (error) {
|
|
250
294
|
throw new Error(`Failed to update member data: ${error.message}`);
|
|
251
295
|
}
|
|
252
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* Whether the given email is already used by a DIFFERENT member (case-insensitive).
|
|
299
|
+
* Returns false when the email is free or belongs to the same member, so a member can
|
|
300
|
+
* always keep/normalize their own email.
|
|
301
|
+
* @param {string} email
|
|
302
|
+
* @param {string|number} memberId - The member requesting the change
|
|
303
|
+
* @returns {Promise<boolean>}
|
|
304
|
+
*/
|
|
253
305
|
async function isEmailAlreadyUsed(email, memberId) {
|
|
254
306
|
const member = await getMemberByContactEmail(email);
|
|
255
|
-
return member !== null && member.memberId !== memberId;
|
|
307
|
+
return member !== null && String(member.memberId) !== String(memberId);
|
|
256
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Finds the member that owns an email (in either the login or contact-form field),
|
|
311
|
+
* matching case-insensitively. Emails are normalized (lowercased + trimmed) on write and
|
|
312
|
+
* backfilled by the email-normalization migration, so stored values are canonical: a single
|
|
313
|
+
* `.eq` against the normalized email is an exact, case-insensitive lookup.
|
|
314
|
+
* Throws if two DIFFERENT members share the email (a data-integrity violation).
|
|
315
|
+
* @param {string} email
|
|
316
|
+
* @returns {Promise<Object|null>}
|
|
317
|
+
*/
|
|
257
318
|
async function getMemberByContactEmail(email) {
|
|
319
|
+
const normalized = normalizeEmail(email);
|
|
320
|
+
if (!normalized) return null;
|
|
321
|
+
|
|
258
322
|
const members = await wixData
|
|
259
323
|
.query(COLLECTIONS.MEMBERS_DATA)
|
|
260
|
-
.eq('contactFormEmail',
|
|
261
|
-
.or(wixData.query(COLLECTIONS.MEMBERS_DATA).eq('email',
|
|
324
|
+
.eq('contactFormEmail', normalized)
|
|
325
|
+
.or(wixData.query(COLLECTIONS.MEMBERS_DATA).eq('email', normalized))
|
|
262
326
|
.limit(2)
|
|
263
327
|
.find()
|
|
264
328
|
.then(res => res.items);
|
|
329
|
+
|
|
265
330
|
if (members.length > 1) {
|
|
266
331
|
throw new Error(
|
|
267
332
|
`[getMemberByContactEmail] Multiple members found with same loginemail or contactFormEmail ${email} membersIds are : [${members.map(member => member.memberId).join(', ')}]`
|
|
@@ -451,6 +516,29 @@ const getAllMembersWithoutContactFormEmail = async () => {
|
|
|
451
516
|
}
|
|
452
517
|
};
|
|
453
518
|
|
|
519
|
+
/**
|
|
520
|
+
* Gets all members whose email or contactFormEmail is stored with non-canonical casing
|
|
521
|
+
* (or surrounding whitespace) and therefore needs the normalization backfill.
|
|
522
|
+
* Wix Data cannot compare a field to its own lowercase form, so we fetch members that have
|
|
523
|
+
* an email set and filter in memory.
|
|
524
|
+
* @returns {Promise<Array>} - Array of member data
|
|
525
|
+
*/
|
|
526
|
+
const getAllMembersNeedingEmailNormalization = async () => {
|
|
527
|
+
try {
|
|
528
|
+
const membersQuery = wixData
|
|
529
|
+
.query(COLLECTIONS.MEMBERS_DATA)
|
|
530
|
+
.isNotEmpty('email')
|
|
531
|
+
.or(wixData.query(COLLECTIONS.MEMBERS_DATA).isNotEmpty('contactFormEmail'))
|
|
532
|
+
.limit(1000);
|
|
533
|
+
|
|
534
|
+
const allItems = await queryAllItems(membersQuery);
|
|
535
|
+
return allItems.filter(memberNeedsEmailNormalization);
|
|
536
|
+
} catch (error) {
|
|
537
|
+
console.error('Error getting members needing email normalization:', error);
|
|
538
|
+
throw new Error(`Failed to get members needing email normalization: ${error.message}`);
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
454
542
|
/* Gets all updated login emails from the updated emails database
|
|
455
543
|
* @returns {Promise<Array>} - Array of updated email data
|
|
456
544
|
*/
|
|
@@ -488,9 +576,13 @@ const getMembersByIds = async memberIds => {
|
|
|
488
576
|
|
|
489
577
|
const getMemberByEmail = async email => {
|
|
490
578
|
try {
|
|
579
|
+
// Login emails are normalized on write, so match against the normalized value (see
|
|
580
|
+
// getMemberByContactEmail) — an exact `.eq` is a case-insensitive lookup.
|
|
581
|
+
const normalized = normalizeEmail(email);
|
|
582
|
+
if (!normalized) return null;
|
|
491
583
|
const members = await wixData
|
|
492
584
|
.query(COLLECTIONS.MEMBERS_DATA)
|
|
493
|
-
.eq('email',
|
|
585
|
+
.eq('email', normalized)
|
|
494
586
|
.limit(2)
|
|
495
587
|
.find()
|
|
496
588
|
.then(res => res.items);
|
|
@@ -638,6 +730,8 @@ module.exports = {
|
|
|
638
730
|
getAllMembersWithExternalImages,
|
|
639
731
|
getMembersWithWixUrl,
|
|
640
732
|
getAllMembersWithoutContactFormEmail,
|
|
733
|
+
getAllMembersNeedingEmailNormalization,
|
|
734
|
+
memberNeedsEmailNormalization,
|
|
641
735
|
getAllUpdatedLoginEmails,
|
|
642
736
|
getMembersByIds,
|
|
643
737
|
getMemberByEmail,
|
package/backend/tasks/consts.js
CHANGED
|
@@ -22,6 +22,8 @@ const TASKS_NAMES = {
|
|
|
22
22
|
fixPrimaryAddressChunk: 'fixPrimaryAddressChunk',
|
|
23
23
|
scheduleFixUrlsWithSpaces: 'scheduleFixUrlsWithSpaces',
|
|
24
24
|
fixUrlsWithSpacesChunk: 'fixUrlsWithSpacesChunk',
|
|
25
|
+
scheduleNormalizeMemberEmails: 'scheduleNormalizeMemberEmails',
|
|
26
|
+
normalizeMemberEmailsChunk: 'normalizeMemberEmailsChunk',
|
|
25
27
|
dailyPullExecutionCheck: 'dailyPullExecutionCheck',
|
|
26
28
|
};
|
|
27
29
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const { taskManager } = require('psdev-task-manager');
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
bulkSaveMembers,
|
|
5
|
+
getMembersByIds,
|
|
6
|
+
getAllMembersNeedingEmailNormalization,
|
|
7
|
+
memberNeedsEmailNormalization,
|
|
8
|
+
} = require('../members-data-methods');
|
|
9
|
+
const { chunkArray } = require('../utils');
|
|
10
|
+
|
|
11
|
+
const { TASKS_NAMES } = require('./consts');
|
|
12
|
+
|
|
13
|
+
const CHUNK_SIZE = 1000;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* One-off backfill: schedules tasks to normalize (lowercase + trim) the email and
|
|
17
|
+
* contactFormEmail fields of existing members so stored values match the normalize-on-write
|
|
18
|
+
* behavior, keeping case-insensitive uniqueness lookups reliable.
|
|
19
|
+
*/
|
|
20
|
+
async function scheduleNormalizeMemberEmails() {
|
|
21
|
+
console.log('=== Scheduling Member Email Normalization ===');
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const members = await getAllMembersNeedingEmailNormalization();
|
|
25
|
+
console.log(`Fetched ${members.length} members needing email normalization`);
|
|
26
|
+
|
|
27
|
+
const memberIds = [
|
|
28
|
+
...new Set(
|
|
29
|
+
members
|
|
30
|
+
.map(member => Number(member.memberId))
|
|
31
|
+
.filter(memberId => Number.isFinite(memberId) && memberId > 0)
|
|
32
|
+
),
|
|
33
|
+
];
|
|
34
|
+
console.log(`Members to normalize: ${memberIds.length}`);
|
|
35
|
+
|
|
36
|
+
if (memberIds.length === 0) {
|
|
37
|
+
console.log('No members need email normalization');
|
|
38
|
+
return {
|
|
39
|
+
success: true,
|
|
40
|
+
message: 'No members need email normalization',
|
|
41
|
+
totalMembers: 0,
|
|
42
|
+
tasksScheduled: 0,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const chunks = chunkArray(memberIds, CHUNK_SIZE);
|
|
47
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
48
|
+
const chunk = chunks[i];
|
|
49
|
+
const task = {
|
|
50
|
+
name: TASKS_NAMES.normalizeMemberEmailsChunk,
|
|
51
|
+
data: {
|
|
52
|
+
memberIds: chunk,
|
|
53
|
+
chunkIndex: i,
|
|
54
|
+
totalChunks: chunks.length,
|
|
55
|
+
},
|
|
56
|
+
type: 'scheduled',
|
|
57
|
+
};
|
|
58
|
+
await taskManager().schedule(task);
|
|
59
|
+
console.log(`Scheduled task ${i + 1}/${chunks.length} (${chunk.length} members)`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const result = {
|
|
63
|
+
success: true,
|
|
64
|
+
message: `Scheduled ${chunks.length} tasks for ${memberIds.length} members`,
|
|
65
|
+
totalMembers: memberIds.length,
|
|
66
|
+
tasksScheduled: chunks.length,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
console.log('=== Scheduling Complete ===');
|
|
70
|
+
console.log(JSON.stringify(result, null, 2));
|
|
71
|
+
|
|
72
|
+
return result;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error('Error scheduling member email normalization:', error);
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Processes a chunk of members, normalizing their email fields.
|
|
81
|
+
* bulkSaveMembers normalizes on write, so we only need to select the members that still
|
|
82
|
+
* have non-canonical values and save them.
|
|
83
|
+
*/
|
|
84
|
+
async function normalizeMemberEmailsChunk(data) {
|
|
85
|
+
const { memberIds, chunkIndex, totalChunks } = data;
|
|
86
|
+
console.log(
|
|
87
|
+
`Processing email normalization chunk ${chunkIndex + 1}/${totalChunks} (${memberIds.length} members)`
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const result = {
|
|
91
|
+
successful: 0,
|
|
92
|
+
failed: 0,
|
|
93
|
+
skipped: 0,
|
|
94
|
+
errors: [],
|
|
95
|
+
failedIds: [],
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const members = await getMembersByIds(memberIds);
|
|
100
|
+
console.log(`Loaded ${members.length} members for this chunk`);
|
|
101
|
+
|
|
102
|
+
const membersToUpdate = members.filter(memberNeedsEmailNormalization);
|
|
103
|
+
result.skipped = members.length - membersToUpdate.length;
|
|
104
|
+
|
|
105
|
+
if (membersToUpdate.length === 0) {
|
|
106
|
+
console.log('No members need updating in this batch');
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
await bulkSaveMembers(membersToUpdate);
|
|
112
|
+
result.successful += membersToUpdate.length;
|
|
113
|
+
console.log(`✅ Successfully normalized ${membersToUpdate.length} members`);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error('❌ Error bulk saving members:', error);
|
|
116
|
+
result.failed += membersToUpdate.length;
|
|
117
|
+
result.failedIds.push(...membersToUpdate.map(member => member.memberId));
|
|
118
|
+
result.errors.push({
|
|
119
|
+
error: error.message,
|
|
120
|
+
memberCount: membersToUpdate.length,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return result;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error(`Error processing email normalization chunk ${chunkIndex}:`, error);
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = {
|
|
132
|
+
scheduleNormalizeMemberEmails,
|
|
133
|
+
normalizeMemberEmailsChunk,
|
|
134
|
+
};
|
|
@@ -10,6 +10,10 @@ const {
|
|
|
10
10
|
} = require('./address-primary-methods');
|
|
11
11
|
const { TASKS_NAMES } = require('./consts');
|
|
12
12
|
const { dailyPullExecutionCheck } = require('./daily-pull-check-methods');
|
|
13
|
+
const {
|
|
14
|
+
scheduleNormalizeMemberEmails,
|
|
15
|
+
normalizeMemberEmailsChunk,
|
|
16
|
+
} = require('./email-normalize-methods');
|
|
13
17
|
const {
|
|
14
18
|
scheduleTaskForEmptyAboutYouMembers,
|
|
15
19
|
convertAboutYouHtmlToRichContent,
|
|
@@ -203,6 +207,20 @@ const TASKS = {
|
|
|
203
207
|
shouldSkipCheck: () => false,
|
|
204
208
|
estimatedDurationSec: 80,
|
|
205
209
|
},
|
|
210
|
+
[TASKS_NAMES.scheduleNormalizeMemberEmails]: {
|
|
211
|
+
name: TASKS_NAMES.scheduleNormalizeMemberEmails,
|
|
212
|
+
getIdentifier: () => 'SHOULD_NEVER_SKIP',
|
|
213
|
+
process: scheduleNormalizeMemberEmails,
|
|
214
|
+
shouldSkipCheck: () => false,
|
|
215
|
+
estimatedDurationSec: 80,
|
|
216
|
+
},
|
|
217
|
+
[TASKS_NAMES.normalizeMemberEmailsChunk]: {
|
|
218
|
+
name: TASKS_NAMES.normalizeMemberEmailsChunk,
|
|
219
|
+
getIdentifier: task => task.data,
|
|
220
|
+
process: normalizeMemberEmailsChunk,
|
|
221
|
+
shouldSkipCheck: () => false,
|
|
222
|
+
estimatedDurationSec: 80,
|
|
223
|
+
},
|
|
206
224
|
[TASKS_NAMES.dailyPullExecutionCheck]: {
|
|
207
225
|
name: TASKS_NAMES.dailyPullExecutionCheck,
|
|
208
226
|
getIdentifier: task => task.data,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
const { taskManager } = require('psdev-task-manager');
|
|
2
2
|
|
|
3
3
|
const { COLLECTIONS } = require('../../public/consts');
|
|
4
|
-
const { COMPILED_FILTERS_FIELDS, CONFIG_KEYS } = require('../consts');
|
|
4
|
+
const { COMPILED_FILTERS_FIELDS, CONFIG_KEYS, LOGIN_EMAIL_SYNC_STATUS } = require('../consts');
|
|
5
|
+
const { changeWixMembersEmails, summarizeLoginEmailOutcomes } = require('../daily-pull/utils');
|
|
5
6
|
const { wixData } = require('../elevated-modules');
|
|
6
|
-
const { updateWixMemberLoginEmail } = require('../members-area-methods');
|
|
7
7
|
const {
|
|
8
8
|
getAllEmptyAboutYouMembers,
|
|
9
9
|
getAllMembersWithExternalImages,
|
|
@@ -481,6 +481,7 @@ const syncMemberLoginEmails = async data => {
|
|
|
481
481
|
membersToUpdate.push({
|
|
482
482
|
...member,
|
|
483
483
|
email: newEmail,
|
|
484
|
+
previousLoginEmail: member.email,
|
|
484
485
|
});
|
|
485
486
|
}
|
|
486
487
|
|
|
@@ -497,14 +498,35 @@ const syncMemberLoginEmails = async data => {
|
|
|
497
498
|
|
|
498
499
|
for (const chunk of updateChunks) {
|
|
499
500
|
try {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
501
|
+
// Change Wix login emails first; only advance the CMS login email for the ones that
|
|
502
|
+
// succeeded. Failures keep their previous email (CMS stays consistent with Wix) and are
|
|
503
|
+
// reported in result.errors for manual handling.
|
|
504
|
+
const outcomes = await changeWixMembersEmails(chunk);
|
|
505
|
+
const { failedMemberIds } = summarizeLoginEmailOutcomes(outcomes);
|
|
506
|
+
const toSave = chunk.map(member => {
|
|
507
|
+
const { previousLoginEmail, ...restMember } = member;
|
|
508
|
+
if (failedMemberIds.has(member.memberId) && previousLoginEmail !== undefined) {
|
|
509
|
+
return { ...restMember, email: previousLoginEmail };
|
|
510
|
+
}
|
|
511
|
+
return restMember;
|
|
512
|
+
});
|
|
513
|
+
await bulkSaveMembers(toSave);
|
|
514
|
+
|
|
515
|
+
outcomes.forEach(outcome => {
|
|
516
|
+
if (outcome.status === LOGIN_EMAIL_SYNC_STATUS.UPDATED) {
|
|
517
|
+
result.successful++;
|
|
518
|
+
} else if (outcome.status === LOGIN_EMAIL_SYNC_STATUS.FAILED) {
|
|
519
|
+
result.failed++;
|
|
520
|
+
result.errors.push({
|
|
521
|
+
memberId: outcome.memberId,
|
|
522
|
+
wixMemberId: outcome.wixMemberId,
|
|
523
|
+
desiredEmail: outcome.desiredEmail,
|
|
524
|
+
error: outcome.error,
|
|
525
|
+
});
|
|
526
|
+
} else {
|
|
527
|
+
result.skipped++;
|
|
528
|
+
}
|
|
529
|
+
});
|
|
508
530
|
} catch (error) {
|
|
509
531
|
console.error(`❌ Error updating chunk ${chunkIndex}:`, error);
|
|
510
532
|
result.failed += chunk.length;
|
|
@@ -515,14 +537,8 @@ const syncMemberLoginEmails = async data => {
|
|
|
515
537
|
});
|
|
516
538
|
}
|
|
517
539
|
}
|
|
518
|
-
// Log comprehensive results including Wix member updates
|
|
519
|
-
const wixStats = result.wixMemberUpdates || { successful: 0, failed: 0 };
|
|
520
|
-
console.log(`Login Emails sync task completed:`);
|
|
521
|
-
console.log(
|
|
522
|
-
` - Member data updates: ${result.successful} successful, ${result.failed} failed, ${result.skipped} skipped`
|
|
523
|
-
);
|
|
524
540
|
console.log(
|
|
525
|
-
`
|
|
541
|
+
`Login Emails sync task completed: ${result.successful} synced, ${result.failed} failed, ${result.skipped} skipped`
|
|
526
542
|
);
|
|
527
543
|
|
|
528
544
|
return result;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "abmp-npm",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.70",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"check-cycles": "madge --circular .",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"@wix/automations": "^1.0.261",
|
|
36
36
|
"@wix/crm": "^1.0.1061",
|
|
37
37
|
"@wix/data": "^1.0.349",
|
|
38
|
-
"@wix/essentials": "^0.
|
|
39
|
-
"@wix/identity": "^1.0.
|
|
38
|
+
"@wix/essentials": "^1.0.7",
|
|
39
|
+
"@wix/identity": "^1.0.195",
|
|
40
40
|
"@wix/media": "^1.0.213",
|
|
41
41
|
"@wix/members": "^1.0.365",
|
|
42
42
|
"@wix/secrets": "^1.0.62",
|
|
@@ -195,6 +195,28 @@ function normalizeExternalUrl(url) {
|
|
|
195
195
|
return `https://${trimmed}`;
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Normalizes an email for comparison (lowercased + trimmed).
|
|
200
|
+
* Wix CRM treats emails as case-insensitive, so comparisons must too.
|
|
201
|
+
* @param {string} email
|
|
202
|
+
* @returns {string}
|
|
203
|
+
*/
|
|
204
|
+
function normalizeEmail(email) {
|
|
205
|
+
return typeof email === 'string' ? email.trim().toLowerCase() : '';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Case-insensitive email equality. Two empty/missing emails are not considered a match.
|
|
210
|
+
* @param {string} a
|
|
211
|
+
* @param {string} b
|
|
212
|
+
* @returns {boolean}
|
|
213
|
+
*/
|
|
214
|
+
function emailsMatch(a, b) {
|
|
215
|
+
const normalizedA = normalizeEmail(a);
|
|
216
|
+
const normalizedB = normalizeEmail(b);
|
|
217
|
+
return Boolean(normalizedA) && normalizedA === normalizedB;
|
|
218
|
+
}
|
|
219
|
+
|
|
198
220
|
module.exports = {
|
|
199
221
|
checkAddressIsVisible,
|
|
200
222
|
formatPracticeAreasForDisplay,
|
|
@@ -208,4 +230,6 @@ module.exports = {
|
|
|
208
230
|
formatAddress,
|
|
209
231
|
isWixHostedImage,
|
|
210
232
|
normalizeExternalUrl,
|
|
233
|
+
normalizeEmail,
|
|
234
|
+
emailsMatch,
|
|
211
235
|
};
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
const { loginQAMember } = require('./qa-login-methods');
|
|
2
|
-
const { authenticateSSOToken } = require('./sso-methods');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Creates login methods with injected generateSessionToken dependency
|
|
6
|
-
* @param {Function} generateSessionToken - The Velo generateSessionToken function to inject
|
|
7
|
-
* @returns {Object} Object containing loginQAMember and authenticateSSOToken methods
|
|
8
|
-
*/
|
|
9
|
-
const createLoginMethods = generateSessionToken => {
|
|
10
|
-
//There is no generateSessionToken SDK version, and the signOn of @wix/identity returns 403 error regardless that the permissions are valid
|
|
11
|
-
//Therefore, as a workaround we need to inject the Velo version of generateSessionToken to the login methods.
|
|
12
|
-
const injectGenerateSessionTokenToMethod =
|
|
13
|
-
method =>
|
|
14
|
-
async (...args) =>
|
|
15
|
-
await method(...args, generateSessionToken);
|
|
16
|
-
return {
|
|
17
|
-
loginQAMember: injectGenerateSessionTokenToMethod(loginQAMember),
|
|
18
|
-
authenticateSSOToken: injectGenerateSessionTokenToMethod(authenticateSSOToken),
|
|
19
|
-
};
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
module.exports = {
|
|
23
|
-
createLoginMethods,
|
|
24
|
-
};
|