abmp-npm 1.8.31 → 1.8.33

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.
@@ -4,7 +4,11 @@ const { COLLECTIONS, MEMBERS_FIELDS } = require('../public/consts.js');
4
4
  const { findMainAddress } = require('../public/Utils/sharedUtils.js');
5
5
  const { calculateDistance, shuffleArray } = require('../public/Utils/sharedUtils.js');
6
6
 
7
- const { PRECISION, MAX__MEMBERS_SEARCH_RESULTS, WIX_QUERY_MAX_LIMIT } = require('./consts.js');
7
+ const {
8
+ GEO_HASH_PRECISION,
9
+ MAX__MEMBERS_SEARCH_RESULTS,
10
+ WIX_QUERY_MAX_LIMIT,
11
+ } = require('./consts.js');
8
12
  const { wixData } = require('./elevated-modules');
9
13
 
10
14
  function buildMembersSearchQuery(data) {
@@ -92,7 +96,7 @@ function buildMembersSearchQuery(data) {
92
96
  query = applyFilterToQuery(query, config, filter);
93
97
  });
94
98
  if (isUserLocationEnabled && isSearchingNearby) {
95
- const userGeohash = geohash.encode(filter.latitude, filter.longitude, PRECISION);
99
+ const userGeohash = geohash.encode(filter.latitude, filter.longitude, GEO_HASH_PRECISION);
96
100
  const neighborGeohashes = geohash.neighbors(userGeohash);
97
101
  const geohashList = [userGeohash, ...neighborGeohashes];
98
102
  query = query.hasSome('locHash', geohashList);
@@ -203,7 +207,33 @@ async function fetchAllItemsInParallel(query) {
203
207
  };
204
208
  }
205
209
 
210
+ /**
211
+ * Get all interests from the database
212
+ * @returns {Promise<Array<string>>} Array of interest titles sorted alphabetically
213
+ */
214
+ async function getInterestAll() {
215
+ try {
216
+ let res = await wixData.query(COLLECTIONS.INTERESTS).limit(1000).find();
217
+
218
+ let interests = res.items.map(x => x.title);
219
+
220
+ while (res.hasNext()) {
221
+ res = await res.next();
222
+ interests.push(...res.items.map(x => x.title));
223
+ }
224
+
225
+ // Sort the interests alphabetically (case-insensitive)
226
+ interests = interests.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
227
+
228
+ return interests;
229
+ } catch (e) {
230
+ console.error('Error in getInterestAll:', e);
231
+ throw e;
232
+ }
233
+ }
234
+
206
235
  module.exports = {
207
236
  buildMembersSearchQuery,
208
237
  fetchAllItemsInParallel,
238
+ getInterestAll,
209
239
  };
package/backend/consts.js CHANGED
@@ -8,26 +8,21 @@ const CONFIG_KEYS = {
8
8
  SITE_ASSOCIATION: 'SITE_ASSOCIATION',
9
9
  };
10
10
 
11
- const PRECISION = 3;
12
11
  const MAX__MEMBERS_SEARCH_RESULTS = 120;
13
12
  const WIX_QUERY_MAX_LIMIT = 1000;
14
13
 
