abmp-npm 1.8.30 → 1.8.32

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,114 @@
1
+ const { taskManager } = require('psdev-task-manager');
2
+
3
+ const { TASKS_NAMES } = require('../consts');
4
+ const { fetchPACMembers } = require('../pac-api-methods');
5
+
6
+ const { bulkProcessAndSaveMemberData } = require('./bulk-process-methods');
7
+ const { isUpdatedMember, isABMPMember } = require('./utils');
8
+
9
+ async function syncMembersDataPerAction(action) {
10
+ try {
11
+ const firstPageResponse = await fetchPACMembers(1, action);
12
+
13
+ if (
14
+ !firstPageResponse ||
15
+ !firstPageResponse.results ||
16
+ firstPageResponse.results.length === 0
17
+ ) {
18
+ return {
19
+ success: true,
20
+ totalPagesProcessed: 0,
21
+ lastPageProcessed: 0,
22
+ completedAt: new Date().toISOString(),
23
+ message: 'No data found',
24
+ };
25
+ }
26
+
27
+ // Calculate total pages from API response
28
+ const totalResults = firstPageResponse.total_results || 0;
29
+ const perPage = firstPageResponse.results.length;
30
+ const totalPages = firstPageResponse.total_pages || 0;
31
+
32
+ // Cap at 1000 pages as safety measure
33
+ const pagesToProcess = Math.min(totalPages, 1000);
34
+
35
+ console.log(
36
+ `Scheduling ${pagesToProcess} pages for processing (${totalResults} total records, ${perPage} per page)`
37
+ );
38
+
39
+ // Schedule tasks for all pages at once
40
+ const toScheduleTasks = Array.from(
41
+ { length: pagesToProcess },
42
+ (_, i) => i + 1 // API expects page number to start from 1
43
+ ).map(pageNumber => ({
44
+ name: TASKS_NAMES.SyncMembers,
45
+ data: {
46
+ pageNumber,
47
+ action,
48
+ },
49
+ type: 'scheduled',
50
+ }));
51
+
52
+ // Wait for all scheduling to complete
53
+ await taskManager().scheduleInBulk(toScheduleTasks);
54
+
55
+ return {
56
+ success: true,
57
+ totalPagesProcessed: pagesToProcess,
58
+ lastPageProcessed: pagesToProcess,
59
+ totalRecords: totalResults,
60
+ recordsPerPage: perPage,
61
+ completedAt: new Date().toISOString(),
62
+ };
63
+ } catch (error) {
64
+ throw new Error(`Synchronization failed: ${error.message}`);
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Synchronizes a single page of member data
70
+ * @param {Object} taskObject - Task object containing page data
71
+ * @returns {Promise<Object>} - Page synchronization result
72
+ */
73
+ async function synchronizeSinglePage(taskObject) {
74
+ const { pageNumber, action } = taskObject.data;
75
+ try {
76
+ const memberDataResponse = await fetchPACMembers(pageNumber, action);
77
+
78
+ if (
79
+ !memberDataResponse ||
80
+ !memberDataResponse.results ||
81
+ memberDataResponse.results.length === 0
82
+ ) {
83
+ throw new Error(`No data found for page ${pageNumber}`);
84
+ }
85
+ const toSyncMembers = memberDataResponse.results.filter(
86
+ member => isUpdatedMember(member) && isABMPMember(member)
87
+ );
88
+ if (toSyncMembers.length === 0) {
89
+ return {
90
+ success: true,
91
+ pageNumber,
92
+ totalPageSize: memberDataResponse.results.length,
93
+ filteredPageSize: toSyncMembers.length,
94
+ message: 'No to be updated, or ABMP members found',
95
+ };
96
+ }
97
+ const result = await bulkProcessAndSaveMemberData(toSyncMembers, pageNumber);
98
+
99
+ return {
100
+ success: true,
101
+ pageNumber,
102
+ totalPageSize: memberDataResponse.results.length,
103
+ filteredPageSize: toSyncMembers.length,
104
+ ...result,
105
+ };
106
+ } catch (error) {
107
+ throw new Error(`Page ${pageNumber} synchronization failed: ${error.message}`);
108
+ }
109
+ }
110
+
111
+ module.exports = {
112
+ syncMembersDataPerAction,
113
+ synchronizeSinglePage,
114
+ };
@@ -0,0 +1,78 @@
1
+ const { updateWixMemberLoginEmail } = require('../members-area-methods');
2
+
3
+ const { MEMBER_ACTIONS } = require('./consts');
4
+
5
+ const isUpdatedMember = member => member.action !== MEMBER_ACTIONS.NONE;
6
+ const isABMPMember = member =>
7
+ member.memberships.some(membership => membership.association === 'ABMP');
8
+
9
+ const changeWixMembersEmails = async toChangeWixMembersEmails => {
10
+ console.log(
11
+ `Changing login emails for ${toChangeWixMembersEmails.length} members with ids: [${toChangeWixMembersEmails.map(member => member.memberId).join(', ')}]`
12
+ );
13
+ return await Promise.all(
14
+ toChangeWixMembersEmails.map(member => updateWixMemberLoginEmail(member, {}))
15
+ );
16
+ };
17
+
18
+ /**
19
+ * Validates core member data requirements
20
+ * @param {Object} inputMemberData - Raw member data from API to validate
21
+ * @returns {boolean} - True if all required fields are valid, false otherwise
22
+ */
23
+ const validateCoreMemberData = inputMemberData => {
24
+ // Check memberid
25
+ if (!inputMemberData?.memberid) {
26
+ console.warn('validateCoreMemberData: Missing required field - memberid is mandatory');
27
+ return false;
28
+ }
29
+
30
+ // Check email
31
+ if (
32
+ !inputMemberData?.email ||
33
+ typeof inputMemberData.email !== 'string' ||
34
+ !inputMemberData.email.trim()
35
+ ) {
36
+ console.warn(
37
+ 'validateCoreMemberData: Missing required field - email (valid string) is mandatory'
38
+ );
39
+ return false;
40
+ }
41
+
42
+ // Check memberships
43
+ if (
44
+ !inputMemberData?.memberships ||
45
+ !Array.isArray(inputMemberData.memberships) ||
46
+ inputMemberData.memberships.length === 0
47
+ ) {
48
+ console.warn(
49
+ 'validateCoreMemberData: Missing required field - memberships (non-empty array) is mandatory'
50
+ );
51
+ return false;
52
+ }
53
+
54
+ return true;
55
+ };
56
+
57
+ const containsNonEnglish = str => /[^a-zA-Z0-9]/.test(str); // if it contains any non-english characters, test1 is allowed, but any others are not
58
+
59
+ /**
60
+ * Creates a full name from first and last name components
61
+ * @param {string} firstName - First name
62
+ * @param {string} lastName - Last name
63
+ * @returns {string} - Combined full name
64
+ */
65
+ const createFullName = (firstName, lastName) => {
66
+ const trimmedFirst = firstName?.trim() || '';
67
+ const trimmedLast = lastName?.trim() || '';
68
+ return `${trimmedFirst} ${trimmedLast}`.trim();
69
+ };
70
+
71
+ module.exports = {
72
+ isUpdatedMember,
73
+ isABMPMember,
74
+ changeWixMembersEmails,
75
+ validateCoreMemberData,
76
+ containsNonEnglish,
77
+ createFullName,
78
+ };
package/backend/index.js CHANGED
@@ -1,6 +1,11 @@
1
1
  module.exports = {
2
2
  ...require('./forms-methods'),
3
3
  ...require('./search-filters-methods'),
4
- ...require('./members-data-methods'),
5
- ...require('./utils'),
4
+ ...require('./jobs'),
5
+ ...require('./utils'), //TODO: remove it once we finish NPM movement
6
+ ...require('./daily-pull'), //TODO: remove it once we finish NPM movement
7
+ ...require('./pac-api-methods'), //TODO: remove it once we finish NPM movement
8
+ ...require('./members-area-methods'), //TODO: remove it once we finish NPM movement
9
+ ...require('./members-data-methods'), //TODO: remove it once we finish NPM movement
10
+ ...require('./cms-data-methods'), //TODO: remove it once we finish NPM movement
6
11
  };
@@ -0,0 +1,30 @@
1
+ const { taskManager } = require('psdev-task-manager');
2
+
3
+ const { TASKS_NAMES } = require('./consts');
4
+ const { TASKS } = require('./tasks');
5
+
6
+ async function runScheduledTasks() {
7
+ try {
8
+ console.log('runScheduledTasks started');
9
+ return await taskManager().runScheduledTasks(TASKS);
10
+ } catch (error) {
11
+ console.error(`Failed to runScheduledTasks: ${error.message}`);
12
+ throw new Error(`Failed to runScheduledTasks: ${error.message}`);
13
+ }
14
+ }
15
+
16
+ async function scheduleDailyPullTask() {
17
+ try {
18
+ console.log('scheduleDailyPullTask started!');
19
+ return await taskManager().schedule({
20
+ name: TASKS_NAMES.ScheduleDailyMembersDataSync,
21
+ data: {},
22
+ type: 'scheduled',
23
+ });
24
+ } catch (error) {
25
+ console.error(`Failed to scheduleDailyPullTask: ${error.message}`);
26
+ throw new Error(`Failed to scheduleDailyPullTask: ${error.message}`);
27
+ }
28
+ }
29
+
30
+ module.exports = { runScheduledTasks, scheduleDailyPullTask };
@@ -1,5 +1,5 @@
1
1
  const { auth } = require('@wix/essentials');
2
- const { members } = require('@wix/members');
2
+ const { members, authentication } = require('@wix/members');
3
3
  const elevatedCreateMember = auth.elevate(members.createMember);
4
4
 
5
5
  function prepareContactData(partner) {
@@ -36,7 +36,54 @@ const getCurrentMember = async () => {
36
36
  return member.member;
37
37
  };
38
38
 
39
+ /**
40
+ * Updates Wix member login email if the member has a contactId (registered Wix member)
41
+ * @param {Object} member - Member object with contactId and email
42
+ * @param {Object} result - Result object to track Wix member updates
43
+ */
44
+ async function updateWixMemberLoginEmail(member, result = {}) {
45
+ if (!member.contactId) {
46
+ console.log(`Member ${member.memberId} has no contactId - skipping Wix login email update`);
47
+ return;
48
+ }
49
+
50
+ try {
51
+ console.log(
52
+ `Updating Wix login email for member ${member.memberId} (contactId: ${member.contactId})`
53
+ );
54
+
55
+ const updatedWixMember = await authentication.changeLoginEmail(member.contactId, member.email);
56
+
57
+ console.log(
58
+ `✅ Successfully updated Wix login email for member ${member.memberId}: ${updatedWixMember.loginEmail}`
59
+ );
60
+
61
+ if (!result.wixMemberUpdates) {
62
+ result.wixMemberUpdates = { successful: 0, failed: 0 };
63
+ }
64
+ result.wixMemberUpdates.successful++;
65
+ } catch (error) {
66
+ console.error(`❌ Failed to update Wix login email for member ${member.memberId}:`, error);
67
+
68
+ if (!result.wixMemberUpdates) {
69
+ result.wixMemberUpdates = { successful: 0, failed: 0 };
70
+ }
71
+ result.wixMemberUpdates.failed++;
72
+
73
+ if (!result.wixMemberErrors) {
74
+ result.wixMemberErrors = [];
75
+ }
76
+ result.wixMemberErrors.push({
77
+ memberId: member.memberId,
78
+ contactId: member.contactId,
79
+ email: member.email,
80
+ error: error.message,
81
+ });
82
+ }
83
+ }
84
+
39
85
  module.exports = {
40
86
  createSiteMember,
41
87
  getCurrentMember,
88
+ updateWixMemberLoginEmail,
42
89
  };
@@ -1,22 +1,19 @@
1
- const { contacts } = require('@wix/crm');
2
- const { auth } = require('@wix/essentials');
3
-
4
1
  const { COLLECTIONS } = require('../public/consts');
5
2
 
6
3
  const { MEMBER_ACTIONS } = require('./consts');
4
+ const { updateMemberContactInfo } = require('./contacts-methods');
7
5
  const { wixData } = require('./elevated-modules');
8
6
  const { createSiteMember, getCurrentMember } = require('./members-area-methods');
9
7
  const {
8
+ createBatches,
9
+ normalizeUrlForComparison,
10
+ queryAllItems,
10
11
  formatDateToMonthYear,
11
12
  getAddressDisplayOptions,
12
13
  isStudent,
13
- generateGeoHash,
14
14
  urlExists,
15
+ generateGeoHash,
15
16
  } = require('./utils');
16
-
17
- const elevatedGetContact = auth.elevate(contacts.getContact);
18
- const elevatedUpdateContact = auth.elevate(contacts.updateContact);
19
-
20
17
  /**
21
18
  * Retrieves member data by member ID
22
19
  * @param {string} memberId - The member ID to search for
@@ -136,126 +133,112 @@ async function validateMemberToken(memberIdInput) {
136
133
  }
137
134
  }
138
135
 
139
- /**
140
- * Generic contact update helper function
141
- * @param {string} contactId - The contact ID in Wix CRM
142
- * @param {function} updateInfoCallback - Function that returns the updated info object
143
- * @param {string} operationName - Name of the operation for logging
136
+ /** Performs bulk save operation for member data
137
+ * @param { Array } memberDataList - Array of member data objects to save
138
+ * @returns { Promise < Object >} - Bulk save operation result
144
139
  */
145
- async function updateContactInfo(contactId, updateInfoCallback, operationName) {
146
- if (!contactId) {
147
- throw new Error('Contact ID is required');
140
+ async function bulkSaveMembers(memberDataList) {
141
+ if (!Array.isArray(memberDataList) || memberDataList.length === 0) {
142
+ throw new Error('Invalid member data list provided for bulk save');
148
143
  }
149
144
 
150
145
  try {
151
- const contact = await elevatedGetContact(contactId);
152
- console.log('contact from updateContactInfo', contact);
153
- const currentInfo = contact.info;
154
- const updatedInfo = updateInfoCallback(currentInfo);
155
-
156
- await elevatedUpdateContact(contactId, { info: updatedInfo }, contact.revision);
146
+ // bulkSave all with batches of 1000 items as this is the Velo limit for bulkSave
147
+ const batches = createBatches(memberDataList, 1000);
148
+ return await Promise.all(
149
+ batches.map(batch => wixData.bulkSave(COLLECTIONS.MEMBERS_DATA, batch))
150
+ );
157
151
  } catch (error) {
158
- console.error(`Error in ${operationName}:`, error);
159
- throw new Error(`Failed to ${operationName}: ${error.message}`);
152
+ console.error('Error bulk saving members:', error);
153
+ throw new Error(`Bulk save failed: ${error.message}`);
160
154
  }
161
155
  }
162
156
 
163
157
  /**
164
- * Updates contact email in Wix CRM
165
- * @param {string} contactId - The contact ID in Wix CRM
166
- * @param {string} newEmail - The new email address
158
+ * Retrieves member data by member ID
159
+ * @param {string} memberId - The member ID to search for
160
+ * @returns {Promise<Object|null>} - Member data or null if not found
167
161
  */
168
- async function updateContactEmail(contactId, newEmail) {
169
- if (!newEmail) {
170
- throw new Error('New email is required');
162
+ async function findMemberById(memberId) {
163
+ if (!memberId) {
164
+ throw new Error('Member ID is required');
171
165
  }
172
166
 
173
- return await updateContactInfo(
174
- contactId,
175
- currentInfo => ({
176
- ...currentInfo,
177
- emails: {
178
- items: [
179
- {
180
- email: newEmail,
181
- primary: true,
182
- },
183
- ],
184
- },
185
- }),
186
- 'update contact email'
187
- );
188
- }
167
+ try {
168
+ const queryResult = await wixData
169
+ .query(COLLECTIONS.MEMBERS_DATA)
170
+ .eq('memberId', memberId)
171
+ .find();
189
172
 
190
- /**
191
- * Updates contact names in Wix CRM
192
- * @param {string} contactId - The contact ID in Wix CRM
193
- * @param {string} firstName - The new first name
194
- * @param {string} lastName - The new last name
195
- */
196
- async function updateContactNames(contactId, firstName, lastName) {
197
- if (!firstName && !lastName) {
198
- throw new Error('At least one name field is required');
173
+ return queryResult.items.length > 0 ? queryResult.items[0] : null;
174
+ } catch (error) {
175
+ throw new Error(`Failed to retrieve member data: ${error.message}`);
199
176
  }
200
-
201
- return await updateContactInfo(
202
- contactId,
203
- currentInfo => ({
204
- ...currentInfo,
205
- name: {
206
- first: firstName || currentInfo?.name?.first || '',
207
- last: lastName || currentInfo?.name?.last || '',
208
- },
209
- }),
210
- 'update contact names'
211
- );
212
177
  }
213
178
 
214
179
  /**
215
- * Update fields if they have changed
216
- * @param {Array} existingValues - Current values for comparison
217
- * @param {Array} newValues - New values to compare against
218
- * @param {Function} updater - Function to call if values changed
219
- * @param {Function} argsBuilder - Function to build arguments for updater
180
+ * Method to get member by slug with flexible filtering options
181
+ * @param {Object} options - Query options
182
+ * @param {string} options.slug - The slug to search for
183
+ * @param {boolean} options.excludeDropped - Whether to exclude dropped members (default: true)
184
+ * @param {boolean} options.excludeSearchedMember - Whether to exclude a specific member (default: false)
185
+ * @param {string|number} [options.memberId] - Member ID to exclude when excludeSearchedMember is true (optional)
186
+ * @param {boolean} [options.queryAllMatches=false] - Whether to query all matches or just the first one (default: false)
187
+ * @returns {Promise<Object|null>} - Member data or null if not found
220
188
  */
221
- const updateIfChanged = (existingValues, newValues, updater, argsBuilder) => {
222
- const hasChanged = existingValues.some((val, idx) => val !== newValues[idx]);
223
- if (!hasChanged) return null;
224
- return updater(...argsBuilder(newValues));
225
- };
189
+ async function getMemberBySlug({
190
+ slug,
191
+ excludeDropped = true,
192
+ excludeSearchedMember = false,
193
+ memberId = null,
194
+ queryAllMatches = false,
195
+ }) {
196
+ if (!slug) return null;
226
197
 
227
- /**
228
- * Updates member contact information in CRM if fields have changed
229
- * @param {string} id - Member ID
230
- * @param {Object} data - New member data
231
- */
232
- const updateMemberContactInfo = async (id, data) => {
233
- const existing = await findMemberByWixDataId(id);
234
- const { contactId } = existing;
235
-
236
- const updateConfig = [
237
- {
238
- fields: ['contactFormEmail'],
239
- updater: updateContactEmail,
240
- args: ([email]) => [contactId, email],
241
- },
242
- {
243
- fields: ['firstName', 'lastName'],
244
- updater: updateContactNames,
245
- args: ([firstName, lastName]) => [contactId, firstName, lastName],
246
- },
247
- ];
248
-
249
- const updatePromises = updateConfig
250
- .map(({ fields, updater, args }) => {
251
- const existingValues = fields.map(field => existing[field]);
252
- const newValues = fields.map(field => data[field]);
253
- return updateIfChanged(existingValues, newValues, updater, args);
254
- })
255
- .filter(Boolean);
256
-
257
- await Promise.all(updatePromises);
258
- };
198
+ try {
199
+ let query = wixData.query(COLLECTIONS.MEMBERS_DATA).contains('url', slug);
200
+
201
+ if (excludeDropped) {
202
+ query = query.ne('action', 'drop');
203
+ }
204
+
205
+ if (excludeSearchedMember && memberId) {
206
+ query = query.ne('memberId', memberId);
207
+ }
208
+ let membersList;
209
+ if (queryAllMatches) {
210
+ query = query.limit(1000);
211
+ membersList = await queryAllItems(query);
212
+ } else {
213
+ membersList = await query.find().then(res => res.items);
214
+ }
215
+ let matchingMembers = membersList.filter(
216
+ item => item.url && item.url.toLowerCase() === slug.toLowerCase()
217
+ );
218
+ if (queryAllMatches) {
219
+ matchingMembers = membersList
220
+ .filter(
221
+ //remove trailing "-1", "-2", etc.
222
+ item => item.url && normalizeUrlForComparison(item.url) === slug.toLowerCase()
223
+ )
224
+ .sort((a, b) => b.url.toLowerCase().localeCompare(a.url.toLowerCase()));
225
+ }
226
+ if (matchingMembers.length > 1) {
227
+ const queryResultMsg = `Multiple members found with same slug ${slug} membersIds are : [${matchingMembers
228
+ .map(member => member.memberId)
229
+ .join(', ')}]`;
230
+ if (!queryAllMatches) {
231
+ throw new Error(queryResultMsg);
232
+ } else {
233
+ console.log(queryResultMsg);
234
+ }
235
+ }
236
+ return matchingMembers[0] || null;
237
+ } catch (error) {
238
+ console.error('Error getting member by slug:', error);
239
+ throw error;
240
+ }
241
+ }
259
242
 
260
243
  /**
261
244
  * Saves member registration data
@@ -305,4 +288,7 @@ module.exports = {
305
288
  createContactAndMemberIfNew,
306
289
  validateMemberToken,
307
290
  saveRegistrationData,
291
+ bulkSaveMembers,
292
+ findMemberById,
293
+ getMemberBySlug,
308
294
  };
@@ -0,0 +1,35 @@
1
+ const { secrets } = require('@wix/secrets');
2
+
3
+ const { PAC_API_URL } = require('./daily-pull/consts');
4
+
5
+ const getHeaders = async () => {
6
+ const AUTH_TOKEN = await secrets.getSecretValue('members-data-api-key');
7
+ const headers = {
8
+ Authorization: `Bearer ${AUTH_TOKEN}`,
9
+ };
10
+ return headers;
11
+ };
12
+ const fetchPACMembers = async (pageNum, actionFilter) => {
13
+ const url = `${PAC_API_URL}/Members?page=${pageNum}&actionFilter=${actionFilter}`;
14
+ const headers = await getHeaders();
15
+ const fetchOptions = {
16
+ method: 'get',
17
+ headers: headers,
18
+ };
19
+ const response = await fetch(url, fetchOptions);
20
+ const responseType = response.headers.get('content-type');
21
+ if (!responseType.includes('application/json')) {
22
+ const errorMessage = `[fetchPACMembers] got invalid responseType: ${responseType} for page ${pageNum} and actionFilter ${actionFilter}`;
23
+ console.error(errorMessage);
24
+ throw new Error(errorMessage);
25
+ }
26
+ if (response.ok) {
27
+ return response.json();
28
+ } else {
29
+ const errorMessage = `[fetchPACMembers] failed with status ${response.status} for page ${pageNum} and actionFilter ${actionFilter}`;
30
+ console.error(errorMessage);
31
+ throw new Error(errorMessage);
32
+ }
33
+ };
34
+
35
+ module.exports = { fetchPACMembers, getHeaders }; //TODO: remove getHeaders from exported methods once npm movement finishes
@@ -0,0 +1,37 @@
1
+ const { TASKS_NAMES } = require('./consts');
2
+ const { MEMBER_ACTIONS, synchronizeSinglePage, syncMembersDataPerAction } = require('./daily-pull');
3
+
4
+ const getDailyMembersDataSyncChildTasks = () => {
5
+ // we don't want to sync none action as it means this members data hasn't changed and we don't need to sync it
6
+ const MEMBER_ACTIONS_EXCEPT_NONE = Object.values(MEMBER_ACTIONS).filter(
7
+ action => action !== MEMBER_ACTIONS.NONE
8
+ );
9
+ return MEMBER_ACTIONS_EXCEPT_NONE.map(action => ({
10
+ name: TASKS_NAMES.ScheduleMembersDataPerAction,
11
+ data: { action },
12
+ }));
13
+ };
14
+ const TASKS = {
15
+ [TASKS_NAMES.ScheduleDailyMembersDataSync]: {
16
+ name: TASKS_NAMES.ScheduleDailyMembersDataSync,
17
+ scheduleChildrenSequentially: false,
18
+ estimatedDurationSec: 60,
19
+ childTasks: getDailyMembersDataSyncChildTasks(),
20
+ },
21
+ [TASKS_NAMES.ScheduleMembersDataPerAction]: {
22
+ name: TASKS_NAMES.ScheduleMembersDataPerAction,
23
+ getIdentifier: task => task.data.action,
24
+ process: syncMembersDataPerAction,
25
+ shouldSkipCheck: () => false,
26
+ estimatedDurationSec: 6,
27
+ },
28
+ [TASKS_NAMES.SyncMembers]: {
29
+ name: TASKS_NAMES.SyncMembers,
30
+ getIdentifier: task => task,
31
+ process: synchronizeSinglePage,
32
+ shouldSkipCheck: () => false,
33
+ estimatedDurationSec: 6,
34
+ },
35
+ };
36
+
37
+ module.exports = { TASKS };