abmp-npm 10.3.7 → 10.3.9

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.
@@ -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
+ });
@@ -0,0 +1,129 @@
1
+ jest.mock('../elevated-modules', () => ({
2
+ wixData: { query: jest.fn() },
3
+ }));
4
+
5
+ const { wixData } = require('../elevated-modules');
6
+ const { findMembersByIds } = require('../members-data-methods');
7
+ const { withTransientErrorRetry, isTransientNetworkError } = require('../utils');
8
+
9
+ const makeQueryResult = items => ({ items, hasNext: () => false });
10
+
11
+ const mockQueryReturning = find => {
12
+ const query = {
13
+ hasSome: jest.fn().mockReturnThis(),
14
+ limit: jest.fn().mockReturnThis(),
15
+ find,
16
+ };
17
+ wixData.query.mockReturnValue(query);
18
+ return query;
19
+ };
20
+
21
+ describe('isTransientNetworkError', () => {
22
+ test('matches undici "fetch failed" and common network error codes', () => {
23
+ expect(isTransientNetworkError(new Error('fetch failed'))).toBe(true);
24
+ expect(isTransientNetworkError({ message: 'request failed', code: 'ECONNRESET' })).toBe(true);
25
+ expect(
26
+ isTransientNetworkError({ message: 'fetch failed', cause: { code: 'UND_ERR_SOCKET' } })
27
+ ).toBe(true);
28
+ });
29
+
30
+ test('does not match application errors', () => {
31
+ expect(isTransientNetworkError(new Error('Multiple members found with memberId 1'))).toBe(
32
+ false
33
+ );
34
+ expect(isTransientNetworkError(new Error('WDE0025: validation failed'))).toBe(false);
35
+ });
36
+ });
37
+
38
+ describe('withTransientErrorRetry', () => {
39
+ test('retries transient failures and resolves with the eventual result', async () => {
40
+ const operation = jest
41
+ .fn()
42
+ .mockRejectedValueOnce(new Error('fetch failed'))
43
+ .mockResolvedValueOnce('ok');
44
+
45
+ await expect(withTransientErrorRetry(operation, { baseDelayMs: 1 })).resolves.toBe('ok');
46
+ expect(operation).toHaveBeenCalledTimes(2);
47
+ });
48
+
49
+ test('rethrows immediately on non-transient errors', async () => {
50
+ const operation = jest.fn().mockRejectedValue(new Error('validation failed'));
51
+
52
+ await expect(withTransientErrorRetry(operation, { baseDelayMs: 1 })).rejects.toThrow(
53
+ 'validation failed'
54
+ );
55
+ expect(operation).toHaveBeenCalledTimes(1);
56
+ });
57
+
58
+ test('gives up after the configured number of retries', async () => {
59
+ const operation = jest.fn().mockRejectedValue(new Error('fetch failed'));
60
+
61
+ await expect(
62
+ withTransientErrorRetry(operation, { retries: 2, baseDelayMs: 1 })
63
+ ).rejects.toThrow('fetch failed');
64
+ expect(operation).toHaveBeenCalledTimes(3);
65
+ });
66
+ });
67
+
68
+ describe('findMembersByIds', () => {
69
+ beforeEach(() => {
70
+ wixData.query.mockReset();
71
+ });
72
+
73
+ test('returns a map of String(memberId) to member record', async () => {
74
+ mockQueryReturning(
75
+ jest.fn().mockResolvedValue(
76
+ makeQueryResult([
77
+ { _id: 'a', memberId: 1 },
78
+ { _id: 'b', memberId: 2 },
79
+ ])
80
+ )
81
+ );
82
+
83
+ const membersById = await findMembersByIds([1, 2, 3]);
84
+
85
+ expect(membersById.get('1')).toEqual({ _id: 'a', memberId: 1 });
86
+ expect(membersById.get('2')).toEqual({ _id: 'b', memberId: 2 });
87
+ expect(membersById.has('3')).toBe(false);
88
+ });
89
+
90
+ test('deduplicates input IDs and skips null/undefined before querying', async () => {
91
+ const query = mockQueryReturning(jest.fn().mockResolvedValue(makeQueryResult([])));
92
+
93
+ await findMembersByIds([1, 1, null, undefined, 2]);
94
+
95
+ expect(query.hasSome).toHaveBeenCalledWith('memberId', [1, 2]);
96
+ });
97
+
98
+ test('returns an empty map without querying when there are no IDs', async () => {
99
+ const membersById = await findMembersByIds([]);
100
+
101
+ expect(membersById.size).toBe(0);
102
+ expect(wixData.query).not.toHaveBeenCalled();
103
+ });
104
+
105
+ test('chunks large ID lists into multiple queries', async () => {
106
+ const query = mockQueryReturning(jest.fn().mockResolvedValue(makeQueryResult([])));
107
+ const ids = Array.from({ length: 250 }, (_, i) => i + 1);
108
+
109
+ await findMembersByIds(ids);
110
+
111
+ expect(query.hasSome).toHaveBeenCalledTimes(3);
112
+ expect(query.hasSome.mock.calls.map(call => call[1].length)).toEqual([100, 100, 50]);
113
+ });
114
+
115
+ test('throws when multiple records share the same memberId', async () => {
116
+ mockQueryReturning(
117
+ jest.fn().mockResolvedValue(
118
+ makeQueryResult([
119
+ { _id: 'a', memberId: 1 },
120
+ { _id: 'b', memberId: 1 },
121
+ ])
122
+ )
123
+ );
124
+
125
+ await expect(findMembersByIds([1])).rejects.toThrow(
126
+ 'Multiple members found with memberId(s): [1]'
127
+ );
128
+ });
129
+ });
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',
@@ -1,8 +1,13 @@
1
- const { bulkSaveMembers, getMemberBySlug } = require('../members-data-methods');
1
+ const { bulkSaveMembers, getMemberBySlug, findMembersByIds } = require('../members-data-methods');
2
2
  const { extractUrlCounter } = require('../utils');
