abmp-npm 2.0.77 → 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/.claude/skills/wix-data-query/SKILL.md +0 -167
  3. package/.claude/skills/wix-data-query/references/backup-archive.md +0 -136
  4. package/.claude/skills/wix-data-query/references/members-data-latest.md +0 -125
  5. package/.claude/skills/wix-data-query/references/recipes.md +0 -264
  6. package/.claude/worktrees/fix-save-button-disable/.husky/pre-commit +0 -19
  7. package/.claude/worktrees/fix-save-button-disable/.prettierignore +0 -7
  8. package/.claude/worktrees/fix-save-button-disable/.prettierrc.json +0 -16
  9. package/.claude/worktrees/fix-save-button-disable/README.md +0 -1
  10. package/.claude/worktrees/fix-save-button-disable/backend/README-member-contact-flows.md +0 -136
  11. package/.claude/worktrees/fix-save-button-disable/backend/__tests__/daily-pull-execution-check.test.js +0 -124
  12. package/.claude/worktrees/fix-save-button-disable/backend/__tests__/ensure-unique-urls-in-batch.test.js +0 -129
  13. package/.claude/worktrees/fix-save-button-disable/backend/__tests__/login-email-sync.test.js +0 -49
  14. package/.claude/worktrees/fix-save-button-disable/backend/__tests__/transient-retry-and-bulk-lookup.test.js +0 -129
  15. package/.claude/worktrees/fix-save-button-disable/backend/__tests__/url-uniqueness.test.js +0 -194
  16. package/.claude/worktrees/fix-save-button-disable/backend/automations-methods.js +0 -13
  17. package/.claude/worktrees/fix-save-button-disable/backend/cms-data-methods.js +0 -251
  18. package/.claude/worktrees/fix-save-button-disable/backend/consts.js +0 -58
  19. package/.claude/worktrees/fix-save-button-disable/backend/contacts-methods.js +0 -67
  20. package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/bulk-process-methods.js +0 -222
  21. package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/consts.js +0 -39
  22. package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/index.js +0 -5
  23. package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/process-member-methods.js +0 -213
  24. package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/schedule-methods.js +0 -41
  25. package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/sync-to-cms-methods.js +0 -161
  26. package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/utils.js +0 -151
  27. package/.claude/worktrees/fix-save-button-disable/backend/data-hooks.js +0 -29
  28. package/.claude/worktrees/fix-save-button-disable/backend/dev-only-methods.js +0 -58
  29. package/.claude/worktrees/fix-save-button-disable/backend/elevated-modules.js +0 -18
  30. package/.claude/worktrees/fix-save-button-disable/backend/forms-methods.js +0 -52
  31. package/.claude/worktrees/fix-save-button-disable/backend/http-functions/httpFunctions.js +0 -86
  32. package/.claude/worktrees/fix-save-button-disable/backend/http-functions/index.js +0 -3
  33. package/.claude/worktrees/fix-save-button-disable/backend/http-functions/interests.js +0 -35
  34. package/.claude/worktrees/fix-save-button-disable/backend/index.js +0 -19
  35. package/.claude/worktrees/fix-save-button-disable/backend/jobs.js +0 -158
  36. package/.claude/worktrees/fix-save-button-disable/backend/login/generate-member-session-token.js +0 -27
  37. package/.claude/worktrees/fix-save-button-disable/backend/login/index.js +0 -8
  38. package/.claude/worktrees/fix-save-button-disable/backend/login/qa-login-methods.js +0 -54
  39. package/.claude/worktrees/fix-save-button-disable/backend/login/sso-methods.js +0 -159
  40. package/.claude/worktrees/fix-save-button-disable/backend/member-contact-orchestration.js +0 -111
  41. package/.claude/worktrees/fix-save-button-disable/backend/members-area-methods.js +0 -88
  42. package/.claude/worktrees/fix-save-button-disable/backend/members-data-methods.js +0 -789
  43. package/.claude/worktrees/fix-save-button-disable/backend/pac-api-methods.js +0 -54
  44. package/.claude/worktrees/fix-save-button-disable/backend/routers/index.js +0 -3
  45. package/.claude/worktrees/fix-save-button-disable/backend/routers/methods.js +0 -191
  46. package/.claude/worktrees/fix-save-button-disable/backend/routers/utils.js +0 -128
  47. package/.claude/worktrees/fix-save-button-disable/backend/search-filters-methods.js +0 -124
  48. package/.claude/worktrees/fix-save-button-disable/backend/tasks/address-primary-methods.js +0 -256
  49. package/.claude/worktrees/fix-save-button-disable/backend/tasks/address-visibility-methods.js +0 -160
  50. package/.claude/worktrees/fix-save-button-disable/backend/tasks/consts.js +0 -34
  51. package/.claude/worktrees/fix-save-button-disable/backend/tasks/daily-pull-check-methods.js +0 -78
  52. package/.claude/worktrees/fix-save-button-disable/backend/tasks/email-normalize-methods.js +0 -134
  53. package/.claude/worktrees/fix-save-button-disable/backend/tasks/index.js +0 -10
  54. package/.claude/worktrees/fix-save-button-disable/backend/tasks/migration-methods.js +0 -46
  55. package/.claude/worktrees/fix-save-button-disable/backend/tasks/tasks-configs.js +0 -251
  56. package/.claude/worktrees/fix-save-button-disable/backend/tasks/tasks-helpers-methods.js +0 -433
  57. package/.claude/worktrees/fix-save-button-disable/backend/tasks/tasks-process-methods.js +0 -606
  58. package/.claude/worktrees/fix-save-button-disable/backend/tasks/url-migration-methods.js +0 -378
  59. package/.claude/worktrees/fix-save-button-disable/backend/tasks/url-space-fix-methods.js +0 -174
  60. package/.claude/worktrees/fix-save-button-disable/backend/utils.js +0 -277
  61. package/.claude/worktrees/fix-save-button-disable/dev-only-scripts/extract-duplicate-url-groups.js +0 -201
  62. package/.claude/worktrees/fix-save-button-disable/dev-only-scripts/find-duplicate-ids.js +0 -159
  63. package/.claude/worktrees/fix-save-button-disable/dev-only-scripts/find-duplicate-urls.js +0 -201
  64. package/.claude/worktrees/fix-save-button-disable/eslint.config.js +0 -120
  65. package/.claude/worktrees/fix-save-button-disable/index.js +0 -5
  66. package/.claude/worktrees/fix-save-button-disable/package-lock.json +0 -12160
  67. package/.claude/worktrees/fix-save-button-disable/package.json +0 -60
  68. package/.claude/worktrees/fix-save-button-disable/pages/ContactUs.js +0 -129
  69. package/.claude/worktrees/fix-save-button-disable/pages/Home.js +0 -779
  70. package/.claude/worktrees/fix-save-button-disable/pages/LearnMore.js +0 -27
  71. package/.claude/worktrees/fix-save-button-disable/pages/LoadingPage.js +0 -29
  72. package/.claude/worktrees/fix-save-button-disable/pages/Profile.js +0 -354
  73. package/.claude/worktrees/fix-save-button-disable/pages/QAPage.js +0 -39
  74. package/.claude/worktrees/fix-save-button-disable/pages/SaveAlerts.js +0 -13
  75. package/.claude/worktrees/fix-save-button-disable/pages/SelectBannerImages.js +0 -46
  76. package/.claude/worktrees/fix-save-button-disable/pages/deleteConfirm.js +0 -19
  77. package/.claude/worktrees/fix-save-button-disable/pages/index.js +0 -12
  78. package/.claude/worktrees/fix-save-button-disable/pages/personalDetails.js +0 -2486
  79. package/.claude/worktrees/fix-save-button-disable/public/Utils/homePage.js +0 -798
  80. package/.claude/worktrees/fix-save-button-disable/public/Utils/personalDetailsUtils.js +0 -25
  81. package/.claude/worktrees/fix-save-button-disable/public/Utils/sharedUtils.js +0 -235
  82. package/.claude/worktrees/fix-save-button-disable/public/consts.js +0 -113
  83. package/.claude/worktrees/fix-save-button-disable/public/index.js +0 -5
  84. package/.claude/worktrees/fix-save-button-disable/public/messages.js +0 -16
  85. package/.claude/worktrees/fix-save-button-disable/public/sso-auth-methods.js +0 -43
  86. package/.husky/pre-commit +0 -19
  87. package/.prettierignore +0 -7
  88. package/.prettierrc.json +0 -16
  89. package/backend/__tests__/daily-pull-execution-check.test.js +0 -124
  90. package/backend/__tests__/ensure-unique-urls-in-batch.test.js +0 -129
  91. package/backend/__tests__/login-email-sync.test.js +0 -49
  92. package/backend/__tests__/map-link.test.js +0 -115
  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/backend/__tests__/url-validation.test.js +0 -69
  96. package/dev-only-scripts/extract-duplicate-url-groups.js +0 -201
  97. package/dev-only-scripts/find-duplicate-ids.js +0 -159
  98. package/dev-only-scripts/find-duplicate-urls.js +0 -201
  99. package/eslint.config.js +0 -120
