abmp-npm 1.6.1 → 1.6.3

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