abmp-npm 10.3.7 → 10.3.9

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.
@@ -1,9 +1,9 @@
1
1
  const { taskManager } = require('psdev-task-manager');
2
2
 
3
3
  const { COLLECTIONS } = require('../../public/consts');
4
- const { COMPILED_FILTERS_FIELDS, CONFIG_KEYS } = require('../consts');
4
+ const { COMPILED_FILTERS_FIELDS, CONFIG_KEYS, LOGIN_EMAIL_SYNC_STATUS } = require('../consts');
5
+ const { changeWixMembersEmails, summarizeLoginEmailOutcomes } = require('../daily-pull/utils');
5
6
  const { wixData } = require('../elevated-modules');
6
- const { updateWixMemberLoginEmail } = require('../members-area-methods');
7
7
  const {
8
8
  getAllEmptyAboutYouMembers,
9
9
  getAllMembersWithExternalImages,
@@ -481,6 +481,7 @@ const syncMemberLoginEmails = async data => {
481
481
  membersToUpdate.push({
482
482
  ...member,
483
483
  email: newEmail,
484
+ previousLoginEmail: member.email,
484
485
  });
485
486
  }
486
487
 
@@ -497,14 +498,35 @@ const syncMemberLoginEmails = async data => {
497
498
 
498
499
  for (const chunk of updateChunks) {
499
500
  try {
500
- await bulkSaveMembers(chunk);
501
-
502
- for (const member of chunk) {
503
- await updateWixMemberLoginEmail(member, result);
504
- }
505
-
506
- result.successful += chunk.length;
507
- console.log(`✅ Successfully updated ${chunkIndex} ${chunk.length} members`);
501
+ // Change Wix login emails first; only advance the CMS login email for the ones that
502
+ // succeeded. Failures keep their previous email (CMS stays consistent with Wix) and are
503
+ // reported in result.errors for manual handling.
504
+ const outcomes = await changeWixMembersEmails(chunk);
505
+ const { failedMemberIds } = summarizeLoginEmailOutcomes(outcomes);
506
+ const toSave = chunk.map(member => {
507
+ const { previousLoginEmail, ...restMember } = member;
508
+ if (failedMemberIds.has(member.memberId) && previousLoginEmail !== undefined) {
509
+ return { ...restMember, email: previousLoginEmail };
510
+ }
511
+ return restMember;
512
+ });
513
+ await bulkSaveMembers(toSave);
514
+
515
+ outcomes.forEach(outcome => {
516
+ if (outcome.status === LOGIN_EMAIL_SYNC_STATUS.UPDATED) {
517
+ result.successful++;
518
+ } else if (outcome.status === LOGIN_EMAIL_SYNC_STATUS.FAILED) {
519
+ result.failed++;
520
+ result.errors.push({
521
+ memberId: outcome.memberId,
522
+ wixMemberId: outcome.wixMemberId,
523
+ desiredEmail: outcome.desiredEmail,
524
+ error: outcome.error,
525
+ });
526
+ } else {
527
+ result.skipped++;
528
+ }
529
+ });
508
530
  } catch (error) {
509
531
  console.error(`❌ Error updating chunk ${chunkIndex}:`, error);
510
532
  result.failed += chunk.length;
@@ -515,14 +537,8 @@ const syncMemberLoginEmails = async data => {
515
537
  });
516
538
  }
517
539
  }
518
- // Log comprehensive results including Wix member updates
519
- const wixStats = result.wixMemberUpdates || { successful: 0, failed: 0 };
520
- console.log(`Login Emails sync task completed:`);
521
- console.log(
522
- ` - Member data updates: ${result.successful} successful, ${result.failed} failed, ${result.skipped} skipped`
523
- );
524
540
  console.log(
525
- ` - Wix member login emails: ${wixStats.successful} successful, ${wixStats.failed} failed`
541
+ `Login Emails sync task completed: ${result.successful} synced, ${result.failed} failed, ${result.skipped} skipped`
526
542
  );
527
543
 
528
544
  return result;
package/backend/utils.js CHANGED
@@ -210,6 +210,46 @@ function formatDateOnly(dateStr) {
210
210
 
211
211
  const runIf = (condition, asyncFn) => (condition ? asyncFn() : Promise.resolve(null));
212
212
 
213
+ /**
214
+ * Whether an error looks like a transient network failure that is safe to retry,
215
+ * e.g. undici's generic "fetch failed" thrown by the Wix SDKs, connection resets
216
+ * or DNS hiccups under heavy load.
217
+ * @param {Error} error
218
+ * @returns {boolean}
219
+ */
220
+ const isTransientNetworkError = error => {
221
+ const message = `${error?.message || ''} ${error?.cause?.message || ''} ${error?.code || ''} ${error?.cause?.code || ''}`;
222
+ return /fetch failed|ECONNRESET|ETIMEDOUT|ECONNREFUSED|EAI_AGAIN|EPIPE|UND_ERR|socket hang up|network error/i.test(
223
+ message
224
+ );
225
+ };
226
+
227
+ /**
228
+ * Runs an async operation, retrying with exponential backoff when it fails with a
229
+ * transient network error. Non-transient errors are rethrown immediately.
230
+ * @param {Function} operation - Async function to run
231
+ * @param {Object} [options]
232
+ * @param {number} [options.retries=2] - Maximum number of retries after the first attempt
233
+ * @param {number} [options.baseDelayMs=500] - Delay before the first retry, doubled each retry
234
+ * @returns {Promise<any>} - The operation's resolved value
235
+ */
236
+ const withTransientErrorRetry = async (operation, { retries = 2, baseDelayMs = 500 } = {}) => {
237
+ for (let attempt = 0; ; attempt++) {
238
+ try {
239
+ return await operation();
240
+ } catch (error) {
241
+ if (attempt >= retries || !isTransientNetworkError(error)) {
242
+ throw error;
243
+ }
244
+ const delayMs = baseDelayMs * 2 ** attempt;
245
+ console.warn(
246
+ `Transient network error (attempt ${attempt + 1}/${retries + 1}): ${error.message}. Retrying in ${delayMs}ms`
247
+ );
248
+ await new Promise(resolve => setTimeout(resolve, delayMs));
249
+ }
250
+ }
251
+ };
252
+
213
253
  module.exports = {
214
254
  getSiteConfigs,
215
255
  retrieveAllItems,
@@ -232,4 +272,6 @@ module.exports = {
232
272
  isPAC_STAFF,
233
273
  searchAllItems,
234
274
  runIf,
275
+ isTransientNetworkError,
276
+ withTransientErrorRetry,
235
277
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abmp-npm",
3
- "version": "10.3.7",
3
+ "version": "10.3.9",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "check-cycles": "madge --circular .",
@@ -35,8 +35,8 @@
35
35
  "@wix/automations": "^1.0.261",
36
36
  "@wix/crm": "^1.0.1061",
37
37
  "@wix/data": "^1.0.349",
38
- "@wix/essentials": "^0.1.28",
39
- "@wix/identity": "^1.0.178",
38
+ "@wix/essentials": "^1.0.7",
39
+ "@wix/identity": "^1.0.195",
40
40
  "@wix/media": "^1.0.213",
41
41
  "@wix/members": "^1.0.365",
42
42
  "@wix/secrets": "^1.0.62",
@@ -49,6 +49,7 @@
49
49
  "axios": "^1.13.1",
50
50
  "crypto": "^1.0.1",
51
51
  "csv-parser": "^3.0.0",
52
+ "debug": "^4.4.3",
52
53
  "jwt-js-decode": "^1.9.0",
53
54
  "lodash": "^4.17.21",
54
55
  "ngeohash": "^0.6.3",
package/pages/Home.js CHANGED
@@ -5,7 +5,6 @@ const { withWarmUpData } = require('psdev-utils/frontend');
5
5
 
6
6
  const { ADDRESS_STATUS_TYPES, DEFAULT_FILTER, DROPDOWN_OPTIONS } = require('../public/consts.js');
7
7
  const { createHomepageUtils } = require('../public/Utils/homePage.js');
8
- const { logHomePageLoadPhase } = require('../public/Utils/homePageLoadTrace.js');
9
8
  const {
10
9
  getMainAddress,
11
10
  formatPracticeAreasForDisplay,
@@ -14,8 +13,6 @@ const {
14
13
  normalizeExternalUrl,
15
14
  } = require('../public/Utils/sharedUtils.js');
16
15
 
17
- logHomePageLoadPhase('home_js_module_evaluated');
18
-
19
16
  let filter = JSON.parse(JSON.stringify(DEFAULT_FILTER));
20
17
  let dropDownOptions = JSON.parse(JSON.stringify(DROPDOWN_OPTIONS));
21
18
  let stateCityMap;
@@ -39,7 +36,6 @@ const homePageOnReady = async ({
39
36
  getNonCompiledFiltersOptions,
40
37
  filterProfiles,
41
38
  }) => {
42
- logHomePageLoadPhase('wix_onready_handler_entered');
43
39
  const {
44
40
  getParamsMapping,
45
41
  handlePagination,
@@ -62,9 +58,7 @@ const homePageOnReady = async ({
62
58
  detectMobile();
63
59
  initPageUI();
64
60
  attachEventListeners();
65
- logHomePageLoadPhase('before_handleUrlParams');
66
61
  await handleUrlParams();
67
- logHomePageLoadPhase('after_handleUrlParams');
68
62
 
69
63
  async function detectMobile() {
70
64
  try {
@@ -294,9 +288,7 @@ const homePageOnReady = async ({
294
288
  }
295
289
 
296
290
  async function applyFilterToUI(isDefaultStateParams) {
297
- logHomePageLoadPhase('applyFilterToUI_start', { isDefaultStateParams });
298
291
  const renderingEnv = await rendering.env();
299
- logHomePageLoadPhase('applyFilterToUI_rendering_env', { env: renderingEnv });
300
292
  const setFilterFromParams = async (isInitializeValue = true) => {
301
293
  const params = await wixLocation.query();
302
294
  console.log('params inside setFilterFromParams ', params);
@@ -330,9 +322,7 @@ const homePageOnReady = async ({
330
322
  await setFilterFromParams(true);
331
323
  if (isDefaultStateParams) {
332
324
  console.log('default state set for nearby');
333
- logHomePageLoadPhase('applyFilterToUI_default_path_fetch_and_nearby_start');
334
325
  await Promise.all([fetchFilterData(), nearByHandler(true)]);
335
- logHomePageLoadPhase('applyFilterToUI_default_path_complete');
336
326
  return;
337
327
  }
338
328
  console.log('not default state');
@@ -351,15 +341,12 @@ const homePageOnReady = async ({
351
341
  : () => updateResults('filterTimeout', true);
352
342
  console.log('filter ..', filter);
353
343
  try {
354
- logHomePageLoadPhase('applyFilterToUI_non_default_path_start');
355
344
  await Promise.all([
356
345
  fetchFilterData().then(() => setFilterFromParams(false)),
357
346
  //TODO: remove this workaround to fix issue with SSR showing invalid results
358
347
  renderingEnv === 'backend' ? Promise.resolve() : searchPromise(),
359
348
  ]);
360
- logHomePageLoadPhase('applyFilterToUI_non_default_path_complete');
361
349
  } catch (error) {
362
- logHomePageLoadPhase('applyFilterToUI_error', { message: String(error && error.message) });
363
350
  console.error('[applyFilterToUI] failed with error:', error);
364
351
  multiStateBoxSelector.changeState('errorState');
365
352
  }
@@ -438,7 +425,6 @@ const homePageOnReady = async ({
438
425
  }
439
426
  // NEAR BY FILTER
440
427
  async function nearByHandler(preservePagination = false) {
441
- logHomePageLoadPhase('nearByHandler_start', { preservePagination });
442
428
  const isSearchingNearby = _$w('#nearBy').checked;
443
429
  const renderingEnv = await rendering.env();
444
430
  // 1. Disable nearby input while processing
@@ -452,7 +438,6 @@ const homePageOnReady = async ({
452
438
  filter = newFilter;
453
439
  console.log('filter inside nearByHandler', filter);
454
440
  if (!success) {
455
- logHomePageLoadPhase('nearByHandler_geolocation_failed');
456
441
  if (renderingEnv !== 'backend') {
457
442
  //on Backend environment, geolocation API don't work, so makes no sense to change state for near by
458
443
  multiStateBoxSelector.changeState('nearByState');
@@ -467,7 +452,6 @@ const homePageOnReady = async ({
467
452
  // If location is not selected, change state to "resultsState"
468
453
  if (!isSearchingNearby) {
469
454
  if (await noSearchCriteria()) {
470
- logHomePageLoadPhase('nearByHandler_no_search_criteria');
471
455
  multiStateBoxSelector.changeState('noSearchCriteria');
472
456
  // 4. Re-enable nearby input
473
457
  _$w('#nearBy').enable();
@@ -479,7 +463,6 @@ const homePageOnReady = async ({
479
463
 
480
464
  // 4. Re-enable nearby input when done
481
465
  _$w('#nearBy').enable();
482
- logHomePageLoadPhase('nearByHandler_complete', { success: true });
483
466
  return true;
484
467
  }
485
468
 
@@ -487,7 +470,6 @@ const homePageOnReady = async ({
487
470
  // FETCH STATE/CITY/AREAS OF PRACTICE FROM BACKEND ONCE AND STORE IT
488
471
 
489
472
  async function fetchFilterData() {
490
- logHomePageLoadPhase('fetchFilterData_start');
491
473
  let completeStateList, areasOfPracticesList, stateCityMapList;
492
474
  try {
493
475
  const { COMPILED_STATE_LIST, COMPILED_AREAS_OF_PRACTICES, COMPILED_STATE_CITY_MAP } =
@@ -528,7 +510,6 @@ const homePageOnReady = async ({
528
510
 
529
511
  // Update filter states after data is loaded
530
512
  updateFiltersState();
531
- logHomePageLoadPhase('fetchFilterData_complete');
532
513
  }
533
514
 
534
515
  // CONSTRUCT DROPDOWN OPTIONS FOR STATE, CITY, AREA OF PRACTICES
@@ -3,7 +3,6 @@ const { window: wixWindow, rendering } = require('@wix/site-window');
3
3
 
4
4
  const { DEFAULT_FILTER } = require('../consts.js');
5
5
 
6
- const { logHomePageLoadPhase } = require('./homePageLoadTrace.js');
7
6
  const { debouncedFunction } = require('./sharedUtils.js');
8
7
 
9
8
  function isValidGeolocation(lat, lng) {
@@ -400,8 +399,6 @@ const createHomepageUtils = (_$w, filterProfiles) => {
400
399
  });
401
400
  }
402
401
  async function getAndSetUserLocation(isSearchingNearby, filter) {
403
- logHomePageLoadPhase('getAndSetUserLocation_start', { isSearchingNearby });
404
-
405
402
  const { latitude: existingLat, longitude: existingLng } = filter;
406
403
  if (isValidGeolocation(existingLat, existingLng)) {
407
404
  return {
@@ -411,23 +408,16 @@ const createHomepageUtils = (_$w, filterProfiles) => {
411
408
  }
412
409
 
413
410
  try {
414
- logHomePageLoadPhase('getAndSetUserLocation_before_getCurrentGeolocation');
415
411
  const location = await getCurrentGeolocationWithTimeout();
412
+
416
413
  console.log('location inside getAndSetUserLocation', location);
417
414
  const userLat = location.coords?.latitude ?? 0;
418
415
  const userLong = location.coords?.longitude ?? 0;
419
- logHomePageLoadPhase('getAndSetUserLocation_success', {
420
- lat: userLat,
421
- lng: userLong,
422
- });
423
416
  return {
424
417
  success: true,
425
418
  filter: applyGeolocationToFilter(filter, userLat, userLong, isSearchingNearby),
426
419
  };
427
420
  } catch (error) {
428
- logHomePageLoadPhase('getAndSetUserLocation_error', {
429
- message: String(error && error.message),
430
- });
431
421
  console.warn('Failed to get user location in getAndSetUserLocation', error);
432
422
  return { success: false, filter };
433
423
  }
@@ -546,7 +536,6 @@ const createHomepageUtils = (_$w, filterProfiles) => {
546
536
  );
547
537
  }
548
538
  async function parseAndValidateQueryParams(filter, pagination) {
549
- logHomePageLoadPhase('parseAndValidateQueryParams_start');
550
539
  const params = await wixLocation.query();
551
540
  const paramsMapping = getParamsMapping(filter, pagination);
552
541
  const {
@@ -558,11 +547,6 @@ const createHomepageUtils = (_$w, filterProfiles) => {
558
547
  const isSearchingNearby = params.nearby === 'true';
559
548
  const isNoParams = !withoutPreviewParams || Object.keys(withoutPreviewParams).length === 0;
560
549
  const { success, filter: newFilter } = await getAndSetUserLocation(isSearchingNearby, filter);
561
- logHomePageLoadPhase('parseAndValidateQueryParams_after_geolocation', {
562
- isNoParams,
563
- isSearchingNearby,
564
- success,
565
- });
566
550
 
567
551
  // Auto-enable nearby if GPS permission granted on fresh page load
568
552
  if (
@@ -573,7 +557,6 @@ const createHomepageUtils = (_$w, filterProfiles) => {
573
557
  !isSearchingNearby
574
558
  ) {
575
559
  await wixQueryParams.add({ nearby: 'true', page: '1' });
576
- logHomePageLoadPhase('parseAndValidateQueryParams_return', { branch: 'auto_nearby_url' });
577
560
  return { isDefaultStateParams: true, filter: newFilter };
578
561
  }
579
562
 
@@ -587,7 +570,6 @@ const createHomepageUtils = (_$w, filterProfiles) => {
587
570
  // });
588
571
  // Don't search yet - let the caller decide what to do
589
572
  // The search will be handled in applyFilterToUI
590
- logHomePageLoadPhase('parseAndValidateQueryParams_return', { branch: 'default_no_params' });
591
573
  return { isDefaultStateParams: true, filter: newFilter };
592
574
  }
593
575
  let autoAdjustFilters = false;
@@ -627,10 +609,6 @@ const createHomepageUtils = (_$w, filterProfiles) => {
627
609
  withoutPreviewParams.page) ||
628
610
  (Object.keys(withoutPreviewParams).length === 1 && withoutPreviewParams.nearby);
629
611
  const isDefaultStateParams = isNoParams || isNearbyFilter;
630
- logHomePageLoadPhase('parseAndValidateQueryParams_return', {
631
- branch: 'with_query_params',
632
- isDefaultStateParams,
633
- });
634
612
  return { isDefaultStateParams, filter: newFilter };
635
613
  }
636
614
 
@@ -714,7 +692,6 @@ const createHomepageUtils = (_$w, filterProfiles) => {
714
692
  isSearchingNearby,
715
693
  preservePagination = false,
716
694
  }) {
717
- logHomePageLoadPhase('search_start', { timeoutType, isSearchingNearby });
718
695
  const multiStateBoxSelector = _$w('#resultsStateBox');
719
696
  const renderingEnv = await rendering.env();
720
697
  const initSearchResultsUI = () => {
@@ -736,7 +713,6 @@ const createHomepageUtils = (_$w, filterProfiles) => {
736
713
  longitude: 0,
737
714
  }) === JSON.stringify(DEFAULT_FILTER)
738
715
  ) {
739
- logHomePageLoadPhase('search_short_circuit_no_criteria');
740
716
  multiStateBoxSelector.changeState('noSearchCriteria');
741
717
  return [];
742
718
  }
@@ -760,19 +736,14 @@ const createHomepageUtils = (_$w, filterProfiles) => {
760
736
  timeoutType,
761
737
  args: { filter, isSearchingNearby },
762
738
  });
763
- logHomePageLoadPhase('search_before_filterProfiles', { renderingEnv });
764
739
  const { success, response, error } = await funcPromise();
765
740
  if (!success) {
766
741
  _$w('#numberOfResults').text = '';
767
742
  console.error('[search] failed with error:', error);
768
- logHomePageLoadPhase('search_filterProfiles_failed', {
769
- message: String(error && error.message),
770
- });
771
743
  multiStateBoxSelector.changeState('errorState');
772
744
  return [];
773
745
  }
774
746
  const totalCount = response.items.length;
775
- logHomePageLoadPhase('search_filterProfiles_success', { totalCount });
776
747
  if (!totalCount) {
777
748
  _$w('#numberOfResults').text = 'Showing 0 results';
778
749
  _$w('#noResultsMessage').text = `${
@@ -195,6 +195,28 @@ function normalizeExternalUrl(url) {
195
195
  return `https://${trimmed}`;
196
196
  }
197
197
 
198
+ /**
199
+ * Normalizes an email for comparison (lowercased + trimmed).
200
+ * Wix CRM treats emails as case-insensitive, so comparisons must too.
201
+ * @param {string} email
202
+ * @returns {string}
203
+ */
204
+ function normalizeEmail(email) {
205
+ return typeof email === 'string' ? email.trim().toLowerCase() : '';
206
+ }
207
+
208
+ /**
209
+ * Case-insensitive email equality. Two empty/missing emails are not considered a match.
210
+ * @param {string} a
211
+ * @param {string} b
212
+ * @returns {boolean}
213
+ */
214
+ function emailsMatch(a, b) {
215
+ const normalizedA = normalizeEmail(a);
216
+ const normalizedB = normalizeEmail(b);
217
+ return Boolean(normalizedA) && normalizedA === normalizedB;
218
+ }
219
+
198
220
  module.exports = {
199
221
  checkAddressIsVisible,
200
222
  formatPracticeAreasForDisplay,
@@ -208,4 +230,6 @@ module.exports = {
208
230
  formatAddress,
209
231
  isWixHostedImage,
210
232
  normalizeExternalUrl,
233
+ normalizeEmail,
234
+ emailsMatch,
211
235
  };
@@ -1,24 +0,0 @@
1
- const { loginQAMember } = require('./qa-login-methods');
2
- const { authenticateSSOToken } = require('./sso-methods');
3
-
4
- /**
5
- * Creates login methods with injected generateSessionToken dependency
6
- * @param {Function} generateSessionToken - The Velo generateSessionToken function to inject
7
- * @returns {Object} Object containing loginQAMember and authenticateSSOToken methods
8
- */
9
- const createLoginMethods = generateSessionToken => {
10
- //There is no generateSessionToken SDK version, and the signOn of @wix/identity returns 403 error regardless that the permissions are valid
11
- //Therefore, as a workaround we need to inject the Velo version of generateSessionToken to the login methods.
12
- const injectGenerateSessionTokenToMethod =
13
- method =>
14
- async (...args) =>
15
- await method(...args, generateSessionToken);
16
- return {
17
- loginQAMember: injectGenerateSessionTokenToMethod(loginQAMember),
18
- authenticateSSOToken: injectGenerateSessionTokenToMethod(authenticateSSOToken),
19
- };
20
- };
21
-
22
- module.exports = {
23
- createLoginMethods,
24
- };
@@ -1,58 +0,0 @@
1
- /**
2
- * One session per full page load. First log call creates `loadId` (send this to support for GCL search).
3
- * Logs a plain object so DevTools shows an expandable tree; `loadId` is still easy to copy for GCL.
4
- */
5
-
6
- function randomSegment() {
7
- return Math.random().toString(36).slice(2, 10);
8
- }
9
-
10
- function nowMs() {
11
- if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
12
- return performance.now();
13
- }
14
- return Date.now();
15
- }
16
-
17
- let session = null;
18
-
19
- function ensureSession() {
20
- if (session) {
21
- return session;
22
- }
23
- const t0 = nowMs();
24
- session = {
25
- loadId: `hpl_${Date.now()}_${randomSegment()}_${randomSegment()}`,
26
- t0,
27
- };
28
- return session;
29
- }
30
-
31
- /**
32
- * @param {string} phase
33
- * @param {Record<string, unknown>} [detail]
34
- */
35
- function logHomePageLoadPhase(phase, detail) {
36
- const s = ensureSession();
37
- const elapsed = Math.round(nowMs() - s.t0);
38
- const payload = {
39
- type: 'HomePageLoad',
40
- loadId: s.loadId,
41
- phase,
42
- elapsedSinceStartMs: elapsed,
43
- wallTimeIso: new Date().toISOString(),
44
- };
45
- if (detail && typeof detail === 'object') {
46
- payload.detail = detail;
47
- }
48
- console.log('[HomePageLoad]', payload);
49
- }
50
-
51
- function getHomePageLoadId() {
52
- return ensureSession().loadId;
53
- }
54
-
55
- module.exports = {
56
- logHomePageLoadPhase,
57
- getHomePageLoadId,
58
- };