@@ -1,264 +0,0 @@
1
- # Query cookbook
2
-
3
- All bodies below are the JSON body for
4
- `POST https://www.wixapis.com/wix-data/v2/items/query`
5
- ([docs](https://dev.wix.com/docs/api-reference/business-solutions/cms/data-items/query-data-items)).
6
-
7
- Send them via `CallWixSiteAPI(siteId, url, method: "POST", body)` or via curl with
8
- `Authorization: $WIX_API_KEY` + `wix-site-id: <siteId>`.
9
-
10
- ---
11
-
12
- ## Look up one member
13
-
14
- By profile slug (from the URL in the ticket):
15
-
16
- ```json
17
- {
18
- "dataCollectionId": "MembersDataLatest",
19
- "query": { "filter": { "url": "karriknowles" }, "paging": { "limit": 1 } }
20
- }
21
- ```
22
-
23
- By PAC member ID — **number, not string**:
24
-
25
- ```json
26
- {
27
- "dataCollectionId": "MembersDataLatest",
28
- "query": { "filter": { "memberId": 731898 }, "paging": { "limit": 1 } }
29
- }
30
- ```
31
-
32
- By email:
33
-
34
- ```json
35
- {
36
- "dataCollectionId": "MembersDataLatest",
37
- "query": { "filter": { "email": "someone@example.com" }, "paging": { "limit": 1 } }
38
- }
39
- ```
40
-
41
- By name — `fullName` only, never the encrypted `firstName`/`lastName`:
42
-
43
- ```json
44
- {
45
- "dataCollectionId": "MembersDataLatest",
46
- "query": { "filter": { "fullName": { "$contains": "Knowles" } }, "paging": { "limit": 20 } }
47
- }
48
- ```
49
-
50
- ## Return only the fields you care about
51
-
52
- Large records (galleries, testimonials, rich text) drown the useful bits. Project:
53
-
54
- ```json
55
- {
56
- "dataCollectionId": "MembersDataLatest",
57
- "query": {
58
- "filter": { "memberId": 949741 },
59
- "fields": [
60
- "memberId",
61
- "url",
62
- "fullName",
63
- "bookingUrl",
64
- "showBookingUrl",
65
- "apiBookingUrl",
66
- "APIBookingUrl",
67
- "isVisible",
68
- "action"
69
- ],
70
- "paging": { "limit": 1 }
71
- }
72
- }
73
- ```
74
-
75
- ## Count / scope an issue
76
-
77
- How many members are affected, without pulling them all. Use the **dedicated count
78
- endpoint** — `POST https://www.wixapis.com/wix-data/v2/items/count`
79
- ([docs](https://dev.wix.com/docs/api-reference/business-solutions/cms/data-items/count-data-items)).
80
- Note the different body shape: `filter` sits at the top level, there is no `query` wrapper.
81
-
82
- ```json
83
- {
84
- "dataCollectionId": "MembersDataLatest",
85
- "filter": { "showBookingUrl": false, "bookingUrl": { "$ne": "" } }
86
- }
87
- ```
88
-
89
- Returns `{"totalCount": 82449}`.
90
-
91
- > ⚠️ **Do not rely on `returnTotalCount` in the query endpoint.** Verified 2026-08-04 against
92
- > `MembersDataLatest`: passing `"returnTotalCount": true` — with or without an explicit
93
- > `paging.offset` — returns `pagingMetadata` containing `count`, `offset`, `tooManyToCount`,
94
- > `cursors` and `hasNext`, but **no `total` field**. Use `/items/count` for totals.
95
-
96
- ## Members who should have been dropped but are still visible
97
-
98
- The shape behind the "multies not renewing / expired members still listed" class of ticket:
99
-
100
- ```json
101
- {
102
- "dataCollectionId": "MembersDataLatest",
103
- "query": {
104
- "filter": { "action": "drop", "isVisible": true },
105
- "fields": ["memberId", "url", "fullName", "action", "isVisible", "memberships"],
106
- "paging": { "limit": 100 }
107
- }
108
- }
109
- ```
110
-
111
- Inverse — visible members whose membership already expired:
112
-
113
- ```json
114
- {
115
- "dataCollectionId": "MembersDataLatest",
116
- "query": {
117
- "filter": { "isVisible": true, "memberships.expiration": { "$lt": "2026-08-04T00:00:00" } },
118
- "fields": ["memberId", "url", "memberships", "action"],
119
- "paging": { "limit": 100 }
120
- }
121
- }
122
- ```
123
-
124
- > ⚠️ **`memberships.expiration` is a plain ISO string, not a date.** Compare it as a string.
125
- > Verified 2026-08-04 on ABMP prod: `{"$lt": {"$date": "2026-08-04T00:00:00.000Z"}}` counts
126
- > **0**, while `{"$lt": "2026-08-04T00:00:00"}` counts **1008**. The `$date` wrapper is only
127
- > correct for true DATETIME fields such as `_createdDate` / `_updatedDate`. Note the stored
128
- > strings have no `Z` suffix and no milliseconds — match that format.
129
-
130
- ## Recently synced records
131
-
132
- Useful for confirming whether a nightly run touched a member at all:
133
-
134
- ```json
135
- {
136
- "dataCollectionId": "MembersDataLatest",
137
- "query": {
138
- "filter": { "_updatedDate": { "$gte": { "$date": "2026-08-01T00:00:00.000Z" } } },
139
- "sort": [{ "fieldName": "_updatedDate", "order": "DESC" }],
140
- "fields": ["memberId", "url", "_updatedDate", "action", "pageNumber"],
141
- "paging": { "limit": 50 }
142
- }
143
- }
144
- ```
145
-
146
- ## Combining conditions
147
-
148
- ```json
149
- {
150
- "dataCollectionId": "MembersDataLatest",
151
- "query": {
152
- "filter": {
153
- "$and": [
154
- { "isVisible": true },
155
- { "$or": [{ "showBookingUrl": true }, { "showWebsite": true }] }
156
- ]
157
- },
158
- "paging": { "limit": 25 }
159
- }
160
- }
161
- ```
162
-
163
- ## Paging past 100
164
-
165
- Offset paging (each request may carry its own filter/sort):
166
-
167
- ```json
168
- {
169
- "dataCollectionId": "MembersDataLatest",
170
- "query": { "filter": {}, "paging": { "limit": 100, "offset": 100 } }
171
- }
172
- ```
173
-
174
- Cursor paging for a long scan — set `filter`/`sort` on the **first** request only, then pass
175
- back `pagingMetadata.cursors.next` alone:
176
-
177
- ```json
178
- {
179
- "dataCollectionId": "MembersDataLatest",
180
- "query": { "cursorPaging": { "limit": 100, "cursor": "<cursors.next>" } }
181
- }
182
- ```
183
-
184
- ---
185
-
186
- ## Simulate a site query to test whether a filter really excludes something
187
-
188
- The highest-value technique in this skill. When a ticket claims "X shouldn't be showing but is",
189
- rebuild the site's own filters against `/items/count` and check whether the record survives. This
190
- tests the filter's real semantics instead of reasoning about the code.
191
-
192
- The directory query (`buildMembersSearchQuery` in `backend/cms-data-methods.js`) is:
193
-
194
- ```json
195
- {
196
- "dataCollectionId": "MembersDataLatest",
197
- "filter": {
198
- "optOut": { "$ne": true },
199
- "action": { "$ne": "drop" },
200
- "memberships.membertype": { "$ne": "PAC STAFF" },
201
- "isVisible": true
202
- }
203
- }
204
- ```
205
-
206
- Add the member you are investigating and read the count. **Always run the control too** — the
207
- same filter _without_ the clause under test — or a `0` tells you nothing:
208
-
209
- ```
210
- with the PAC STAFF clause -> 0 (excluded)
211
- without the PAC STAFF clause -> 1 (would otherwise appear)
212
- ```
213
-
214
- To ask "does _anything_ slip through?", combine both conditions on the same field with `$and`
215
- (a filter object cannot hold two operators under one key):
216
-
217
- ```json
218
- {
219
- "dataCollectionId": "MembersDataLatest",
220
- "filter": {
221
- "$and": [
222
- { "memberships.membertype": { "$contains": "PAC" } },
223
- { "memberships.membertype": { "$ne": "PAC STAFF" } },
224
- { "optOut": { "$ne": true } },
225
- { "isVisible": true }
226
- ]
227
- }
228
- }
229
- ```
230
-
231
- Nested paths into array fields (`memberships.membertype`, `addresses.postalcode`) work
232
- throughout.
233
-
234
- > Caveat proven on 2026-08-04: this validates **filter semantics**, not that the deployed site
235
- > applies them. A filter can be provably correct while the live page still misbehaves. To settle
236
- > that, get the site's actual API response — the browser devtools network tab is faster than any
237
- > amount of reasoning, and `?nearby=true` style search UIs often need geolocation before they
238
- > return anything at all.
239
-
240
- ## Other endpoints
241
-
242
- **Collection schema** — field names, types, and which operators each field allows:
243
-
244
- ```
245
- GET https://www.wixapis.com/wix-data/v2/collections/MembersDataLatest
246
- ```
247
-
248
- **List all collections on the site:**
249
-
250
- ```
251
- GET https://www.wixapis.com/wix-data/v2/collections
252
- ```
253
-
254
- **Full-text search** across fields (`POST .../wix-data/v2/items/search`) — note it takes
255
- `data_collection_id` (snake_case) and a `search` object rather than `query`. Only works on
256
- CMS-native collections. See
257
- [Search Data Items](https://dev.wix.com/docs/api-reference/business-solutions/cms/data-items/search-data-items).
258
-
259
- **Aggregate** (`AGGREGATE` is supported on `MembersDataLatest`) for group-by counts, e.g.
260
- distribution of `action` values across the directory.
261
-
262
- Other collections worth knowing, from `COLLECTIONS` in `public/consts.js`:
263
- `SiteConfigs`, `CompiledStateCityMap`, `State`, `City`, `interests`,
264
- `contactUsSubmissions`, `updatedLoginEmails`, `QA_Users`, `ButtonClicks`.
@@ -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
- });