15
- /**
16
- * Member action types
17
- * @readonly
18
- * @enum {string}
19
- */
20
- const MEMBER_ACTIONS = {
21
- UPDATE: 'update',
22
- NEW: 'new',
23
- DROP: 'drop',
24
- NONE: 'none',
14
+ const TASKS_NAMES = {
15
+ ScheduleDailyMembersDataSync: 'ScheduleDailyMembersDataSync',
16
+ ScheduleMembersDataPerAction: 'ScheduleMembersDataPerAction',
17
+ SyncMembers: 'SyncMembers',
25
18
  };
26
19
 
20
+ const GEO_HASH_PRECISION = 3;
21
+
27
22
  module.exports = {
28
23
  CONFIG_KEYS,
29
- PRECISION,
30
24
  MAX__MEMBERS_SEARCH_RESULTS,
31
25
  WIX_QUERY_MAX_LIMIT,
32
- MEMBER_ACTIONS,
26
+ TASKS_NAMES,
27
+ GEO_HASH_PRECISION,
33
28
  };
@@ -0,0 +1,130 @@
1
+ const { contacts } = require('@wix/crm');
2
+ const { auth } = require('@wix/essentials');
3
+ const elevatedGetContact = auth.elevate(contacts.getContact);
4
+ const elevatedUpdateContact = auth.elevate(contacts.updateContact);
5
+
6
+ const { findMemberByWixDataId } = require('./members-data-methods');
7
+
8
+ /**
9
+ * Generic contact update helper function
10
+ * @param {string} contactId - The contact ID in Wix CRM
11
+ * @param {function} updateInfoCallback - Function that returns the updated info object
12
+ * @param {string} operationName - Name of the operation for logging
13
+ */
14
+ async function updateContactInfo(contactId, updateInfoCallback, operationName) {
15
+ if (!contactId) {
16
+ throw new Error('Contact ID is required');
17
+ }
18
+
19
+ try {
20
+ const contact = await elevatedGetContact(contactId);
21
+ const currentInfo = contact.info;
22
+ const updatedInfo = updateInfoCallback(currentInfo);
23
+
24
+ await elevatedUpdateContact(contactId, { info: updatedInfo }, contact.revision);
25
+ } catch (error) {
26
+ console.error(`Error in ${operationName}:`, error);
27
+ throw new Error(`Failed to ${operationName}: ${error.message}`);
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Updates contact email in Wix CRM
33
+ * @param {string} contactId - The contact ID in Wix CRM
34
+ * @param {string} newEmail - The new email address
35
+ */
36
+ async function updateContactEmail(contactId, newEmail) {
37
+ if (!newEmail) {
38
+ throw new Error('New email is required');
39
+ }
40
+
41
+ return await updateContactInfo(
42
+ contactId,
43
+ currentInfo => ({
44
+ ...currentInfo,
45
+ emails: {
46
+ items: [
47
+ {
48
+ email: newEmail,
49
+ primary: true,
50
+ },
51
+ ],
52
+ },
53
+ }),
54
+ 'update contact email'
55
+ );
56
+ }
57
+
58
+ /**
59
+ * Updates contact names in Wix CRM
60
+ * @param {string} contactId - The contact ID in Wix CRM
61
+ * @param {string} firstName - The new first name
62
+ * @param {string} lastName - The new last name
63
+ */
64
+ async function updateContactNames(contactId, firstName, lastName) {
65
+ if (!firstName && !lastName) {
66
+ throw new Error('At least one name field is required');
67
+ }
68
+
69
+ return await updateContactInfo(
70
+ contactId,
71
+ currentInfo => ({
72
+ ...currentInfo,
73
+ name: {
74
+ first: firstName || currentInfo?.name?.first || '',
75
+ last: lastName || currentInfo?.name?.last || '',
76
+ },
77
+ }),
78
+ 'update contact names'
79
+ );
80
+ }
81
+
82
+ /**
83
+ * Update fields if they have changed
84
+ * @param {Array} existingValues - Current values for comparison
85
+ * @param {Array} newValues - New values to compare against
86
+ * @param {Function} updater - Function to call if values changed
87
+ * @param {Function} argsBuilder - Function to build arguments for updater
88
+ */
89
+ const updateIfChanged = (existingValues, newValues, updater, argsBuilder) => {
90
+ const hasChanged = existingValues.some((val, idx) => val !== newValues[idx]);
91
+ if (!hasChanged) return null;
92
+ return updater(...argsBuilder(newValues));
93
+ };
94
+
95
+ /**
96
+ * Updates member contact information in CRM if fields have changed
97
+ * @param {string} id - Member ID
98
+ * @param {Object} data - New member data
99
+ */
100
+ const updateMemberContactInfo = async (id, data) => {
101
+ const existing = await findMemberByWixDataId(id);
102
+ const { contactId } = existing;
103
+
104
+ const updateConfig = [
105
+ {
106
+ fields: ['contactFormEmail'],
107
+ updater: updateContactEmail,
108
+ args: ([email]) => [contactId, email],
109
+ },
110
+ {
111
+ fields: ['firstName', 'lastName'],
112
+ updater: updateContactNames,
113
+ args: ([firstName, lastName]) => [contactId, firstName, lastName],
114
+ },
115
+ ];
116
+
117
+ const updatePromises = updateConfig
118
+ .map(({ fields, updater, args }) => {
119
+ const existingValues = fields.map(field => existing[field]);
120
+ const newValues = fields.map(field => data[field]);
121
+ return updateIfChanged(existingValues, newValues, updater, args);
122
+ })
123
+ .filter(Boolean);
124
+
125
+ await Promise.all(updatePromises);
126
+ };
127
+
128
+ module.exports = {
129
+ updateMemberContactInfo,
130
+ };
@@ -0,0 +1,65 @@
1
+ const { bulkSaveMembers } = require('../members-data-methods');
2
+
3
+ const { generateUpdatedMemberData } = require('./process-member-methods');
4
+ const { changeWixMembersEmails } = require('./utils');
5
+
6
+ /**
7
+ * Processes and saves multiple member records in bulk
8
+ * @param {Array} memberDataList - Array of member data from API
9
+ * @param {number} currentPageNumber - Current page number being processed
10
+ * @returns {Promise<Object>} - Bulk save operation result with statistics
11
+ */
12
+ const bulkProcessAndSaveMemberData = async (memberDataList, currentPageNumber) => {
13
+ if (!Array.isArray(memberDataList) || memberDataList.length === 0) {
14
+ throw new Error('Invalid member data list provided');
15
+ }
16
+
17
+ const startTime = Date.now();
18
+
19
+ try {
20
+ const processedMemberDataPromises = memberDataList.map(memberData =>
21
+ generateUpdatedMemberData(memberData, currentPageNumber)
22
+ );
23
+
24
+ const processedMemberDataList = await Promise.all(processedMemberDataPromises);
25
+ const validMemberData = processedMemberDataList.filter(
26
+ data => data !== null && data !== undefined
27
+ );
28
+
29
+ if (validMemberData.length === 0) {
30
+ return {
31
+ totalProcessed: memberDataList.length,
32
+ totalSaved: 0,
33
+ totalFailed: memberDataList.length,
34
+ processingTime: Date.now() - startTime,
35
+ };
36
+ }
37
+ const toChangeWixMembersEmails = [];
38
+ const toSaveMembersData = validMemberData.map(member => {
39
+ const { isLoginEmailChanged, ...restMemberData } = member;
40
+ if (member.contactId && isLoginEmailChanged) {
41
+ toChangeWixMembersEmails.push(member);
42
+ }
43
+ 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
44
+ });
45
+ const saveResult = await bulkSaveMembers(toSaveMembersData);
46
+ // 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
47
+ if (toChangeWixMembersEmails.length > 0) {
48
+ await changeWixMembersEmails(toChangeWixMembersEmails);
49
+ }
50
+ const totalFailed = memberDataList.length - validMemberData.length;
51
+ const processingTime = Date.now() - startTime;
52
+
53
+ return {
54
+ ...saveResult,
55
+ totalProcessed: memberDataList.length,
56
+ totalSaved: validMemberData.length,
57
+ totalFailed: totalFailed,
58
+ processingTime: processingTime,
59
+ };
60
+ } catch (error) {
61
+ throw new Error(`Bulk operation failed: ${error.message}`);
62
+ }
63
+ };
64
+
65
+ module.exports = { bulkProcessAndSaveMemberData };
@@ -0,0 +1,34 @@
1
+ const PAC_API_URL = 'https://members.abmp.com/eweb/api/Wix';
2
+
3
+ const MEMBER_ACTIONS = {
4
+ UPDATE: 'update',
5
+ NEW: 'new',
6
+ DROP: 'drop',
7
+ NONE: 'none',
8
+ };
9
+
10
+ /**
11
+ * Address visibility configuration options
12
+ */
13
+ const ADDRESS_VISIBILITY_OPTIONS = {
14
+ ALL: 'all',
15
+ NONE: 'none',
16
+ };
17
+
18
+ /**
19
+ * Default display settings for member profiles
20
+ */
21
+ const DEFAULT_MEMBER_DISPLAY_SETTINGS = {
22
+ showLicenseNo: true,
23
+ showName: true,
24
+ showBookingUrl: false,
25
+ showWebsite: false,
26
+ showWixUrl: true,
27
+ };
28
+
29
+ module.exports = {
30
+ MEMBER_ACTIONS,
31
+ ADDRESS_VISIBILITY_OPTIONS,
32
+ DEFAULT_MEMBER_DISPLAY_SETTINGS,
33
+ PAC_API_URL,
34
+ };
@@ -0,0 +1,4 @@
1
+ module.exports = {
2
+ ...require('./sync-to-cms-methods'),
3
+ ...require('./consts'), //TODO: remove it once we finish NPM movement
4
+ };
@@ -0,0 +1,290 @@
1
+ const { ADDRESS_STATUS_TYPES } = require('../../public/consts');
2
+ const { findMemberById, getMemberBySlug } = require('../members-data-methods');
3
+ const { isValidArray, generateGeoHash } = require('../utils');
4
+
5
+ const {
6
+ MEMBER_ACTIONS,
7
+ ADDRESS_VISIBILITY_OPTIONS,
8
+ DEFAULT_MEMBER_DISPLAY_SETTINGS,
9
+ } = require('./consts');
10
+ const { validateCoreMemberData, containsNonEnglish, createFullName } = require('./utils');
11
+
12
+ /**
13
+ * Ensures a URL is unique by appending a counter if necessary
14
+ * @param {Object} options - The options object
15
+ * @param {string} options.url - The base URL to make unique
16
+ * @param {string|number} options.memberId - The member ID requesting this URL
17
+ * @param {string} options.fullName - The full name of the member
18
+ * @returns {Promise<string>} - A unique URL
19
+ */
20
+ const ensureUniqueUrl = async ({ url, memberId, fullName }) => {
21
+ const baseUrl = url;
22
+ let uniqueUrl = url;
23
+ if (!url) {
24
+ console.log(`member with id ${memberId} has no url, creating one`);
25
+ const fullNameWithoutSpace = fullName?.replace(/ /g, '');
26
+ if (!fullNameWithoutSpace || containsNonEnglish(fullNameWithoutSpace)) {
27
+ console.log(
28
+ `member with id ${memberId} has non-english full name, will use fallback url: 'firstNameLastName'`
29
+ );
30
+ uniqueUrl = 'firstNameLastName';
31
+ } else {
32
+ uniqueUrl = fullNameWithoutSpace; //fallback if there is no full name for this user
33
+ }
34
+ console.log(
35
+ `member with id ${memberId} and no API provided url, will have this initial url ${uniqueUrl}`
36
+ );
37
+ }
38
+ if (!memberId) throw new Error('Member ID is required');
39
+
40
+ const existingMember = await getMemberBySlug({
41
+ slug: uniqueUrl,
42
+ excludeDropped: true,
43
+ excludeSearchedMember: true,
44
+ memberId,
45
+ queryAllMatches: true,
46
+ });
47
+ if (existingMember && existingMember.url) {
48
+ console.log(
49
+ `Found member with same url ${existingMember.url} for memberId ${memberId} and URL ${uniqueUrl}, increasing counter by 1`
50
+ );
51
+ const lastSegment = existingMember.url.split('-').pop() || '0';
52
+ const lastCounter = parseInt(lastSegment, 10) || 0;
53
+ uniqueUrl = `${uniqueUrl}-${lastCounter + 1}`;
54
+ }
55
+ if (uniqueUrl !== baseUrl) {
56
+ console.log(`URL conflict resolved: ${baseUrl} -> ${uniqueUrl} for member ${memberId}`);
57
+ }
58
+ return uniqueUrl;
59
+ };
60
+
61
+ /**
62
+ * Generates complete updated member data by combining existing and migration data
63
+ * @param {Object} inputMemberData - Raw member data from API
64
+ * @param {number} currentPageNumber - Current page number being processed
65
+ * @returns {Promise<Object|null>} - Complete updated member data or null if validation fails
66
+ */
67
+ async function generateUpdatedMemberData(inputMemberData, currentPageNumber) {
68
+ if (!validateCoreMemberData(inputMemberData)) {
69
+ throw new Error(
70
+ 'Invalid member data: memberid, email (valid string), and memberships (array) are required'
71
+ );
72
+ }
73
+
74
+ const existingDbMember = await findMemberById(inputMemberData.memberid);
75
+
76
+ const updatedMemberData = await createCoreMemberData(
77
+ inputMemberData,
78
+ existingDbMember,
79
+ currentPageNumber
80
+ );
81
+
82
+ // If createCoreMemberData returns null due to validation failure, return null
83
+ if (!updatedMemberData) {
84
+ return null;
85
+ }
86
+
87
+ // Only enrich with migration and address data for new members
88
+ if (!existingDbMember) {
89
+ enrichWithMigrationData(updatedMemberData, inputMemberData.migrationData);
90
+
91
+ enrichWithAddressData(
92
+ updatedMemberData,
93
+ inputMemberData.addresses,
94
+ inputMemberData.migrationData?.addressinfo
95
+ );
96
+ }
97
+
98
+ return updatedMemberData;
99
+ }
100
+
101
+ /**
102
+ * Processes and adds address data with proper status
103
+ * @param {Object} memberDataToUpdate - Member data object to enhance
104
+ * @param {Array} addressesList - Array of address objects
105
+ * @param {Object} addressDisplayInfo - Address visibility configuration
106
+ */
107
+ function enrichWithAddressData(memberDataToUpdate, addressesList, addressDisplayInfo) {
108
+ if (isValidArray(addressesList)) {
109
+ memberDataToUpdate.addresses = processAddressesWithStatus(addressesList, addressDisplayInfo);
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Processes multiple addresses with their display statuses
115
+ * @param {Array} addressesList - Array of address objects
116
+ * @param {Object} displayConfiguration - Address display configuration
117
+ * @returns {Array} - Processed addresses with status information
118
+ */
119
+ function processAddressesWithStatus(addressesList, displayConfiguration = {}) {
120
+ if (!isValidArray(addressesList)) {
121
+ return [];
122
+ }
123
+
124
+ return addressesList.map(address => {
125
+ const displayStatus = displayConfiguration[address.key]
126
+ ? determineAddressDisplayStatus(displayConfiguration[address.key])
127
+ : ADDRESS_STATUS_TYPES.STATE_CITY_ZIP;
128
+
129
+ return {
130
+ ...address,
131
+ addressStatus: displayStatus,
132
+ };
133
+ });
134
+ }
135
+
136
+ /**
137
+ * Determines address display status based on visibility settings
138
+ * @param {string} visibilityValue - The address visibility value from migration data
139
+ * @returns {string} - The corresponding address status
140
+ */
141
+ function determineAddressDisplayStatus(visibilityValue) {
142
+ if (!visibilityValue) {
143
+ return ADDRESS_STATUS_TYPES.STATE_CITY_ZIP;
144
+ }
145
+
146
+ const normalizedValue = visibilityValue.trim().toLowerCase();
147
+
148
+ switch (normalizedValue) {
149
+ case ADDRESS_VISIBILITY_OPTIONS.ALL:
150
+ return ADDRESS_STATUS_TYPES.FULL_ADDRESS;
151
+ case ADDRESS_VISIBILITY_OPTIONS.NONE:
152
+ return ADDRESS_STATUS_TYPES.DONT_SHOW;
153
+ default:
154
+ return ADDRESS_STATUS_TYPES.STATE_CITY_ZIP;
155
+ }
156
+ }
157
+ /**
158
+ * Helper function to get fields that should only be set for new members
159
+ * @param {Object} inputMemberData - Raw member data from API
160
+ * @param {Object} existingDbMember - Existing member data from database
161
+ * @returns {Promise<Object>} - Object with fields that should only be set for new members
162
+ */
163
+ async function getNewMemberOnlyFields(inputMemberData, existingDbMember) {
164
+ if (existingDbMember) {
165
+ return {};
166
+ }
167
+
168
+ // Only set these fields for new members
169
+ const sanitizedFirstName = inputMemberData.firstname?.trim() || '';
170
+ const sanitizedLastName = inputMemberData.lastname?.trim() || '';
171
+ const fullName = createFullName(sanitizedFirstName, sanitizedLastName);
172
+
173
+ const uniqueUrl = await ensureUniqueUrl({
174
+ url: inputMemberData.url,
175
+ memberId: inputMemberData.memberid,
176
+ fullName,
177
+ });
178
+ return {
179
+ memberId: inputMemberData.memberid,
180
+ firstName: sanitizedFirstName,
181
+ lastName: sanitizedLastName,
182
+ fullName,
183
+ phones: inputMemberData.phones || [],
184
+ toShowPhone: inputMemberData.migrationData?.show_phone || '',
185
+ optOut: inputMemberData.migrationData?.opted_out || false,
186
+ url: uniqueUrl,
187
+ showContactForm: true,
188
+ bookingUrl: inputMemberData.migrationData?.schedule_code?.startsWith('http')
189
+ ? inputMemberData.migrationData?.schedule_code
190
+ : '',
191
+ APIBookingUrl: inputMemberData.migrationData?.schedule_code,
192
+ showABMP: inputMemberData.migrationData?.show_member_since || false,
193
+ locHash: generateGeoHash(inputMemberData.addresses || []),
194
+ ...DEFAULT_MEMBER_DISPLAY_SETTINGS,
195
+ };
196
+ }
197
+ /**
198
+ * Enriches member data with optional migration properties
199
+ * @param {Object} memberDataToUpdate - Member data object to enhance
200
+ * @param {Object} migrationData - Migration data containing optional properties
201
+ */
202
+ function enrichWithMigrationData(memberDataToUpdate, migrationData) {
203
+ if (!migrationData) return;
204
+
205
+ memberDataToUpdate.addressInfo = migrationData.addressinfo;
206
+
207
+ if (migrationData.website) {
208
+ memberDataToUpdate.website = migrationData.website;
209
+ memberDataToUpdate.showWebsite = true;
210
+ }
211
+
212
+ if (migrationData.interests) {
213
+ memberDataToUpdate.areasOfPractices = processInterests(migrationData.interests);
214
+ }
215
+ }
216
+ /**
217
+ * Processes interests string into clean array
218
+ * @param {string} interestsString - Comma-separated interests string
219
+ * @returns {Array} - Array of trimmed, non-empty interests
220
+ */
221
+ function processInterests(interestsString) {
222
+ if (!interestsString) return [];
223
+
224
+ return interestsString
225
+ .split(',')
226
+ .map(interest => interest.trim())
227
+ .filter(interest => interest.length > 0);
228
+ }
229
+ /**
230
+ * Creates base member data structure with core properties
231
+ * @param {Object} inputMemberData - Raw member data from API
232
+ * @param {Object} existingDbMember - Existing member data from database
233
+ * @param {number} currentPageNumber - Current page number being processed
234
+ * @returns {Promise<Object|null>} - Structured base member data or null if required fields are missing
235
+ */
236
+ async function createCoreMemberData(inputMemberData, existingDbMember, currentPageNumber) {
237
+ if (!validateCoreMemberData(inputMemberData)) {
238
+ return null;
239
+ }
240
+
241
+ const getMemberEmails = () => {
242
+ // Update both loginEmail & contactFormEmail only for new members who don't exist in DB
243
+ // Note: PAC API member actions are not reliable, so we need to check if the member exists in DB to know if it's a new member or not
244
+ const newEmail = inputMemberData.email.trim();
245
+ const isMemberExistInDb = Boolean(existingDbMember?._id);
246
+ if (!isMemberExistInDb) {
247
+ return {
248
+ email: newEmail,
249
+ contactFormEmail: newEmail,
250
+ };
251
+ }
252
+ //If exists in DB then only update loginEmail for those who came in with action new based on logic below, otherwise we don't update emails
253
+ const isMemberReinstatedWithNewEmail =
254
+ inputMemberData.action === MEMBER_ACTIONS.NEW &&
255
+ newEmail &&
256
+ existingDbMember.email !== newEmail;
257
+ if (isMemberReinstatedWithNewEmail) {
258
+ // 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
259
+ return {
260
+ email: newEmail,
261
+ isLoginEmailChanged: true,
262
+ };
263
+ }
264
+ //If exists in DB but not reinstated with new email, then don't update emails
265
+ return {};
266
+ };
267
+
268
+ const newMemberFields = await getNewMemberOnlyFields(inputMemberData, existingDbMember);
269
+
270
+ return {
271
+ ...existingDbMember,
272
+
273
+ // Always update these core fields from API
274
+ action: inputMemberData.action,
275
+ licenses: inputMemberData.licenses || [],
276
+ memberships: inputMemberData.memberships,
277
+ pageNumber: currentPageNumber,
278
+ isVisible: inputMemberData.action !== MEMBER_ACTIONS.DROP,
279
+
280
+ // Handle Member emails
281
+ ...getMemberEmails(),
282
+
283
+ // Handle fields that should only be set for new members
284
+ ...newMemberFields,
285
+ };
286
+ }
287
+
288
+ module.exports = {
289
+ generateUpdatedMemberData,
290
+ };