abmp-npm 1.8.4 → 1.8.5

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/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  module.exports = {
2
2
  ...require('./forms-methods'),
3
3
  ...require('./search-filters-methods'),
4
+ ...require('./personal-details-form-methods'),
4
5
  };
@@ -0,0 +1,399 @@
1
+ const { crm } = require('@wix/crm');
2
+ const { items } = require('@wix/data');
3
+ const { auth } = require('@wix/essentials');
4
+ const { members: membersSDK, authentication } = require('@wix/members');
5
+ const { encode } = require('ngeohash');
6
+
7
+ const { COLLECTIONS } = require('../public/consts');
8
+
9
+ const { wixData } = require('./elevated-modules');
10
+ const { findMemberByWixDataId } = require('./members-data-methods');
11
+ const { retrieveAllItems } = require('./utils');
12
+
13
+ // Elevated functions
14
+ const elevatedGetCurrentMember = auth.elevate(authentication.currentMember.getCurrentMember);
15
+ const elevatedUpdateContact = auth.elevate(crm.contacts.updateContact);
16
+
17
+ // Constants
18
+ const MEMBER_ACTIONS = {
19
+ DROP: 'drop',
20
+ };
21
+
22
+ const PRECISION = 3;
23
+
24
+ /**
25
+ * Helper functions
26
+ */
27
+
28
+ function generateGeoHash(addresses) {
29
+ const geohash = addresses
30
+ ?.filter(address => (isNaN(address?.latitude) && isNaN(address?.longitude) ? false : address))
31
+ ?.map(address => encode(address.latitude, address.longitude, PRECISION));
32
+ return geohash && geohash.length > 0 ? geohash : [];
33
+ }
34
+
35
+ function formatDateToMonthYear(dateString) {
36
+ if (!dateString) return '';
37
+
38
+ const date = new Date(dateString);
39
+ if (isNaN(date.getTime())) return '';
40
+ const options = { year: 'numeric', month: 'long' };
41
+ return date.toLocaleDateString(undefined, options);
42
+ }
43
+
44
+ function hasStudentMembership(member, checkAssociation = false) {
45
+ const memberships = member?.memberships;
46
+ if (!Array.isArray(memberships)) return false;
47
+
48
+ const SITE_ASSOCIATION = 'ABMP';
49
+ const MEMBERSHIPS_TYPES = {
50
+ STUDENT: 'Student',
51
+ };
52
+
53
+ return memberships.some(membership => {
54
+ const isStudent = membership.membertype === MEMBERSHIPS_TYPES.STUDENT;
55
+ const hasCorrectAssociation = !checkAssociation || membership.association === SITE_ASSOCIATION;
56
+ return isStudent && hasCorrectAssociation;
57
+ });
58
+ }
59
+
60
+ function isStudent(member) {
61
+ return hasStudentMembership(member, false);
62
+ }
63
+
64
+ function getAddressDisplayOptions(member) {
65
+ const addresses = member.addresses || [];
66
+ const displayOptions = member.addressDisplayOption || [];
67
+ if (addresses.length === 1 && addresses[0].key) {
68
+ return [{ key: addresses[0].key, isMain: true }];
69
+ }
70
+ return displayOptions;
71
+ }
72
+
73
+ /**
74
+ * Retrieves all interests from the database
75
+ * @returns {Promise<Array<string>>} Sorted array of interest titles
76
+ */
77
+ const getInterestAll = async () => {
78
+ try {
79
+ const interests = await retrieveAllItems(COLLECTIONS.INTERESTS);
80
+ const titles = interests.map(x => x.title);
81
+
82
+ // Sort the interests alphabetically (case-insensitive)
83
+ return titles.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
84
+ } catch (e) {
85
+ console.error('Error in getInterestAll:', e);
86
+ throw e;
87
+ }
88
+ };
89
+
90
+ /**
91
+ * Get member by slug with various filtering options
92
+ * @param {Object} options - Query options
93
+ * @param {string} options.slug - The URL slug to search for
94
+ * @param {boolean} options.excludeDropped - Whether to exclude dropped members
95
+ * @param {boolean} options.excludeSearchedMember - Whether to exclude a specific member
96
+ * @param {string} options.memberId - Member ID to exclude
97
+ * @returns {Promise<Object|null>} Member data or null
98
+ */
99
+ async function getMemberBySlug({
100
+ slug,
101
+ excludeDropped = true,
102
+ excludeSearchedMember = false,
103
+ memberId = null,
104
+ }) {
105
+ if (!slug) return null;
106
+
107
+ try {
108
+ let query = wixData.query(COLLECTIONS.MEMBERS_DATA).contains('url', slug);
109
+
110
+ if (excludeDropped) {
111
+ query = query.ne('action', 'drop');
112
+ }
113
+
114
+ if (excludeSearchedMember && memberId) {
115
+ query = query.ne('memberId', memberId);
116
+ }
117
+
118
+ const result = await query.find();
119
+ const matchingMembers = result.items.filter(
120
+ item => item.url && item.url.toLowerCase() === slug.toLowerCase()
121
+ );
122
+
123
+ if (matchingMembers.length > 1) {
124
+ console.warn(`Multiple members found with same slug ${slug}`);
125
+ }
126
+
127
+ return matchingMembers[0] || null;
128
+ } catch (error) {
129
+ console.error('Error getting member by slug:', error);
130
+ throw error;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Checks if a URL already exists for a different member
136
+ * @param {string} url - The URL to check
137
+ * @param {string} excludeMemberId - Member ID to exclude from check
138
+ * @returns {Promise<boolean>} True if URL exists for another member
139
+ */
140
+ async function urlExists(url, excludeMemberId) {
141
+ if (!url) return false;
142
+
143
+ try {
144
+ const member = await getMemberBySlug({
145
+ slug: url,
146
+ excludeDropped: true,
147
+ excludeSearchedMember: true,
148
+ memberId: excludeMemberId,
149
+ });
150
+
151
+ return member !== null;
152
+ } catch (error) {
153
+ console.error('Error checking URL existence:', error);
154
+ return false;
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Validates member token and returns member data
160
+ * @param {string} memberIdInput - The member ID to validate
161
+ * @returns {Promise<Object>} Object with memberData and isValid flag
162
+ */
163
+ const validateMemberToken = async memberIdInput => {
164
+ const invalidTokenResponse = { memberData: null, isValid: false };
165
+
166
+ if (!memberIdInput) {
167
+ return invalidTokenResponse;
168
+ }
169
+
170
+ try {
171
+ const member = await elevatedGetCurrentMember();
172
+
173
+ if (!member || !member.contactId) {
174
+ console.log('member not found from getCurrentMember for memberIdInput', memberIdInput);
175
+ return invalidTokenResponse;
176
+ }
177
+
178
+ const result = await wixData
179
+ .query(COLLECTIONS.MEMBERS_DATA)
180
+ .eq('contactId', member.contactId)
181
+ .find();
182
+
183
+ console.log('items', result.items[0]);
184
+ console.log('member.contactId', member.contactId);
185
+
186
+ if (!result.items[0]?._id) {
187
+ const errorMessage = `No record found in DB for logged in Member [Corrupted Data - Duplicate Members?] - There is no match in DB for currentMember: ${JSON.stringify({ memberIdInput, currentMemberId: member.contactId })}`;
188
+ console.error(errorMessage);
189
+ throw new Error('CORRUPTED_MEMBER_DATA');
190
+ }
191
+
192
+ console.log(`Id found in DB for memberIdInput :${memberIdInput} is ${result.items[0]?._id}`);
193
+
194
+ const memberData = result.items[0];
195
+ memberData.memberships = memberData.memberships.map(membership => ({
196
+ ...membership,
197
+ membersince: formatDateToMonthYear(membership.membersince),
198
+ }));
199
+
200
+ const savedMemberId = memberData?._id;
201
+ const isValid = savedMemberId === memberIdInput;
202
+
203
+ if (!savedMemberId || !isValid) {
204
+ return invalidTokenResponse;
205
+ }
206
+
207
+ if (memberData.action === MEMBER_ACTIONS.DROP) {
208
+ return invalidTokenResponse;
209
+ }
210
+
211
+ memberData.addressDisplayOption = getAddressDisplayOptions(memberData);
212
+ console.log('memberData', memberData);
213
+ memberData.isStudent = isStudent(memberData);
214
+
215
+ return { memberData, isValid };
216
+ } catch (error) {
217
+ console.error('Error in validateMemberToken:', error);
218
+ throw error;
219
+ }
220
+ };
221
+
222
+ /**
223
+ * Checks if URL is unique for the member
224
+ * @param {string} url - The URL to check
225
+ * @param {string} memberId - The member ID
226
+ * @returns {Promise<Object>} Object with isUnique flag
227
+ */
228
+ const checkUrlUniqueness = async (url, memberId) => {
229
+ if (!url || !memberId) {
230
+ throw new Error('Missing required parameters: url and memberId are required');
231
+ }
232
+
233
+ try {
234
+ const trimmedUrl = url.trim();
235
+ const exists = await urlExists(trimmedUrl, memberId);
236
+
237
+ return { isUnique: !exists };
238
+ } catch (error) {
239
+ console.error('Error checking URL uniqueness:', error);
240
+ throw new Error(`Failed to check URL uniqueness: ${error.message}`);
241
+ }
242
+ };
243
+
244
+ /**
245
+ * Updates contact email in Wix CRM
246
+ * @param {string} contactId - The contact ID
247
+ * @param {string} newEmail - The new email
248
+ */
249
+ async function updateContactEmail(contactId, newEmail) {
250
+ if (!newEmail) {
251
+ throw new Error('New email is required');
252
+ }
253
+
254
+ try {
255
+ const contact = await crm.contacts.getContact(contactId);
256
+ const currentInfo = contact.info || {};
257
+
258
+ await elevatedUpdateContact(contactId, {
259
+ info: {
260
+ ...currentInfo,
261
+ emails: {
262
+ items: [
263
+ {
264
+ email: newEmail,
265
+ primary: true,
266
+ },
267
+ ],
268
+ },
269
+ },
270
+ });
271
+ } catch (error) {
272
+ console.error('Error updating contact email:', error);
273
+ throw new Error(`Failed to update contact email: ${error.message}`);
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Updates contact names in Wix CRM
279
+ * @param {string} contactId - The contact ID
280
+ * @param {string} firstName - The new first name
281
+ * @param {string} lastName - The new last name
282
+ */
283
+ async function updateContactNames(contactId, firstName, lastName) {
284
+ if (!firstName && !lastName) {
285
+ throw new Error('At least one name field is required');
286
+ }
287
+
288
+ try {
289
+ const contact = await crm.contacts.getContact(contactId);
290
+ const currentInfo = contact.info || {};
291
+
292
+ await elevatedUpdateContact(contactId, {
293
+ info: {
294
+ ...currentInfo,
295
+ name: {
296
+ first: firstName || currentInfo?.name?.first || '',
297
+ last: lastName || currentInfo?.name?.last || '',
298
+ },
299
+ },
300
+ });
301
+ } catch (error) {
302
+ console.error('Error updating contact names:', error);
303
+ throw new Error(`Failed to update contact names: ${error.message}`);
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Update fields if they have changed
309
+ */
310
+ const updateIfChanged = (existingValues, newValues, updater, argsBuilder) => {
311
+ const hasChanged = !existingValues.every((val, idx) => val === newValues[idx]);
312
+ if (!hasChanged) return null;
313
+ return updater(...argsBuilder(newValues));
314
+ };
315
+
316
+ /**
317
+ * Updates member contact information in CRM if fields have changed
318
+ * @param {string} id - Member ID
319
+ * @param {Object} data - New member data
320
+ */
321
+ const updateMemberContactInfo = async (id, data) => {
322
+ const existing = await findMemberByWixDataId(id);
323
+ const { contactId } = existing;
324
+
325
+ const updateConfig = [
326
+ {
327
+ fields: ['contactFormEmail'],
328
+ updater: updateContactEmail,
329
+ args: ([email]) => [contactId, email],
330
+ },
331
+ {
332
+ fields: ['firstName', 'lastName'],
333
+ updater: updateContactNames,
334
+ args: ([firstName, lastName]) => [contactId, firstName, lastName],
335
+ },
336
+ ];
337
+
338
+ const updatePromises = updateConfig
339
+ .map(({ fields, updater, args }) => {
340
+ const existingValues = fields.map(field => existing[field]);
341
+ const newValues = fields.map(field => data[field]);
342
+ return updateIfChanged(existingValues, newValues, updater, args);
343
+ })
344
+ .filter(Boolean);
345
+
346
+ await Promise.all(updatePromises);
347
+ };
348
+
349
+ /**
350
+ * Saves registration data for a member
351
+ * @param {Object} data - Member data to save
352
+ * @param {string} id - Member ID
353
+ * @returns {Promise<Object>} Result object with type and data
354
+ */
355
+ const saveRegistrationData = async (data, id) => {
356
+ try {
357
+ console.log(' saveRegistrationData data._id', data._id);
358
+ console.log(' saveRegistrationData id', id);
359
+
360
+ if (data._id !== id) return { type: 'notAuthorized' };
361
+
362
+ if (data.url) {
363
+ const isDuplicate = await urlExists(data.url, data.memberId);
364
+
365
+ if (isDuplicate) {
366
+ return {
367
+ type: 'error',
368
+ error: 'URL slug is already taken. Please choose a different one.',
369
+ };
370
+ }
371
+ }
372
+
373
+ if (data.addresses && Array.isArray(data.addresses)) {
374
+ data.locHash = generateGeoHash(data.addresses);
375
+ }
376
+
377
+ await updateMemberContactInfo(id, data);
378
+
379
+ const saveData = await wixData.update(COLLECTIONS.MEMBERS_DATA, data);
380
+
381
+ return {
382
+ type: 'success',
383
+ saveData,
384
+ };
385
+ } catch (error) {
386
+ console.error(error);
387
+ return {
388
+ type: 'error',
389
+ error,
390
+ };
391
+ }
392
+ };
393
+
394
+ module.exports = {
395
+ getInterestAll,
396
+ validateMemberToken,
397
+ checkUrlUniqueness,
398
+ saveRegistrationData,
399
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abmp-npm",
3
- "version": "1.8.4",
3
+ "version": "1.8.5",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -32,7 +32,9 @@
32
32
  "@wix/essentials": "^0.1.28",
33
33
  "@wix/members": "^1.0.330",
34
34
  "@wix/site-location": "^1.31.0",
35
+ "@wix/site-storage": "^1.22.0",
35
36
  "@wix/site-window": "^1.44.0",
37
+ "lodash": "^4.17.21",
36
38
  "ngeohash": "^0.6.3",
37
39
  "phone": "^3.1.67",
38
40
  "psdev-utils": "1.1.1"