3
3
 
4
4
  const { generateUpdatedMemberData } = require('./process-member-methods');
5
- const { changeWixMembersEmails, incrementUrlCounter, extractBaseUrl } = require('./utils');
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
@@ -127,10 +132,18 @@ const bulkProcessAndSaveMemberData = async ({ memberDataList, currentPageNumber
127
132
  const startTime = Date.now();
128
133
 
129
134
  try {
135
+ // Prefetch all existing members for this page in a few chunked queries instead of
136
+ // one query per member — thousands of parallel lookups made the whole page fail
137
+ // whenever a single one hit a transient network error.
138
+ const existingMembersById = await findMembersByIds(
139
+ memberDataList.map(memberData => memberData?.memberid)
140
+ );
141
+
130
142
  const processedMemberDataPromises = memberDataList.map(memberData =>
131
143
  generateUpdatedMemberData({
132
144
  inputMemberData: memberData,
133
145
  currentPageNumber,
146
+ existingDbMember: existingMembersById.get(String(memberData?.memberid)) ?? null,
134
147
  })
135
148
  );
136
149
 
@@ -151,28 +164,55 @@ const bulkProcessAndSaveMemberData = async ({ memberDataList, currentPageNumber
151
164
  // Ensure unique URLs within the batch to prevent duplicates (also checks DB for cross-page conflicts)
152
165
  const uniqueUrlsNewToDBMembersList = await ensureUniqueUrlsInBatch(newMembers);
153
166
  const uniqueUrlsMembersData = [...uniqueUrlsNewToDBMembersList, ...existingMembers];
154
- const toChangeWixMembersEmails = [];
167
+
168
+ // Change Wix login emails FIRST so we know which succeeded before saving the CMS records.
169
+ const toChangeWixMembersEmails = uniqueUrlsMembersData.filter(
170
+ member => member.wixMemberId && member.isLoginEmailChanged
171
+ );
172
+ let failedLoginEmailIds = new Set();
173
+ let loginEmailFailures = [];
174
+ if (toChangeWixMembersEmails.length > 0) {
175
+ const outcomes = await changeWixMembersEmails(toChangeWixMembersEmails);
176
+ ({ failedMemberIds: failedLoginEmailIds, failures: loginEmailFailures } =
177
+ summarizeLoginEmailOutcomes(outcomes));
178
+ }
179
+
155
180
  const toSaveMembersData = uniqueUrlsMembersData.map(member => {
156
- const { isLoginEmailChanged, isNewToDb: _isNewToDb, ...restMemberData } = member;
157
- if (member.wixMemberId && isLoginEmailChanged) {
158
- toChangeWixMembersEmails.push(member);
181
+ // Transient flags only drive the sync above; never persist them.
182
+ const {
183
+ isLoginEmailChanged: _isLoginEmailChanged,
184
+ isNewToDb: _isNewToDb,
185
+ previousLoginEmail,
186
+ ...restMemberData
187
+ } = member;
188
+ // If the Wix login-email change failed, keep the CMS login email unchanged (revert to the
189
+ // previous value) so the CMS stays consistent with Wix; the failure is reported below for
190
+ // manual handling.
191
+ if (failedLoginEmailIds.has(member.memberId) && previousLoginEmail !== undefined) {
192
+ return { ...restMemberData, email: previousLoginEmail };
159
193
  }
160
- 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
194
+ return restMemberData;
161
195
  });
162
196
  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
197
  const totalFailed = memberDataList.length - validMemberData.length;
168
198
  const processingTime = Date.now() - startTime;
169
199
 
200
+ if (loginEmailFailures.length > 0) {
201
+ console.error(
202
+ `[loginEmailSync] ${loginEmailFailures.length} login-email change(s) failed on page ${currentPageNumber}; CMS login email left unchanged for memberIds: [${loginEmailFailures
203
+ .map(failure => failure.memberId)
204
+ .join(', ')}]`
205
+ );
206
+ }
207
+
170
208
  return {
171
209
  ...saveResult,
172
210
  totalProcessed: memberDataList.length,
173
211
  totalSaved: validMemberData.length,
174
212
  totalFailed: totalFailed,
175
213
  processingTime: processingTime,
214
+ loginEmailFailedCount: loginEmailFailures.length,
215
+ loginEmailFailures,
176
216
  };
177
217
  } catch (error) {
178
218
  throw new Error(`Bulk operation failed: ${error.message}`);
@@ -58,16 +58,26 @@ const ensureUniqueUrl = async ({ url, memberId, fullName }) => {
58
58
  * @param {Object} options - The options object
59
59
  * @param {Object} options.inputMemberData - Raw member data from API
60
60
  * @param {number} options.currentPageNumber - Current page number being processed
61
+ * @param {Object|null} [options.existingDbMember] - Prefetched DB member (null if none exists).
62
+ * Pass it when processing members in bulk to avoid one DB query per member; when omitted,
63
+ * the member is looked up individually.
61
64
  * @returns {Promise<Object|null>} - Complete updated member data or null if validation fails
62
65
  */
63
- async function generateUpdatedMemberData({ inputMemberData, currentPageNumber }) {
66
+ async function generateUpdatedMemberData({
67
+ inputMemberData,
68
+ currentPageNumber,
69
+ existingDbMember: prefetchedDbMember,
70
+ }) {
64
71
  if (!validateCoreMemberData(inputMemberData)) {
65
72
  throw new Error(
66
73
  'Invalid member data: memberid, email (valid string), and memberships (array) are required'
67
74
  );
68
75
  }
69
76
 
70
- const existingDbMember = await findMemberById(inputMemberData.memberid);
77
+ const existingDbMember =
78
+ prefetchedDbMember === undefined
79
+ ? await findMemberById(inputMemberData.memberid)
80
+ : prefetchedDbMember;
71
81
 
72
82
  const updatedMemberData = await createCoreMemberData(
73
83
  inputMemberData,
@@ -165,10 +175,12 @@ async function createCoreMemberData(inputMemberData, existingDbMember, currentPa
165
175
  newEmail &&
166
176
  existingDbMember.email !== newEmail;
167
177
  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
178
+ // 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.
179
+ // previousLoginEmail lets the caller keep the old email if the Wix login-email change fails.
169
180
  return {
170
181
  email: newEmail,
171
182
  isLoginEmailChanged: true,
183
+ previousLoginEmail: existingDbMember.email,
172
184
  };
173
185
  }
174
186
  //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
- `Changing login emails for ${toChangeWixMembersEmails.length} members with ids: [${toChangeWixMembersEmails.map(member => member.memberId).join(', ')}]`
19
+ `[loginEmailSync] changing login emails for ${toChangeWixMembersEmails.length} members with ids: [${toChangeWixMembersEmails.map(member => member.memberId).join(', ')}]`
13
20
  );
14
- return await Promise.all(
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,
@@ -1,3 +1,5 @@
1
+ const axios = require('axios');
2
+
1
3
  const { COLLECTIONS } = require('../../public/consts');
2
4
  const { clearCollection } = require('../cms-data-methods');
3
5
  const { CONFIG_KEYS } = require('../consts');
@@ -10,13 +12,9 @@ const getInterests = async () => {
10
12
  getSiteConfigs(CONFIG_KEYS.INTERESTS_API_URL),
11
13
  getHeaders(),
12
14
  ]);
13
- const fetchOptions = {
14
- method: 'get',
15
- headers: headers,
16
- };
17
15
  try {
18
- const response = await fetch(url, fetchOptions);
19
- return await response.json();
16
+ const response = await axios.get(url, { headers });
17
+ return response.data;
20
18
  } catch (e) {
21
19
  console.error('Error getting interests:', e);
22
20
  throw e;
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
+ };
@@ -1,7 +1,8 @@
1
- const { createLoginMethods } = require('./login-methods-factory');
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
- createLoginMethods,
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 }, generateSessionToken) => {
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 generateSessionToken(memberData.email, qaSecret);
40
+ const token = await generateMemberSessionToken(memberData.email);
40
41
  return {
41
42
  success: true,
42
43
  token,
@@ -1,5 +1,6 @@
1
1
  const { createHmac } = require('crypto');
2
2
 
3
+ const axios = require('axios');
3
4
  const { decode } = require('jwt-js-decode');
4
5
 
5
6
  const { CONFIG_KEYS, SSO_TOKEN_AUTH_API_URL } = require('../consts');
@@ -14,6 +15,8 @@ const {
14
15
  getSecret,
15
16
  } = require('../utils');
16
17
 
18
+ const { generateMemberSessionToken } = require('./generate-member-session-token');
19
+
17
20
  /**
18
21
  * Validates member token and retrieves member data
19
22
  * @param {string} memberIdInput - The member ID to validate
@@ -91,17 +94,16 @@ async function checkAndFetchSSO(token) {
91
94
  const SSO_TOKEN_AUTH_API_KEY = await getSecret('SSO_TOKEN_AUTH_API_KEY');
92
95
  const signature = createHmac('sha256', SSO_TOKEN_AUTH_API_KEY).update(token).digest('hex');
93
96
  const professionalassistcorpUrl = `${SSO_TOKEN_AUTH_API_URL}/eweb/SSOToken.ashx?token=${token}&Partner=Wix&Signature=${signature}`;
94
- const options = {
95
- method: 'get',
96
- };
97
97
  try {
98
- const httpResponse = await fetch(professionalassistcorpUrl, options);
98
+ const httpResponse = await axios.get(professionalassistcorpUrl, {
99
+ transformResponse: [d => d],
100
+ validateStatus: () => true,
101
+ });
99
102
  console.log('httpResponse status', httpResponse.status);
100
- if (!httpResponse.ok) {
103
+ if (httpResponse.status < 200 || httpResponse.status >= 300) {
101
104
  throw new Error('Fetch did not succeed with status: ' + httpResponse.status);
102
105
  }
103
- const responseToken = await httpResponse.text();
104
- return responseToken;
106
+ return httpResponse.data;
105
107
  } catch (error) {
106
108
  console.error('Error in checkAndFetchSSO', error);
107
109
  return null;
@@ -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 }, generateSessionToken) => {
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 generateSessionToken(memberData.email);
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
- const contactEmailDiffersFromLogin = loginEmail !== newContactEmail;
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) {