abmp-npm 2.0.76 → 2.0.78

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.
Files changed (99) hide show
  1. package/package.json +8 -1
  2. package/pages/Home.js +27 -55
  3. package/pages/index.js +0 -1
  4. package/public/Utils/personalDetailsUtils.js +5 -1
  5. package/public/Utils/sharedUtils.js +32 -0
  6. package/public/consts.js +0 -1
  7. package/.claude/worktrees/fix-save-button-disable/.husky/pre-commit +0 -19
  8. package/.claude/worktrees/fix-save-button-disable/.prettierignore +0 -7
  9. package/.claude/worktrees/fix-save-button-disable/.prettierrc.json +0 -16
  10. package/.claude/worktrees/fix-save-button-disable/README.md +0 -1
  11. package/.claude/worktrees/fix-save-button-disable/backend/README-member-contact-flows.md +0 -136
  12. package/.claude/worktrees/fix-save-button-disable/backend/__tests__/daily-pull-execution-check.test.js +0 -124
  13. package/.claude/worktrees/fix-save-button-disable/backend/__tests__/ensure-unique-urls-in-batch.test.js +0 -129
  14. package/.claude/worktrees/fix-save-button-disable/backend/__tests__/login-email-sync.test.js +0 -49
  15. package/.claude/worktrees/fix-save-button-disable/backend/__tests__/transient-retry-and-bulk-lookup.test.js +0 -129
  16. package/.claude/worktrees/fix-save-button-disable/backend/__tests__/url-uniqueness.test.js +0 -194
  17. package/.claude/worktrees/fix-save-button-disable/backend/automations-methods.js +0 -13
  18. package/.claude/worktrees/fix-save-button-disable/backend/cms-data-methods.js +0 -251
  19. package/.claude/worktrees/fix-save-button-disable/backend/consts.js +0 -58
  20. package/.claude/worktrees/fix-save-button-disable/backend/contacts-methods.js +0 -67
  21. package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/bulk-process-methods.js +0 -222
  22. package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/consts.js +0 -39
  23. package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/index.js +0 -5
  24. package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/process-member-methods.js +0 -213
  25. package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/schedule-methods.js +0 -41
  26. package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/sync-to-cms-methods.js +0 -161
  27. package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/utils.js +0 -151
  28. package/.claude/worktrees/fix-save-button-disable/backend/data-hooks.js +0 -29
  29. package/.claude/worktrees/fix-save-button-disable/backend/dev-only-methods.js +0 -58
  30. package/.claude/worktrees/fix-save-button-disable/backend/elevated-modules.js +0 -18
  31. package/.claude/worktrees/fix-save-button-disable/backend/forms-methods.js +0 -52
  32. package/.claude/worktrees/fix-save-button-disable/backend/http-functions/httpFunctions.js +0 -86
  33. package/.claude/worktrees/fix-save-button-disable/backend/http-functions/index.js +0 -3
  34. package/.claude/worktrees/fix-save-button-disable/backend/http-functions/interests.js +0 -35
  35. package/.claude/worktrees/fix-save-button-disable/backend/index.js +0 -19
  36. package/.claude/worktrees/fix-save-button-disable/backend/jobs.js +0 -158
  37. package/.claude/worktrees/fix-save-button-disable/backend/login/generate-member-session-token.js +0 -27
  38. package/.claude/worktrees/fix-save-button-disable/backend/login/index.js +0 -8
  39. package/.claude/worktrees/fix-save-button-disable/backend/login/qa-login-methods.js +0 -54
  40. package/.claude/worktrees/fix-save-button-disable/backend/login/sso-methods.js +0 -159
  41. package/.claude/worktrees/fix-save-button-disable/backend/member-contact-orchestration.js +0 -111
  42. package/.claude/worktrees/fix-save-button-disable/backend/members-area-methods.js +0 -88
  43. package/.claude/worktrees/fix-save-button-disable/backend/members-data-methods.js +0 -789
  44. package/.claude/worktrees/fix-save-button-disable/backend/pac-api-methods.js +0 -54
  45. package/.claude/worktrees/fix-save-button-disable/backend/routers/index.js +0 -3
  46. package/.claude/worktrees/fix-save-button-disable/backend/routers/methods.js +0 -191
  47. package/.claude/worktrees/fix-save-button-disable/backend/routers/utils.js +0 -128
  48. package/.claude/worktrees/fix-save-button-disable/backend/search-filters-methods.js +0 -124
  49. package/.claude/worktrees/fix-save-button-disable/backend/tasks/address-primary-methods.js +0 -256
  50. package/.claude/worktrees/fix-save-button-disable/backend/tasks/address-visibility-methods.js +0 -160
  51. package/.claude/worktrees/fix-save-button-disable/backend/tasks/consts.js +0 -34
  52. package/.claude/worktrees/fix-save-button-disable/backend/tasks/daily-pull-check-methods.js +0 -78
  53. package/.claude/worktrees/fix-save-button-disable/backend/tasks/email-normalize-methods.js +0 -134
  54. package/.claude/worktrees/fix-save-button-disable/backend/tasks/index.js +0 -10
  55. package/.claude/worktrees/fix-save-button-disable/backend/tasks/migration-methods.js +0 -46
  56. package/.claude/worktrees/fix-save-button-disable/backend/tasks/tasks-configs.js +0 -251
  57. package/.claude/worktrees/fix-save-button-disable/backend/tasks/tasks-helpers-methods.js +0 -433
  58. package/.claude/worktrees/fix-save-button-disable/backend/tasks/tasks-process-methods.js +0 -606
  59. package/.claude/worktrees/fix-save-button-disable/backend/tasks/url-migration-methods.js +0 -378
  60. package/.claude/worktrees/fix-save-button-disable/backend/tasks/url-space-fix-methods.js +0 -174
  61. package/.claude/worktrees/fix-save-button-disable/backend/utils.js +0 -277
  62. package/.claude/worktrees/fix-save-button-disable/dev-only-scripts/extract-duplicate-url-groups.js +0 -201
  63. package/.claude/worktrees/fix-save-button-disable/dev-only-scripts/find-duplicate-ids.js +0 -159
  64. package/.claude/worktrees/fix-save-button-disable/dev-only-scripts/find-duplicate-urls.js +0 -201
  65. package/.claude/worktrees/fix-save-button-disable/eslint.config.js +0 -120
  66. package/.claude/worktrees/fix-save-button-disable/index.js +0 -5
  67. package/.claude/worktrees/fix-save-button-disable/package-lock.json +0 -12160
  68. package/.claude/worktrees/fix-save-button-disable/package.json +0 -60
  69. package/.claude/worktrees/fix-save-button-disable/pages/ContactUs.js +0 -129
  70. package/.claude/worktrees/fix-save-button-disable/pages/Home.js +0 -779
  71. package/.claude/worktrees/fix-save-button-disable/pages/LearnMore.js +0 -27
  72. package/.claude/worktrees/fix-save-button-disable/pages/LoadingPage.js +0 -29
  73. package/.claude/worktrees/fix-save-button-disable/pages/Profile.js +0 -354
  74. package/.claude/worktrees/fix-save-button-disable/pages/QAPage.js +0 -39
  75. package/.claude/worktrees/fix-save-button-disable/pages/SaveAlerts.js +0 -13
  76. package/.claude/worktrees/fix-save-button-disable/pages/SelectBannerImages.js +0 -46
  77. package/.claude/worktrees/fix-save-button-disable/pages/deleteConfirm.js +0 -19
  78. package/.claude/worktrees/fix-save-button-disable/pages/index.js +0 -12
  79. package/.claude/worktrees/fix-save-button-disable/pages/personalDetails.js +0 -2486
  80. package/.claude/worktrees/fix-save-button-disable/public/Utils/homePage.js +0 -798
  81. package/.claude/worktrees/fix-save-button-disable/public/Utils/personalDetailsUtils.js +0 -25
  82. package/.claude/worktrees/fix-save-button-disable/public/Utils/sharedUtils.js +0 -235
  83. package/.claude/worktrees/fix-save-button-disable/public/consts.js +0 -113
  84. package/.claude/worktrees/fix-save-button-disable/public/index.js +0 -5
  85. package/.claude/worktrees/fix-save-button-disable/public/messages.js +0 -16
  86. package/.claude/worktrees/fix-save-button-disable/public/sso-auth-methods.js +0 -43
  87. package/.husky/pre-commit +0 -19
  88. package/.prettierignore +0 -7
  89. package/.prettierrc.json +0 -16
  90. package/backend/__tests__/daily-pull-execution-check.test.js +0 -124
  91. package/backend/__tests__/ensure-unique-urls-in-batch.test.js +0 -129
  92. package/backend/__tests__/login-email-sync.test.js +0 -49
  93. package/backend/__tests__/transient-retry-and-bulk-lookup.test.js +0 -129
  94. package/backend/__tests__/url-uniqueness.test.js +0 -194
  95. package/dev-only-scripts/extract-duplicate-url-groups.js +0 -201
  96. package/dev-only-scripts/find-duplicate-ids.js +0 -159
  97. package/dev-only-scripts/find-duplicate-urls.js +0 -201
  98. package/eslint.config.js +0 -120
  99. package/pages/contactForLocation.js +0 -28
