abmp-npm 10.3.8 → 10.3.10
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__/daily-pull-execution-check.test.js +124 -0
- package/backend/__tests__/login-email-sync.test.js +49 -0
- package/backend/__tests__/transient-retry-and-bulk-lookup.test.js +129 -0
- package/backend/consts.js +10 -0
- package/backend/contacts-methods.js +3 -0
- package/backend/daily-pull/bulk-process-methods.js +51 -11
- package/backend/daily-pull/process-member-methods.js +15 -3
- package/backend/daily-pull/schedule-methods.js +41 -0
- package/backend/daily-pull/utils.js +41 -3
- package/backend/jobs.js +46 -18
- package/backend/member-contact-orchestration.js +6 -1
- package/backend/members-area-methods.js +33 -34
- package/backend/members-data-methods.js +152 -13
- package/backend/tasks/consts.js +4 -0
- package/backend/tasks/daily-pull-check-methods.js +38 -18
- package/backend/tasks/email-normalize-methods.js +134 -0
- package/backend/tasks/hide-addresses-methods.js +159 -0
- package/backend/tasks/tasks-configs.js +36 -0
- package/backend/tasks/tasks-process-methods.js +33 -17
- package/backend/utils.js +42 -0
- package/package.json +2 -1
- package/public/Utils/sharedUtils.js +24 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
jest.mock('../elevated-modules', () => ({
|
|
2
|
+
wixData: { query: jest.fn() },
|
|
3
|
+
}));
|
|
4
|
+
jest.mock('psdev-task-manager', () => ({ taskManager: jest.fn() }));
|
|
5
|
+
|
|
6
|
+
const { taskManager } = require('psdev-task-manager');
|
|
7
|
+
|
|
8
|
+
const { buildDailyPullTasks } = require('../daily-pull/schedule-methods');
|
|
9
|
+
const { wixData } = require('../elevated-modules');
|
|
10
|
+
const { TASKS_NAMES } = require('../tasks/consts');
|
|
11
|
+
const { dailyPullExecutionCheck } = require('../tasks/daily-pull-check-methods');
|
|
12
|
+
|
|
13
|
+
const makeQueryResult = items => ({ items, hasNext: () => false });
|
|
14
|
+
|
|
15
|
+
const mockTasksQuery = items => {
|
|
16
|
+
const query = {
|
|
17
|
+
hasSome: jest.fn().mockReturnThis(),
|
|
18
|
+
ge: jest.fn().mockReturnThis(),
|
|
19
|
+
find: jest.fn().mockResolvedValue(makeQueryResult(items)),
|
|
20
|
+
};
|
|
21
|
+
wixData.query.mockReturnValue(query);
|
|
22
|
+
return query;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe('buildDailyPullTasks', () => {
|
|
26
|
+
test('schedules one ScheduleMembersDataPerAction task per action, excluding none by default', () => {
|
|
27
|
+
const tasks = buildDailyPullTasks();
|
|
28
|
+
|
|
29
|
+
expect(tasks.map(task => task.data.action).sort()).toEqual(['drop', 'new', 'update']);
|
|
30
|
+
tasks.forEach(task => {
|
|
31
|
+
expect(task.name).toBe(TASKS_NAMES.ScheduleMembersDataPerAction);
|
|
32
|
+
expect(task.type).toBe('scheduled');
|
|
33
|
+
expect(task.data).toEqual({ action: task.data.action });
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('includes the none action and flag when includeNone is set', () => {
|
|
38
|
+
const tasks = buildDailyPullTasks({ includeNone: true });
|
|
39
|
+
|
|
40
|
+
expect(tasks.map(task => task.data.action).sort()).toEqual(['drop', 'new', 'none', 'update']);
|
|
41
|
+
tasks.forEach(task => expect(task.data.includeNone).toBe(true));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('propagates isTestEnvironment and backupDate to every task', () => {
|
|
45
|
+
const tasks = buildDailyPullTasks({ isTestEnvironment: true, backupDate: '2026-06-01' });
|
|
46
|
+
|
|
47
|
+
tasks.forEach(task => {
|
|
48
|
+
expect(task.data.isTestEnvironment).toBe(true);
|
|
49
|
+
expect(task.data.backupDate).toBe('2026-06-01');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('dailyPullExecutionCheck', () => {
|
|
55
|
+
let scheduleInBulk;
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
wixData.query.mockReset();
|
|
59
|
+
scheduleInBulk = jest.fn().mockResolvedValue(undefined);
|
|
60
|
+
taskManager.mockReturnValue({ scheduleInBulk });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('counts the per-action tasks the cron creates as evidence the pull ran', async () => {
|
|
64
|
+
const query = mockTasksQuery([{ name: TASKS_NAMES.ScheduleMembersDataPerAction }]);
|
|
65
|
+
|
|
66
|
+
const result = await dailyPullExecutionCheck({});
|
|
67
|
+
|
|
68
|
+
expect(query.hasSome).toHaveBeenCalledWith('name', [
|
|
69
|
+
TASKS_NAMES.ScheduleMembersDataPerAction,
|
|
70
|
+
TASKS_NAMES.ScheduleDailyMembersDataSync,
|
|
71
|
+
]);
|
|
72
|
+
expect(result.success).toBe(true);
|
|
73
|
+
expect(result.fallbackScheduled).toBeUndefined();
|
|
74
|
+
expect(scheduleInBulk).not.toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('schedules the per-action fallback with environment flags when no pull is found', async () => {
|
|
78
|
+
mockTasksQuery([]);
|
|
79
|
+
|
|
80
|
+
const result = await dailyPullExecutionCheck({ isTestEnvironment: true, includeNone: true });
|
|
81
|
+
|
|
82
|
+
expect(result.success).toBe(false);
|
|
83
|
+
expect(result.fallbackScheduled).toBe(true);
|
|
84
|
+
expect(scheduleInBulk).toHaveBeenCalledTimes(1);
|
|
85
|
+
const scheduledTasks = scheduleInBulk.mock.calls[0][0];
|
|
86
|
+
expect(scheduledTasks.map(task => task.data.action).sort()).toEqual([
|
|
87
|
+
'drop',
|
|
88
|
+
'new',
|
|
89
|
+
'none',
|
|
90
|
+
'update',
|
|
91
|
+
]);
|
|
92
|
+
scheduledTasks.forEach(task => {
|
|
93
|
+
expect(task.name).toBe(TASKS_NAMES.ScheduleMembersDataPerAction);
|
|
94
|
+
expect(task.data.isTestEnvironment).toBe(true);
|
|
95
|
+
expect(task.data.includeNone).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('fallback defaults to production flags when no task data is provided', async () => {
|
|
100
|
+
mockTasksQuery([]);
|
|
101
|
+
|
|
102
|
+
await dailyPullExecutionCheck(undefined);
|
|
103
|
+
|
|
104
|
+
const scheduledTasks = scheduleInBulk.mock.calls[0][0];
|
|
105
|
+
expect(scheduledTasks.map(task => task.data.action).sort()).toEqual(['drop', 'new', 'update']);
|
|
106
|
+
scheduledTasks.forEach(task => {
|
|
107
|
+
expect(task.data.isTestEnvironment).toBeUndefined();
|
|
108
|
+
expect(task.data.includeNone).toBeUndefined();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('uses the provided lookback window', async () => {
|
|
113
|
+
const query = mockTasksQuery([{ name: TASKS_NAMES.ScheduleDailyMembersDataSync }]);
|
|
114
|
+
const before = Date.now();
|
|
115
|
+
|
|
116
|
+
const result = await dailyPullExecutionCheck({ hoursBack: 12 });
|
|
117
|
+
|
|
118
|
+
const sinceDate = query.ge.mock.calls[0][1];
|
|
119
|
+
expect(query.ge).toHaveBeenCalledWith('_createdDate', expect.any(Date));
|
|
120
|
+
const expectedSince = before - 12 * 60 * 60 * 1000;
|
|
121
|
+
expect(Math.abs(sinceDate.getTime() - expectedSince)).toBeLessThan(5000);
|
|
122
|
+
expect(result.success).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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;
|
|
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({
|
|
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 =
|
|
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
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const { taskManager } = require('psdev-task-manager');
|
|
2
|
+
|
|
3
|
+
const { TASKS_NAMES } = require('../tasks/consts');
|
|
4
|
+
|
|
5
|
+
const { MEMBER_ACTIONS } = require('./consts');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Builds the per-action daily pull tasks. Single source of truth for what a
|
|
9
|
+
* "daily pull" schedules, shared by the cron job and the execution-check
|
|
10
|
+
* fallback so both produce identical tasks (same names, same environment flags).
|
|
11
|
+
* @param {Object} [options]
|
|
12
|
+
* @param {string} [options.backupDate] - Optional backup date (YYYY-MM-DD) to pull from the backup endpoint
|
|
13
|
+
* @param {boolean} [options.isTestEnvironment=false] - Whether to pull from the test PAC API
|
|
14
|
+
* @param {boolean} [options.includeNone=false] - Whether to also sync the NONE action
|
|
15
|
+
* @returns {Array<Object>} - Task definitions for taskManager().scheduleInBulk
|
|
16
|
+
*/
|
|
17
|
+
const buildDailyPullTasks = ({ backupDate, isTestEnvironment, includeNone } = {}) => {
|
|
18
|
+
const actionsToSync = includeNone
|
|
19
|
+
? Object.values(MEMBER_ACTIONS)
|
|
20
|
+
: Object.values(MEMBER_ACTIONS).filter(action => action !== MEMBER_ACTIONS.NONE);
|
|
21
|
+
return actionsToSync.map(action => ({
|
|
22
|
+
name: TASKS_NAMES.ScheduleMembersDataPerAction,
|
|
23
|
+
data: {
|
|
24
|
+
action,
|
|
25
|
+
...(backupDate ? { backupDate } : {}),
|
|
26
|
+
...(isTestEnvironment ? { isTestEnvironment } : {}),
|
|
27
|
+
...(includeNone ? { includeNone } : {}),
|
|
28
|
+
},
|
|
29
|
+
type: 'scheduled',
|
|
30
|
+
}));
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Schedules the daily pull (one ScheduleMembersDataPerAction task per action).
|
|
35
|
+
* @param {Object} [options] - Same options as buildDailyPullTasks
|
|
36
|
+
* @returns {Promise<any>}
|
|
37
|
+
*/
|
|
38
|
+
const scheduleDailyPullTasks = options =>
|
|
39
|
+
taskManager().scheduleInBulk(buildDailyPullTasks(options));
|
|
40
|
+
|
|
41
|
+
module.exports = { buildDailyPullTasks, scheduleDailyPullTasks };
|
|
@@ -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const { taskManager } = require('psdev-task-manager');
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { scheduleDailyPullTasks } = require('./daily-pull/schedule-methods');
|
|
4
4
|
const { TASKS_NAMES } = require('./tasks/consts');
|
|
5
5
|
const { dailyPullExecutionCheck } = require('./tasks/daily-pull-check-methods');
|
|
6
6
|
const { TASKS } = require('./tasks/tasks-configs');
|
|
@@ -34,21 +34,7 @@ async function scheduleDailyPullTask(options = null) {
|
|
|
34
34
|
console.log(`isTestEnvironment: ${isTestEnvironment}`);
|
|
35
35
|
console.log(`includeNone: ${includeNone}`);
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
? Object.values(MEMBER_ACTIONS)
|
|
39
|
-
: Object.values(MEMBER_ACTIONS).filter(action => action !== MEMBER_ACTIONS.NONE);
|
|
40
|
-
const toScheduleTasks = actionsToSync.map(action => ({
|
|
41
|
-
name: TASKS_NAMES.ScheduleMembersDataPerAction,
|
|
42
|
-
data: {
|
|
43
|
-
action,
|
|
44
|
-
...(backupDate ? { backupDate } : {}),
|
|
45
|
-
...(isTestEnvironment ? { isTestEnvironment } : {}),
|
|
46
|
-
...(includeNone ? { includeNone } : {}),
|
|
47
|
-
},
|
|
48
|
-
type: 'scheduled',
|
|
49
|
-
}));
|
|
50
|
-
|
|
51
|
-
return await taskManager().scheduleInBulk(toScheduleTasks);
|
|
37
|
+
return await scheduleDailyPullTasks({ backupDate, isTestEnvironment, includeNone });
|
|
52
38
|
} catch (error) {
|
|
53
39
|
console.error(`Failed to scheduleDailyPullTask: ${error.message}`);
|
|
54
40
|
throw new Error(`Failed to scheduleDailyPullTask: ${error.message}`);
|
|
@@ -97,10 +83,50 @@ async function scheduleFixUrlsWithSpacesTask() {
|
|
|
97
83
|
}
|
|
98
84
|
}
|
|
99
85
|
|
|
100
|
-
async function
|
|
86
|
+
async function scheduleNormalizeMemberEmailsTask() {
|
|
87
|
+
try {
|
|
88
|
+
console.log('scheduleNormalizeMemberEmails started!');
|
|
89
|
+
return await taskManager().schedule({
|
|
90
|
+
name: TASKS_NAMES.scheduleNormalizeMemberEmails,
|
|
91
|
+
data: {},
|
|
92
|
+
type: 'scheduled',
|
|
93
|
+
});
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error(`Failed to scheduleNormalizeMemberEmails: ${error.message}`);
|
|
96
|
+
throw new Error(`Failed to scheduleNormalizeMemberEmails: ${error.message}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Schedules hiding ALL addresses for ALL members that have any address (sets each address's
|
|
102
|
+
* addressStatus to DONT_SHOW). Manually triggered one-off maintenance task.
|
|
103
|
+
*/
|
|
104
|
+
async function scheduleHideAllMemberAddressesTask() {
|
|
105
|
+
try {
|
|
106
|
+
console.log('scheduleHideAllMemberAddresses started!');
|
|
107
|
+
return await taskManager().schedule({
|
|
108
|
+
name: TASKS_NAMES.scheduleHideAllMemberAddresses,
|
|
109
|
+
data: {},
|
|
110
|
+
type: 'scheduled',
|
|
111
|
+
});
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error(`Failed to scheduleHideAllMemberAddresses: ${error.message}`);
|
|
114
|
+
throw new Error(`Failed to scheduleHideAllMemberAddresses: ${error.message}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Runs the daily pull execution check (watchdog).
|
|
120
|
+
* @param {Object} [options]
|
|
121
|
+
* @param {number} [options.hoursBack=4] - Lookback window in hours
|
|
122
|
+
* @param {boolean} [options.isTestEnvironment=false] - Pull from the test PAC API if the fallback fires
|
|
123
|
+
* @param {boolean} [options.includeNone=false] - Include the NONE action if the fallback fires
|
|
124
|
+
* @returns {Promise<Object>} - Check result
|
|
125
|
+
*/
|
|
126
|
+
async function runDailyPullExecutionCheck(options = {}) {
|
|
101
127
|
try {
|
|
102
128
|
console.log('runDailyPullExecutionCheck started!');
|
|
103
|
-
return await dailyPullExecutionCheck({});
|
|
129
|
+
return await dailyPullExecutionCheck(options || {});
|
|
104
130
|
} catch (error) {
|
|
105
131
|
console.error(`Failed to runDailyPullExecutionCheck: ${error.message}`);
|
|
106
132
|
throw new Error(`Failed to runDailyPullExecutionCheck: ${error.message}`);
|
|
@@ -126,5 +152,7 @@ module.exports = {
|
|
|
126
152
|
scheduleCreateContactsFromMembersTask,
|
|
127
153
|
scheduleFixPrimaryAddressForMembersTask,
|
|
128
154
|
scheduleFixUrlsWithSpacesTask,
|
|
155
|
+
scheduleNormalizeMemberEmailsTask,
|
|
156
|
+
scheduleHideAllMemberAddressesTask,
|
|
129
157
|
runDailyPullExecutionCheck,
|
|
130
158
|
};
|