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.
- package/package.json +8 -1
- package/.claude/skills/wix-data-query/SKILL.md +0 -167
- package/.claude/skills/wix-data-query/references/backup-archive.md +0 -136
- package/.claude/skills/wix-data-query/references/members-data-latest.md +0 -125
- package/.claude/skills/wix-data-query/references/recipes.md +0 -264
- package/.claude/worktrees/fix-save-button-disable/.husky/pre-commit +0 -19
- package/.claude/worktrees/fix-save-button-disable/.prettierignore +0 -7
- package/.claude/worktrees/fix-save-button-disable/.prettierrc.json +0 -16
- package/.claude/worktrees/fix-save-button-disable/README.md +0 -1
- package/.claude/worktrees/fix-save-button-disable/backend/README-member-contact-flows.md +0 -136
- package/.claude/worktrees/fix-save-button-disable/backend/__tests__/daily-pull-execution-check.test.js +0 -124
- package/.claude/worktrees/fix-save-button-disable/backend/__tests__/ensure-unique-urls-in-batch.test.js +0 -129
- package/.claude/worktrees/fix-save-button-disable/backend/__tests__/login-email-sync.test.js +0 -49
- package/.claude/worktrees/fix-save-button-disable/backend/__tests__/transient-retry-and-bulk-lookup.test.js +0 -129
- package/.claude/worktrees/fix-save-button-disable/backend/__tests__/url-uniqueness.test.js +0 -194
- package/.claude/worktrees/fix-save-button-disable/backend/automations-methods.js +0 -13
- package/.claude/worktrees/fix-save-button-disable/backend/cms-data-methods.js +0 -251
- package/.claude/worktrees/fix-save-button-disable/backend/consts.js +0 -58
- package/.claude/worktrees/fix-save-button-disable/backend/contacts-methods.js +0 -67
- package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/bulk-process-methods.js +0 -222
- package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/consts.js +0 -39
- package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/index.js +0 -5
- package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/process-member-methods.js +0 -213
- package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/schedule-methods.js +0 -41
- package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/sync-to-cms-methods.js +0 -161
- package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/utils.js +0 -151
- package/.claude/worktrees/fix-save-button-disable/backend/data-hooks.js +0 -29
- package/.claude/worktrees/fix-save-button-disable/backend/dev-only-methods.js +0 -58
- package/.claude/worktrees/fix-save-button-disable/backend/elevated-modules.js +0 -18
- package/.claude/worktrees/fix-save-button-disable/backend/forms-methods.js +0 -52
- package/.claude/worktrees/fix-save-button-disable/backend/http-functions/httpFunctions.js +0 -86
- package/.claude/worktrees/fix-save-button-disable/backend/http-functions/index.js +0 -3
- package/.claude/worktrees/fix-save-button-disable/backend/http-functions/interests.js +0 -35
- package/.claude/worktrees/fix-save-button-disable/backend/index.js +0 -19
- package/.claude/worktrees/fix-save-button-disable/backend/jobs.js +0 -158
- package/.claude/worktrees/fix-save-button-disable/backend/login/generate-member-session-token.js +0 -27
- package/.claude/worktrees/fix-save-button-disable/backend/login/index.js +0 -8
- package/.claude/worktrees/fix-save-button-disable/backend/login/qa-login-methods.js +0 -54
- package/.claude/worktrees/fix-save-button-disable/backend/login/sso-methods.js +0 -159
- package/.claude/worktrees/fix-save-button-disable/backend/member-contact-orchestration.js +0 -111
- package/.claude/worktrees/fix-save-button-disable/backend/members-area-methods.js +0 -88
- package/.claude/worktrees/fix-save-button-disable/backend/members-data-methods.js +0 -789
- package/.claude/worktrees/fix-save-button-disable/backend/pac-api-methods.js +0 -54
- package/.claude/worktrees/fix-save-button-disable/backend/routers/index.js +0 -3
- package/.claude/worktrees/fix-save-button-disable/backend/routers/methods.js +0 -191
- package/.claude/worktrees/fix-save-button-disable/backend/routers/utils.js +0 -128
- package/.claude/worktrees/fix-save-button-disable/backend/search-filters-methods.js +0 -124
- package/.claude/worktrees/fix-save-button-disable/backend/tasks/address-primary-methods.js +0 -256
- package/.claude/worktrees/fix-save-button-disable/backend/tasks/address-visibility-methods.js +0 -160
- package/.claude/worktrees/fix-save-button-disable/backend/tasks/consts.js +0 -34
- package/.claude/worktrees/fix-save-button-disable/backend/tasks/daily-pull-check-methods.js +0 -78
- package/.claude/worktrees/fix-save-button-disable/backend/tasks/email-normalize-methods.js +0 -134
- package/.claude/worktrees/fix-save-button-disable/backend/tasks/index.js +0 -10
- package/.claude/worktrees/fix-save-button-disable/backend/tasks/migration-methods.js +0 -46
- package/.claude/worktrees/fix-save-button-disable/backend/tasks/tasks-configs.js +0 -251
- package/.claude/worktrees/fix-save-button-disable/backend/tasks/tasks-helpers-methods.js +0 -433
- package/.claude/worktrees/fix-save-button-disable/backend/tasks/tasks-process-methods.js +0 -606
- package/.claude/worktrees/fix-save-button-disable/backend/tasks/url-migration-methods.js +0 -378
- package/.claude/worktrees/fix-save-button-disable/backend/tasks/url-space-fix-methods.js +0 -174
- package/.claude/worktrees/fix-save-button-disable/backend/utils.js +0 -277
- package/.claude/worktrees/fix-save-button-disable/dev-only-scripts/extract-duplicate-url-groups.js +0 -201
- package/.claude/worktrees/fix-save-button-disable/dev-only-scripts/find-duplicate-ids.js +0 -159
- package/.claude/worktrees/fix-save-button-disable/dev-only-scripts/find-duplicate-urls.js +0 -201
- package/.claude/worktrees/fix-save-button-disable/eslint.config.js +0 -120
- package/.claude/worktrees/fix-save-button-disable/index.js +0 -5
- package/.claude/worktrees/fix-save-button-disable/package-lock.json +0 -12160
- package/.claude/worktrees/fix-save-button-disable/package.json +0 -60
- package/.claude/worktrees/fix-save-button-disable/pages/ContactUs.js +0 -129
- package/.claude/worktrees/fix-save-button-disable/pages/Home.js +0 -779
- package/.claude/worktrees/fix-save-button-disable/pages/LearnMore.js +0 -27
- package/.claude/worktrees/fix-save-button-disable/pages/LoadingPage.js +0 -29
- package/.claude/worktrees/fix-save-button-disable/pages/Profile.js +0 -354
- package/.claude/worktrees/fix-save-button-disable/pages/QAPage.js +0 -39
- package/.claude/worktrees/fix-save-button-disable/pages/SaveAlerts.js +0 -13
- package/.claude/worktrees/fix-save-button-disable/pages/SelectBannerImages.js +0 -46
- package/.claude/worktrees/fix-save-button-disable/pages/deleteConfirm.js +0 -19
- package/.claude/worktrees/fix-save-button-disable/pages/index.js +0 -12
- package/.claude/worktrees/fix-save-button-disable/pages/personalDetails.js +0 -2486
- package/.claude/worktrees/fix-save-button-disable/public/Utils/homePage.js +0 -798
- package/.claude/worktrees/fix-save-button-disable/public/Utils/personalDetailsUtils.js +0 -25
- package/.claude/worktrees/fix-save-button-disable/public/Utils/sharedUtils.js +0 -235
- package/.claude/worktrees/fix-save-button-disable/public/consts.js +0 -113
- package/.claude/worktrees/fix-save-button-disable/public/index.js +0 -5
- package/.claude/worktrees/fix-save-button-disable/public/messages.js +0 -16
- package/.claude/worktrees/fix-save-button-disable/public/sso-auth-methods.js +0 -43
- package/.husky/pre-commit +0 -19
- package/.prettierignore +0 -7
- package/.prettierrc.json +0 -16
- package/backend/__tests__/daily-pull-execution-check.test.js +0 -124
- package/backend/__tests__/ensure-unique-urls-in-batch.test.js +0 -129
- package/backend/__tests__/login-email-sync.test.js +0 -49
- package/backend/__tests__/map-link.test.js +0 -115
- package/backend/__tests__/transient-retry-and-bulk-lookup.test.js +0 -129
- package/backend/__tests__/url-uniqueness.test.js +0 -194
- package/backend/__tests__/url-validation.test.js +0 -69
- package/dev-only-scripts/extract-duplicate-url-groups.js +0 -201
- package/dev-only-scripts/find-duplicate-ids.js +0 -159
- package/dev-only-scripts/find-duplicate-urls.js +0 -201
- 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,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
|
-
});
|