package/package.json CHANGED
@@ -1,7 +1,14 @@
1
1
  {
2
2
  "name": "abmp-npm",
3
- "version": "2.0.76",
3
+ "version": "2.0.78",
4
4
  "main": "index.js",
5
+ "files": [
6
+ "index.js",
7
+ "backend",
8
+ "pages",
9
+ "public",
10
+ "!backend/__tests__"
11
+ ],
5
12
  "scripts": {
6
13
  "check-cycles": "madge --circular .",
7
14
  "test": "jest backend/__tests__",
package/pages/Home.js CHANGED
@@ -3,20 +3,15 @@ const { location: wixLocation } = require('@wix/site-location');
3
3
  const { window: wixWindow, rendering } = require('@wix/site-window');
4
4
  const { withWarmUpData } = require('psdev-utils/frontend');
5
5
 
6
- const {
7
- ADDRESS_STATUS_TYPES,
8
- DEFAULT_FILTER,
9
- DROPDOWN_OPTIONS,
10
- LIGHTBOX_NAMES,
11
- } = require('../public/consts.js');
6
+ const { ADDRESS_STATUS_TYPES, DEFAULT_FILTER, DROPDOWN_OPTIONS } = require('../public/consts.js');
12
7
  const { createHomepageUtils } = require('../public/Utils/homePage.js');
13
8
  const {
14
9
  getMainAddress,
15
- findMainAddress,
16
- checkAddressIsVisible,
17
10
  formatPracticeAreasForDisplay,
11
+ checkAddressIsVisible,
18
12
  isWixHostedImage,
19
13
  normalizeExternalUrl,
14
+ buildMapLink,
20
15
  } = require('../public/Utils/sharedUtils.js');
21
16
 
22
17
  let filter = JSON.parse(JSON.stringify(DEFAULT_FILTER));
@@ -36,28 +31,6 @@ const pagination = {
36
31
  let searchResults = [];
37
32
  let isMobile = false;
38
33
 
39
- const isMappable = addr =>
40
- addr?.addressStatus === ADDRESS_STATUS_TYPES.FULL_ADDRESS && addr.latitude && addr.longitude;
41
-
42
- // The button is only offered when the member has at least one mappable address, which is
43
- // the same condition as before the lightbox was introduced. When that address is not the
44
- // primary one we still show the button but withhold the link, so the click opens the
45
- // lightbox instead of sending the visitor to a secondary address in another city.
46
- const getDirectionsTarget = itemData => {
47
- const addresses = Array.isArray(itemData?.addresses) ? itemData.addresses : [];
48
- const showButton = checkAddressIsVisible(addresses).some(isMappable);
49
- if (!showButton) {
50
- return { showButton: false, mapsLink: null };
51
- }
52
- const mainAddr = findMainAddress(itemData?.addressDisplayOption, addresses);
53
- return {
54
- showButton: true,
55
- mapsLink: isMappable(mainAddr)
56
- ? `https://maps.google.com/?q=${mainAddr.latitude},${mainAddr.longitude}`
57
- : null,
58
- };
59
- };
60
-
61
34
  const homePageOnReady = async ({
62
35
  _$w,
63
36
  getCompiledFiltersOptions,
@@ -209,16 +182,6 @@ const homePageOnReady = async ({
209
182
  await updateResults('zeroTimeout');
210
183
  });
211
184
  const baseUrl = await wixLocation.baseUrl();
212
- _$w('#showMaps').onClick(event => {
213
- const member = searchResults.find(result => result._id === event.context.itemId);
214
- if (!member) {
215
- return;
216
- }
217
- const { showButton, mapsLink } = getDirectionsTarget(member);
218
- if (showButton && !mapsLink) {
219
- wixWindow.openLightbox(LIGHTBOX_NAMES.CONTACT_FOR_LOCATION, member);
220
- }
221
- });
222
185
  _$w('#profileRepeater').onItemReady(($item, itemData) => {
223
186
  // 1) safely default to arrays
224
187
  const addresses = Array.isArray(itemData.addresses) ? itemData.addresses : [];
@@ -263,24 +226,33 @@ const homePageOnReady = async ({
263
226
  $item('#milesAwayText').text = '';
264
227
  }
265
228
 
266
- // 7) "Show maps" button. Links to the primary address when it is a full address;
267
- // otherwise it stays visible and the page-level onClick opens a lightbox.
268
- const { showButton, mapsLink } = getDirectionsTarget(itemData);
229
+ // 7) "Show maps" button shown only when we can actually build a link for it
230
+ const visible = checkAddressIsVisible(addresses);
231
+ const fullAddressWithValidCoords = visible.find(
232
+ addr =>
233
+ addr.addressStatus === ADDRESS_STATUS_TYPES.FULL_ADDRESS &&
234
+ addr.latitude &&
235
+ addr.longitude
236
+ );
269
237
 
270
- if (!showButton) {
271
- $item('#showMaps').hide();
272
- } else {
238
+ // Links to the street address rather than the stored coordinates, which are
239
+ // unreliable for some members. The full-address filter above is what keeps
240
+ // this safe: members set to state_city_zip or dont_show never reach here, so
241
+ // a hidden street address is never put in a maps URL.
242
+ //
243
+ // Gate on the link rather than on the address. buildMapLink returns '' when
244
+ // it can build neither an address nor a coordinate link, and it also handles
245
+ // being passed undefined - so this cannot leave a visible button linking
246
+ // nowhere, even if the filter above is later relaxed.
247
+ const mapLink = buildMapLink(fullAddressWithValidCoords);
248
+
249
+ if (mapLink) {
273
250
  $item('#showMaps').enable();
274
251
  $item('#showMaps').show();
275
-
276
- if (mapsLink) {
277
- $item('#showMaps').link = mapsLink;
278
- $item('#showMaps').target = '_blank';
279
- } else {
280
- // Clear any link left over from a recycled repeater item, otherwise the
281
- // button would still navigate to the previous member's address.
282
- $item('#showMaps').link = undefined;
283
- }
252
+ $item('#showMaps').link = mapLink;
253
+ $item('#showMaps').target = '_blank';
254
+ } else {
255
+ $item('#showMaps').hide();
284
256
  }
285
257
 
286
258
  // 8) Phone / contact form
package/pages/index.js CHANGED
@@ -9,5 +9,4 @@ module.exports = {
9
9
  ...require('./deleteConfirm.js'),
10
10
  ...require('./SaveAlerts.js'),
11
11
  ...require('./LearnMore.js'),
12
- ...require('./contactForLocation.js'),
13
12
  };
@@ -13,8 +13,12 @@ function isNotValidUrl(url) {
13
13
  if (!url) return false;
14
14
 
15
15
  // URL must start with protocol OR www - handles all TLDs including multi-level and query params
16
+ // NOTE: the host class is `[\da-z.-]` (digit, letter, dot, hyphen). It previously read
17
+ // `[da-z.-]`, which - missing the backslash - matched only a literal "d" plus a-z, so any
18
+ // domain containing a digit was rejected (e.g. https://patty-10439.square.site).
19
+ // The `i` flag keeps mixed-case hosts valid; domains are case-insensitive.
16
20
  const urlRegex =
17
- /^(https?:\/\/|www\.)([da-z.-]+)\.([a-z.]{2,})([/\w .-]*)*(\?[&\w=.-]*)?(#[&\w=.-]*)?\/?$/;
21
+ /^(https?:\/\/|www\.)([\da-z.-]+)\.([a-z.]{2,})([/\w .-]*)*(\?[&\w=.-]*)?(#[&\w=.-]*)?\/?$/i;
18
22
 
19
23
  return !urlRegex.test(url);
20
24
  }
@@ -118,6 +118,37 @@ function formatAddress(item) {
118
118
  return addressParts.filter(Boolean).join(', ');
119
119
  }
120
120
 
121
+ /**
122
+ * Builds the outbound map link for an address.
123
+ *
124
+ * Prefers the street address over the stored coordinates. NetForum's address
125
+ * verifier (Cdyne) sometimes returns coordinates that are miles away from the
126
+ * real address, and some coordinates were edited by hand in the past to change
127
+ * directory ranking, so the address text is the more reliable of the two.
128
+ * Distance ranking and the "XX miles away" figure keep using the coordinates -
129
+ * that is a separate calculation (see calculateDistance) and is unaffected.
130
+ *
131
+ * Falls back to coordinates only when the address cannot be formatted, so the
132
+ * button never links nowhere.
133
+ *
134
+ * @param {Object} address - a single address entry
135
+ * @returns {string} map URL, or '' when neither an address nor coordinates exist
136
+ */
137
+ function buildMapLink(address) {
138
+ if (!address) return '';
139
+
140
+ const query = formatAddress(address);
141
+ if (query) {
142
+ return `https://maps.google.com/?q=${encodeURIComponent(query)}`;
143
+ }
144
+
145
+ if (isValidLocation(address)) {
146
+ return `https://maps.google.com/?q=${address.latitude},${address.longitude}`;
147
+ }
148
+
149
+ return '';
150
+ }
151
+
121
152
  /**
122
153
  * @param {Array} addressDisplayOption
123
154
  * @param {Array} addresses
@@ -228,6 +259,7 @@ module.exports = {
228
259
  toRadians,
229
260
  generateId,
230
261
  formatAddress,
262
+ buildMapLink,
231
263
  isWixHostedImage,
232
264
  normalizeExternalUrl,
233
265
  normalizeEmail,
package/public/consts.js CHANGED
@@ -85,7 +85,6 @@ const LIGHTBOX_NAMES = {
85
85
  SELECT_BANNER_IMAGES: 'Select Banner Images',
86
86
  CONTACT_US: 'Contact Us',
87
87
  MAIN_ADDRESS_ERROR: 'mainAddressError',
88
- CONTACT_FOR_LOCATION: 'contactForLocation',
89
88
  };
90
89
 
91
90
  const FREE_WEBSITE_TEXT_STATES = {
@@ -1,19 +0,0 @@
1
- echo "Running ESLint check..."
2
- npm run lint
3
-
4
- if [ $? -ne 0 ]; then
5
- echo "❌ ESLint check failed. Please fix the linting errors before committing."
6
- exit 1
7
- fi
8
-
9
- echo "Running Prettier check..."
10
- npm run format:check
11
-
12
- if [ $? -ne 0 ]; then
13
- echo "❌ Prettier check failed. Please format your code before committing."
14
- echo "Run 'npm run format' to automatically format your files."
15
- exit 1
16
- fi
17
-
18
- echo "✅ All checks passed!"
19
-
@@ -1,7 +0,0 @@
1
- node_modules
2
- dist
3
- build
4
- coverage
5
- *.min.js
6
- package-lock.json
7
-
@@ -1,16 +0,0 @@
1
- {
2
- "singleQuote": true,
3
- "trailingComma": "es5",
4
- "printWidth": 100,
5
- "tabWidth": 2,
6
- "semi": true,
7
- "bracketSpacing": true,
8
- "bracketSameLine": false,
9
- "arrowParens": "avoid",
10
- "endOfLine": "lf",
11
- "quoteProps": "as-needed",
12
- "jsxSingleQuote": false,
13
- "requirePragma": false,
14
- "insertPragma": false,
15
- "proseWrap": "preserve"
16
- }
@@ -1 +0,0 @@
1
- # abmp-npm
@@ -1,136 +0,0 @@
1
- # Member–Contact Flows
2
-
3
- How Wix **Members** (login identity) and **CRM Contacts** (form/submission identity) stay in sync in ABMP.
4
-
5
- ## Concepts
6
-
7
- | Concept | Meaning |
8
- | ---------------------- | ---------------------------------------------------------------------------------------------------------- |
9
- | **Single entity** | Member and contact use the same Wix ID (`wixContactId === wixMemberId`). One person, one record. |
10
- | **Separate contact** | Member has a login email; contact has a different “form” email. Two Wix entities: one Member, one Contact. |
11
- | **Login email** | `memberData.email` — used to sign in (Wix Member). |
12
- | **Contact form email** | `memberData.contactFormEmail` — email used on forms; can differ from login. |
13
-
14
- ---
15
-
16
- ## 1. Login / first-time flow: `createContactAndMemberIfNew`
17
-
18
- **Used when:** A user logs in or is created and we need to ensure they have both a Wix Member and (if needed) a CRM Contact.
19
-
20
- **File:** `members-data-methods.js` → `createContactAndMemberIfNew(memberData)`
21
-
22
- ### Logic in short
23
-
24
- - If no **Wix Member** → create one (login identity).
25
- - If no **Wix Contact** and contact form email **differs** from login email → create a separate Contact.
26
- - If contact form email **equals** login email → no separate contact; we use the member ID as the contact ID (single entity).
27
- - Persist `wixMemberId` and `wixContactId` on the member record.
28
-
29
- ### Flow diagram
30
-
31
- ```mermaid
32
- flowchart TD
33
- A[createContactAndMemberIfNew] --> B{Has wixMemberId?}
34
- B -->|No| C[Create Wix Member]
35
- B -->|Yes| D{Has wixContactId?}
36
- C --> D
37
- D -->|No| E{contactFormEmail !== email?}
38
- D -->|Yes| K[Keep existing IDs]
39
- E -->|Yes| F[Create Wix Contact]
40
- E -->|No| G[Use member ID as contact ID]
41
- F --> H[Resolve IDs]
42
- G --> H
43
- K --> H
44
- H[newWixContactId = createdContact OR memberId] --> I[Set wixMemberId, wixContactId on member]
45
- I --> J[updateMember + return]
46
- ```
47
-
48
- ### Parallel work
49
-
50
- ```
51
- ┌─────────────────────────────────────┐
52
- │ createContactAndMemberIfNew │
53
- └─────────────────────────────────────┘
54
-
55
- ┌───────────────────┴───────────────────┐
56
- │ Promise.all([ │
57
- │ runIf(needsWixMember, createMember), │
58
- │ runIf(needsContact && differentEmail, │
59
- │ createContact) │
60
- │ ]) │
61
- └───────────────────┬───────────────────┘
62
-
63
- newWixContactId = createdWixContactId || newWixMemberId
64
- (if no separate contact, member ID is the contact ID)
65
-
66
- ┌───────────────────┴───────────────────┐
67
- │ updateMember({ ...memberData, │
68
- │ wixMemberId, wixContactId }) │
69
- └───────────────────────────────────────┘
70
- ```
71
-
72
- ### Outcome
73
-
74
- | Scenario | wixMemberId | wixContactId |
75
- | --------------------------------------- | ------------- | ----------------------------------- |
76
- | New user, same email for login and form | New member ID | Same as wixMemberId (single entity) |
77
- | New user, form email ≠ login email | New member ID | New contact ID (separate contact) |
78
- | Existing user (already has both IDs) | Unchanged | Unchanged |
79
-
80
- ---
81
-
82
- ## 2. Update contact email flow: `updateContactEmail`
83
-
84
- **Used when:** User changes their contact/form email in the app and we need to update CRM and optionally the member record.
85
-
86
- **File:** `member-contact-orchestration.js` → `updateContactEmail(newContactEmail, existingMemberData)` (used by `updateMemberContactInfo`).
87
-
88
- ### Logic in short
89
-
90
- - **Single entity** (`wixContactId === wixMemberId`):
91
- - If new email **equals** login → no change.
92
- - If new email **differs** → create a new Contact with the new email and point the member to it (we now have a separate contact).
93
- - **Separate contact** (different IDs):
94
- - If new email **equals** login → delete the extra contact and set `wixContactId = wixMemberId` (collapse to single entity).
95
- - If new email **differs** → update the existing contact’s email in CRM.
96
-
97
- ### Flow diagram
98
-
99
- ```mermaid
100
- flowchart TD
101
- A[updateContactEmail] --> B{contactEmail === loginEmail?}
102
- B -->|Yes| C{Single entity?}
103
- B -->|No| D{Single entity?}
104
- C -->|Yes| E[No-op: already in sync]
105
- C -->|No| F[Delete contact\nSet wixContactId = wixMemberId]
106
- D -->|Yes| G[Create new Contact with new email\nUpdate member wixContactId]
107
- D -->|No| H[Update existing contact email in CRM]
108
- ```
109
-
110
- ### Decision table
111
-
112
- | Single entity? | New email vs login | Action |
113
- | -------------- | ------------------ | ----------------------------------------------------------------- |
114
- | Yes | Same | No-op |
115
- | Yes | Different | Create new Contact; set member’s `wixContactId` to new contact ID |
116
- | No | Same | Delete Contact; set member’s `wixContactId = wixMemberId` |
117
- | No | Different | Update existing Contact’s email in CRM (no member change) |
118
-
119
- ### Where it’s used
120
-
121
- `updateContactEmail` is called from `updateMemberContactInfo` when the **contactFormEmail** field changes (e.g. profile or form update). So:
122
-
123
- - **Login flow** → `createContactAndMemberIfNew` (ensure member + contact exist and IDs stored).
124
- - **Profile/form email change** → `updateMemberContactInfo` → `updateContactEmail` (keep contact and member in sync).
125
-
126
- ---
127
-
128
- ## File roles
129
-
130
- | File | Role |
131
- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
132
- | `contacts-methods.js` | Contact CRUD only: create/update/delete contact in Wix CRM. |
133
- | `member-contact-orchestration.js` | Orchestration: when to create/update/delete contact and when to update member’s `wixContactId`. Uses injected `updateMember`. |
134
- | `members-data-methods.js` | Member CRUD, `createContactAndMemberIfNew`; requires `updateMemberContactInfo` and calls `updateMember` once with its return value. |
135
-
136
- Dependency direction: **members-data** → **member-contact-orchestration** → **contacts** (no cycle).
@@ -1,124 +0,0 @@
1
- jest.mock('../elevated-modules', () => ({
2
- wixData: { query: jest.fn() },
3
- }));
4
- jest.mock('psdev-task-manager', () => ({ taskManager: jest.fn() }));
5
-
6
- const { taskManager } = require('psdev-task-manager');
7
-
8
- const { buildDailyPullTasks } = require('../daily-pull/schedule-methods');
9
- const { wixData } = require('../elevated-modules');
10
- const { TASKS_NAMES } = require('../tasks/consts');
11
- const { dailyPullExecutionCheck } = require('../tasks/daily-pull-check-methods');
12
-
13
- const makeQueryResult = items => ({ items, hasNext: () => false });
14
-
15
- const mockTasksQuery = items => {
16
- const query = {
17
- hasSome: jest.fn().mockReturnThis(),
18
- ge: jest.fn().mockReturnThis(),
19
- find: jest.fn().mockResolvedValue(makeQueryResult(items)),
20
- };
21
- wixData.query.mockReturnValue(query);
22
- return query;
23
- };
24
-
25
- describe('buildDailyPullTasks', () => {
26
- test('schedules one ScheduleMembersDataPerAction task per action, excluding none by default', () => {
27
- const tasks = buildDailyPullTasks();
28
-
29
- expect(tasks.map(task => task.data.action).sort()).toEqual(['drop', 'new', 'update']);
30
- tasks.forEach(task => {
31
- expect(task.name).toBe(TASKS_NAMES.ScheduleMembersDataPerAction);
32
- expect(task.type).toBe('scheduled');
33
- expect(task.data).toEqual({ action: task.data.action });
34
- });
35
- });
36
-
37
- test('includes the none action and flag when includeNone is set', () => {
38
- const tasks = buildDailyPullTasks({ includeNone: true });
39
-
40
- expect(tasks.map(task => task.data.action).sort()).toEqual(['drop', 'new', 'none', 'update']);
41
- tasks.forEach(task => expect(task.data.includeNone).toBe(true));
42
- });
43
-
44
- test('propagates isTestEnvironment and backupDate to every task', () => {
45
- const tasks = buildDailyPullTasks({ isTestEnvironment: true, backupDate: '2026-06-01' });
46
-
47
- tasks.forEach(task => {
48
- expect(task.data.isTestEnvironment).toBe(true);
49
- expect(task.data.backupDate).toBe('2026-06-01');
50
- });
51
- });
52
- });
53
-
54
- describe('dailyPullExecutionCheck', () => {
55
- let scheduleInBulk;
56
-
57
- beforeEach(() => {
58
- wixData.query.mockReset();
59
- scheduleInBulk = jest.fn().mockResolvedValue(undefined);
60
- taskManager.mockReturnValue({ scheduleInBulk });
61
- });
62
-
63
- test('counts the per-action tasks the cron creates as evidence the pull ran', async () => {
64
- const query = mockTasksQuery([{ name: TASKS_NAMES.ScheduleMembersDataPerAction }]);
65
-
66
- const result = await dailyPullExecutionCheck({});
67
-
68
- expect(query.hasSome).toHaveBeenCalledWith('name', [
69
- TASKS_NAMES.ScheduleMembersDataPerAction,
70
- TASKS_NAMES.ScheduleDailyMembersDataSync,
71
- ]);
72
- expect(result.success).toBe(true);
73
- expect(result.fallbackScheduled).toBeUndefined();
74
- expect(scheduleInBulk).not.toHaveBeenCalled();
75
- });
76
-
77
- test('schedules the per-action fallback with environment flags when no pull is found', async () => {
78
- mockTasksQuery([]);
79
-
80
- const result = await dailyPullExecutionCheck({ isTestEnvironment: true, includeNone: true });
81
-
82
- expect(result.success).toBe(false);
83
- expect(result.fallbackScheduled).toBe(true);
84
- expect(scheduleInBulk).toHaveBeenCalledTimes(1);
85
- const scheduledTasks = scheduleInBulk.mock.calls[0][0];
86
- expect(scheduledTasks.map(task => task.data.action).sort()).toEqual([
87
- 'drop',
88
- 'new',
89
- 'none',
90
- 'update',
91
- ]);
92
- scheduledTasks.forEach(task => {
93
- expect(task.name).toBe(TASKS_NAMES.ScheduleMembersDataPerAction);
94
- expect(task.data.isTestEnvironment).toBe(true);
95
- expect(task.data.includeNone).toBe(true);
96
- });
97
- });
98
-
99
- test('fallback defaults to production flags when no task data is provided', async () => {
100
- mockTasksQuery([]);
101
-
102
- await dailyPullExecutionCheck(undefined);
103
-
104
- const scheduledTasks = scheduleInBulk.mock.calls[0][0];
105
- expect(scheduledTasks.map(task => task.data.action).sort()).toEqual(['drop', 'new', 'update']);
106
- scheduledTasks.forEach(task => {
107
- expect(task.data.isTestEnvironment).toBeUndefined();
108
- expect(task.data.includeNone).toBeUndefined();
109
- });
110
- });
111
-
112
- test('uses the provided lookback window', async () => {
113
- const query = mockTasksQuery([{ name: TASKS_NAMES.ScheduleDailyMembersDataSync }]);
114
- const before = Date.now();
115
-
116
- const result = await dailyPullExecutionCheck({ hoursBack: 12 });
117
-
118
- const sinceDate = query.ge.mock.calls[0][1];
119
- expect(query.ge).toHaveBeenCalledWith('_createdDate', expect.any(Date));
120
- const expectedSince = before - 12 * 60 * 60 * 1000;
121
- expect(Math.abs(sinceDate.getTime() - expectedSince)).toBeLessThan(5000);
122
- expect(result.success).toBe(true);
123
- });
124
- });
@@ -1,129 +0,0 @@
1
- const { ensureUniqueUrlsInBatch } = require('../daily-pull/bulk-process-methods');
2
- const membersDataMethods = require('../members-data-methods');
3
- jest.mock('../members-data-methods');
4
-
5
- describe('ensureUniqueUrlsInBatch', () => {
6
- beforeEach(() => {
7
- membersDataMethods.getMemberBySlug.mockReset();
8
- });
9
-
10
- describe('case-insensitivity (regression: avoid creating John-12 AND john-12)', () => {
11
- test('merges John-11 and john-11 into one group and assigns unique sequential URLs', async () => {
12
- membersDataMethods.getMemberBySlug.mockResolvedValue({ url: 'John-11', memberId: 1 });
13
-
14
- const members = [
15
- { memberId: 101, url: 'John-11', fullName: 'John Doe' },
16
- { memberId: 102, url: 'john-11', fullName: 'John Doe' },
17
- ];
18
-
19
- const result = await ensureUniqueUrlsInBatch(members);
20
-
21
- const urls = result.map(m => m.url);
22
-
23
- expect(urls).toHaveLength(2);
24
- expect(new Set(urls).size).toBe(2);
25
- expect(urls.some(u => u.endsWith('-12'))).toBe(true);
26
- expect(urls.some(u => u.endsWith('-13'))).toBe(true);
27
- });
28
-
29
- test('does NOT create both John-12 and john-12 (case duplicate)', async () => {
30
- membersDataMethods.getMemberBySlug.mockResolvedValue({ url: 'John-11', memberId: 1 });
31
-
32
- const members = [
33
- { memberId: 101, url: 'John-11' },
34
- { memberId: 102, url: 'john-11' },
35
- ];
36
-
37
- const result = await ensureUniqueUrlsInBatch(members);
38
- const urls = result.map(m => m.url);
39
-
40
- const hasJohn12 = urls.some(u => u === 'John-12');
41
- const hasjohn12 = urls.some(u => u === 'john-12');
42
- expect(hasJohn12 && hasjohn12).toBe(false);
43
- });
44
-
45
- test('handles JOHN, John, john variants - all merge into single group', async () => {
46
- membersDataMethods.getMemberBySlug.mockResolvedValue(null);
47
-
48
- const members = [
49
- { memberId: 101, url: 'JOHN-5' },
50
- { memberId: 102, url: 'John-5' },
51
- { memberId: 103, url: 'john-5' },
52
- ];
53
-
54
- const result = await ensureUniqueUrlsInBatch(members);
55
- const urls = result.map(m => m.url);
56
-
57
- expect(urls).toHaveLength(3);
58
- expect(new Set(urls).size).toBe(3);
59
- const bases = urls.map(u => u.replace(/-\d+$/, '').toLowerCase());
60
- expect(new Set(bases).size).toBe(1);
61
- });
62
- });
63
-
64
- describe('multiple members with same base URL', () => {
65
- test('assigns sequential counters starting after batch max and DB max', async () => {
66
- membersDataMethods.getMemberBySlug.mockResolvedValue({
67
- url: 'firstNameLastName-11',
68
- memberId: 1,
69
- });
70
-
71
- const members = [
72
- { memberId: 201, url: 'firstNameLastName-11' },
73
- { memberId: 202, url: 'firstNameLastName-11' },
74
- { memberId: 203, url: 'firstNameLastName-11' },
75
- ];
76
-
77
- const result = await ensureUniqueUrlsInBatch(members);
78
- const urls = result.map(m => m.url).sort();
79
-
80
- expect(urls).toEqual([
81
- 'firstNameLastName-12',
82
- 'firstNameLastName-13',
83
- 'firstNameLastName-14',
84
- ]);
85
- });
86
-
87
- test('uses batch max when DB has no matches', async () => {
88
- membersDataMethods.getMemberBySlug.mockResolvedValue(null);
89
-
90
- const members = [
91
- { memberId: 301, url: 'testUser-5' },
92
- { memberId: 302, url: 'testUser-5' },
93
- ];
94
-
95
- const result = await ensureUniqueUrlsInBatch(members);
96
- const urls = result.map(m => m.url).sort();
97
-
98
- expect(urls).toEqual(['testUser-6', 'testUser-7']);
99
- });
100
- });
101
-
102
- describe('edge cases', () => {
103
- test('returns empty array unchanged', async () => {
104
- const result = await ensureUniqueUrlsInBatch([]);
105
- expect(result).toEqual([]);
106
- });
107
-
108
- test('returns non-array unchanged', async () => {
109
- const input = { not: 'array' };
110
- const result = await ensureUniqueUrlsInBatch(input);
111
- expect(result).toBe(input);
112
- });
113
-
114
- test('skips members without url', async () => {
115
- membersDataMethods.getMemberBySlug.mockResolvedValue(null);
116
-
117
- const members = [
118
- { memberId: 401, url: 'validUrl' },
119
- { memberId: 402, url: null },
120
- { memberId: 403 },
121
- ];
122
-
123
- const result = await ensureUniqueUrlsInBatch(members);
124
- expect(result).toHaveLength(3);
125
- expect(result[0].url).toBe('validUrl');
126
- expect(result[1].url).toBeNull();
127
- });
128
- });
129
- });