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,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
|
-
});
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
const { LOGIN_EMAIL_SYNC_STATUS } = require('../consts');
|
|
2
|
-
const { summarizeLoginEmailOutcomes } = require('../daily-pull/utils');
|
|
3
|
-
|
|
4
|
-
const { UPDATED, FAILED, SKIPPED } = LOGIN_EMAIL_SYNC_STATUS;
|
|
5
|
-
|
|
6
|
-
describe('summarizeLoginEmailOutcomes', () => {
|
|
7
|
-
test('collects only FAILED outcomes as failures and failed ids', () => {
|
|
8
|
-
const outcomes = [
|
|
9
|
-
{ memberId: 1, wixMemberId: 'w1', desiredEmail: 'a@x.com', status: UPDATED },
|
|
10
|
-
{
|
|
11
|
-
memberId: 2,
|
|
12
|
-
wixMemberId: 'w2',
|
|
13
|
-
desiredEmail: 'b@x.com',
|
|
14
|
-
status: FAILED,
|
|
15
|
-
error: 'duplicate',
|
|
16
|
-
},
|
|
17
|
-
{ memberId: 3, status: SKIPPED, desiredEmail: 'c@x.com' },
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
const { failedMemberIds, failures } = summarizeLoginEmailOutcomes(outcomes);
|
|
21
|
-
|
|
22
|
-
expect([...failedMemberIds]).toEqual([2]);
|
|
23
|
-
expect(failures).toEqual([
|
|
24
|
-
{ memberId: 2, wixMemberId: 'w2', desiredEmail: 'b@x.com', error: 'duplicate' },
|
|
25
|
-
]);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test('UPDATED and SKIPPED never produce failures (CMS email is left to advance/no-op)', () => {
|
|
29
|
-
const outcomes = [
|
|
30
|
-
{ memberId: 1, status: UPDATED, desiredEmail: 'a@x.com' },
|
|
31
|
-
{ memberId: 2, status: SKIPPED, desiredEmail: 'b@x.com' },
|
|
32
|
-
];
|
|
33
|
-
const { failedMemberIds, failures } = summarizeLoginEmailOutcomes(outcomes);
|
|
34
|
-
expect(failedMemberIds.size).toBe(0);
|
|
35
|
-
expect(failures).toEqual([]);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test('defaults a missing error message', () => {
|
|
39
|
-
const { failures } = summarizeLoginEmailOutcomes([
|
|
40
|
-
{ memberId: 9, wixMemberId: 'w9', desiredEmail: 'z@x.com', status: FAILED },
|
|
41
|
-
]);
|
|
42
|
-
expect(failures[0].error).toBe('unknown error');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test('handles empty / missing input', () => {
|
|
46
|
-
expect(summarizeLoginEmailOutcomes([]).failures).toEqual([]);
|
|
47
|
-
expect(summarizeLoginEmailOutcomes().failures).toEqual([]);
|
|
48
|
-
});
|
|
49
|
-
});
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
const { ADDRESS_STATUS_TYPES } = require('../../public/consts');
|
|
2
|
-
const { buildMapLink } = require('../../public/Utils/sharedUtils');
|
|
3
|
-
|
|
4
|
-
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
5
|
-
|
|
6
|
-
const fullAddress = (overrides = {}) => ({
|
|
7
|
-
line1: '1366 4th Ave',
|
|
8
|
-
line2: '',
|
|
9
|
-
city: 'Coraopolis',
|
|
10
|
-
state: 'PA',
|
|
11
|
-
postalcode: '15108-1675',
|
|
12
|
-
latitude: 40.4129180908203,
|
|
13
|
-
longitude: -80.0293197631836,
|
|
14
|
-
addressStatus: ADDRESS_STATUS_TYPES.FULL_ADDRESS,
|
|
15
|
-
...overrides,
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
const queryOf = url => decodeURIComponent((url.split('?q=')[1] || '').replace(/\+/g, ' '));
|
|
19
|
-
|
|
20
|
-
// ─── Monday 12596102059 ──────────────────────────────────────────────
|
|
21
|
-
// NetForum's address verifier returned coordinates ~10.8 miles from the real
|
|
22
|
-
// address for member 1806273. The directions link should resolve from the
|
|
23
|
-
// address text, which is correct, rather than from the coordinates.
|
|
24
|
-
|
|
25
|
-
describe('buildMapLink - prefers the address over coordinates', () => {
|
|
26
|
-
it('builds the link from the street address, not the lat/long', () => {
|
|
27
|
-
const url = buildMapLink(fullAddress());
|
|
28
|
-
|
|
29
|
-
expect(url).toContain('maps.google.com');
|
|
30
|
-
expect(queryOf(url)).toBe('1366 4th Ave, Coraopolis, PA, 15108');
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('does not put the stored coordinates in the link when an address exists', () => {
|
|
34
|
-
const url = buildMapLink(fullAddress());
|
|
35
|
-
|
|
36
|
-
expect(url).not.toContain('40.4129');
|
|
37
|
-
expect(url).not.toContain('-80.0293');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('still resolves correctly when the coordinates are wrong', () => {
|
|
41
|
-
// Coordinates deliberately far from the address - the link must ignore them.
|
|
42
|
-
const url = buildMapLink(fullAddress({ latitude: 0, longitude: 0 }));
|
|
43
|
-
|
|
44
|
-
expect(queryOf(url)).toBe('1366 4th Ave, Coraopolis, PA, 15108');
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('url-encodes the address so spaces do not break the link', () => {
|
|
48
|
-
const url = buildMapLink(fullAddress());
|
|
49
|
-
|
|
50
|
-
expect(url).not.toMatch(/\?q=.*\s/);
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
// ─── Privacy ─────────────────────────────────────────────────────────
|
|
55
|
-
// formatAddress omits line1 for anything other than full_address. Callers gate
|
|
56
|
-
// on full_address before showing the button, but the helper must not leak a
|
|
57
|
-
// hidden street address even if it is called directly.
|
|
58
|
-
|
|
59
|
-
describe('buildMapLink - does not leak a hidden street address', () => {
|
|
60
|
-
it('omits the street line for a state_city_zip address', () => {
|
|
61
|
-
const url = buildMapLink(fullAddress({ addressStatus: ADDRESS_STATUS_TYPES.STATE_CITY_ZIP }));
|
|
62
|
-
|
|
63
|
-
expect(url).not.toContain('4th');
|
|
64
|
-
expect(queryOf(url)).toBe('Coraopolis, PA, 15108');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('falls back to coordinates rather than the street line for dont_show', () => {
|
|
68
|
-
const url = buildMapLink(fullAddress({ addressStatus: ADDRESS_STATUS_TYPES.DONT_SHOW }));
|
|
69
|
-
|
|
70
|
-
expect(url).not.toContain('4th');
|
|
71
|
-
expect(url).toBe('https://maps.google.com/?q=40.4129180908203,-80.0293197631836');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('truncates the postal code to five digits', () => {
|
|
75
|
-
const url = buildMapLink(fullAddress());
|
|
76
|
-
|
|
77
|
-
expect(url).not.toContain('1675');
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
// ─── Fallbacks ───────────────────────────────────────────────────────
|
|
82
|
-
|
|
83
|
-
describe('buildMapLink - fallbacks', () => {
|
|
84
|
-
it('uses coordinates when the address has no printable parts', () => {
|
|
85
|
-
const url = buildMapLink({
|
|
86
|
-
line1: '',
|
|
87
|
-
line2: '',
|
|
88
|
-
city: '',
|
|
89
|
-
state: '',
|
|
90
|
-
postalcode: '',
|
|
91
|
-
latitude: 40.5,
|
|
92
|
-
longitude: -80.1,
|
|
93
|
-
addressStatus: ADDRESS_STATUS_TYPES.FULL_ADDRESS,
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
expect(url).toBe('https://maps.google.com/?q=40.5,-80.1');
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('returns an empty string when there is neither an address nor coordinates', () => {
|
|
100
|
-
expect(
|
|
101
|
-
buildMapLink({
|
|
102
|
-
line1: '',
|
|
103
|
-
city: '',
|
|
104
|
-
state: '',
|
|
105
|
-
postalcode: '',
|
|
106
|
-
addressStatus: ADDRESS_STATUS_TYPES.FULL_ADDRESS,
|
|
107
|
-
})
|
|
108
|
-
).toBe('');
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('returns an empty string for a missing address', () => {
|
|
112
|
-
expect(buildMapLink(null)).toBe('');
|
|
113
|
-
expect(buildMapLink(undefined)).toBe('');
|
|
114
|
-
});
|
|
115
|
-
});
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
jest.mock('../elevated-modules', () => ({
|
|
2
|
-
wixData: { query: jest.fn() },
|
|
3
|
-
}));
|
|
4
|
-
|
|
5
|
-
const { wixData } = require('../elevated-modules');
|
|
6
|
-
const { findMembersByIds } = require('../members-data-methods');
|
|
7
|
-
const { withTransientErrorRetry, isTransientNetworkError } = require('../utils');
|
|
8
|
-
|
|
9
|
-
const makeQueryResult = items => ({ items, hasNext: () => false });
|
|
10
|
-
|
|
11
|
-
const mockQueryReturning = find => {
|
|
12
|
-
const query = {
|
|
13
|
-
hasSome: jest.fn().mockReturnThis(),
|
|
14
|
-
limit: jest.fn().mockReturnThis(),
|
|
15
|
-
find,
|
|
16
|
-
};
|
|
17
|
-
wixData.query.mockReturnValue(query);
|
|
18
|
-
return query;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
describe('isTransientNetworkError', () => {
|
|
22
|
-
test('matches undici "fetch failed" and common network error codes', () => {
|
|
23
|
-
expect(isTransientNetworkError(new Error('fetch failed'))).toBe(true);
|
|
24
|
-
expect(isTransientNetworkError({ message: 'request failed', code: 'ECONNRESET' })).toBe(true);
|
|
25
|
-
expect(
|
|
26
|
-
isTransientNetworkError({ message: 'fetch failed', cause: { code: 'UND_ERR_SOCKET' } })
|
|
27
|
-
).toBe(true);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test('does not match application errors', () => {
|
|
31
|
-
expect(isTransientNetworkError(new Error('Multiple members found with memberId 1'))).toBe(
|
|
32
|
-
false
|
|
33
|
-
);
|
|
34
|
-
expect(isTransientNetworkError(new Error('WDE0025: validation failed'))).toBe(false);
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
describe('withTransientErrorRetry', () => {
|
|
39
|
-
test('retries transient failures and resolves with the eventual result', async () => {
|
|
40
|
-
const operation = jest
|
|
41
|
-
.fn()
|
|
42
|
-
.mockRejectedValueOnce(new Error('fetch failed'))
|
|
43
|
-
.mockResolvedValueOnce('ok');
|
|
44
|
-
|
|
45
|
-
await expect(withTransientErrorRetry(operation, { baseDelayMs: 1 })).resolves.toBe('ok');
|
|
46
|
-
expect(operation).toHaveBeenCalledTimes(2);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('rethrows immediately on non-transient errors', async () => {
|
|
50
|
-
const operation = jest.fn().mockRejectedValue(new Error('validation failed'));
|
|
51
|
-
|
|
52
|
-
await expect(withTransientErrorRetry(operation, { baseDelayMs: 1 })).rejects.toThrow(
|
|
53
|
-
'validation failed'
|
|
54
|
-
);
|
|
55
|
-
expect(operation).toHaveBeenCalledTimes(1);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test('gives up after the configured number of retries', async () => {
|
|
59
|
-
const operation = jest.fn().mockRejectedValue(new Error('fetch failed'));
|
|
60
|
-
|
|
61
|
-
await expect(
|
|
62
|
-
withTransientErrorRetry(operation, { retries: 2, baseDelayMs: 1 })
|
|
63
|
-
).rejects.toThrow('fetch failed');
|
|
64
|
-
expect(operation).toHaveBeenCalledTimes(3);
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
describe('findMembersByIds', () => {
|
|
69
|
-
beforeEach(() => {
|
|
70
|
-
wixData.query.mockReset();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test('returns a map of String(memberId) to member record', async () => {
|
|
74
|
-
mockQueryReturning(
|
|
75
|
-
jest.fn().mockResolvedValue(
|
|
76
|
-
makeQueryResult([
|
|
77
|
-
{ _id: 'a', memberId: 1 },
|
|
78
|
-
{ _id: 'b', memberId: 2 },
|
|
79
|
-
])
|
|
80
|
-
)
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
const membersById = await findMembersByIds([1, 2, 3]);
|
|
84
|
-
|
|
85
|
-
expect(membersById.get('1')).toEqual({ _id: 'a', memberId: 1 });
|
|
86
|
-
expect(membersById.get('2')).toEqual({ _id: 'b', memberId: 2 });
|
|
87
|
-
expect(membersById.has('3')).toBe(false);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test('deduplicates input IDs and skips null/undefined before querying', async () => {
|
|
91
|
-
const query = mockQueryReturning(jest.fn().mockResolvedValue(makeQueryResult([])));
|
|
92
|
-
|
|
93
|
-
await findMembersByIds([1, 1, null, undefined, 2]);
|
|
94
|
-
|
|
95
|
-
expect(query.hasSome).toHaveBeenCalledWith('memberId', [1, 2]);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test('returns an empty map without querying when there are no IDs', async () => {
|
|
99
|
-
const membersById = await findMembersByIds([]);
|
|
100
|
-
|
|
101
|
-
expect(membersById.size).toBe(0);
|
|
102
|
-
expect(wixData.query).not.toHaveBeenCalled();
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test('chunks large ID lists into multiple queries', async () => {
|
|
106
|
-
const query = mockQueryReturning(jest.fn().mockResolvedValue(makeQueryResult([])));
|
|
107
|
-
const ids = Array.from({ length: 250 }, (_, i) => i + 1);
|
|
108
|
-
|
|
109
|
-
await findMembersByIds(ids);
|
|
110
|
-
|
|
111
|
-
expect(query.hasSome).toHaveBeenCalledTimes(3);
|
|
112
|
-
expect(query.hasSome.mock.calls.map(call => call[1].length)).toEqual([100, 100, 50]);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test('throws when multiple records share the same memberId', async () => {
|
|
116
|
-
mockQueryReturning(
|
|
117
|
-
jest.fn().mockResolvedValue(
|
|
118
|
-
makeQueryResult([
|
|
119
|
-
{ _id: 'a', memberId: 1 },
|
|
120
|
-
{ _id: 'b', memberId: 1 },
|
|
121
|
-
])
|
|
122
|
-
)
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
await expect(findMembersByIds([1])).rejects.toThrow(
|
|
126
|
-
'Multiple members found with memberId(s): [1]'
|
|
127
|
-
);
|
|
128
|
-
});
|
|
129
|
-
});
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
const { incrementUrlCounter, extractBaseUrl } = require('../daily-pull/utils');
|
|
2
|
-
const {
|
|
3
|
-
normalizeUrlForComparison,
|
|
4
|
-
sortByUrlCounterDescending,
|
|
5
|
-
extractUrlCounter,
|
|
6
|
-
} = require('../utils');
|
|
7
|
-
|
|
8
|
-
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Simulates getMemberBySlug's normalized-comparison branch using
|
|
12
|
-
* the ACTUAL production sort comparator imported from utils.js.
|
|
13
|
-
*/
|
|
14
|
-
function simulateGetHighestMember(allMembers, slug) {
|
|
15
|
-
const matching = allMembers.filter(
|
|
16
|
-
m => m.url && normalizeUrlForComparison(m.url) === slug.toLowerCase()
|
|
17
|
-
);
|
|
18
|
-
matching.sort(sortByUrlCounterDescending);
|
|
19
|
-
return matching[0] || null;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Simulates ensureUniqueUrl's counter-increment logic given the
|
|
24
|
-
* "highest" member returned by getMemberBySlug.
|
|
25
|
-
*/
|
|
26
|
-
function simulateEnsureUniqueUrl(baseSlug, highestMember) {
|
|
27
|
-
if (!highestMember || !highestMember.url) return baseSlug;
|
|
28
|
-
return incrementUrlCounter(highestMember?.url, baseSlug);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// ─── Test data ───────────────────────────────────────────────────────
|
|
32
|
-
|
|
33
|
-
function buildMembersInDb() {
|
|
34
|
-
return [
|
|
35
|
-
{ memberId: 1, url: 'firstNameLastName' },
|
|
36
|
-
{ memberId: 2, url: 'firstNameLastName-1' },
|
|
37
|
-
{ memberId: 3, url: 'firstNameLastName-2' },
|
|
38
|
-
{ memberId: 4, url: 'firstNameLastName-3' },
|
|
39
|
-
{ memberId: 5, url: 'firstNameLastName-4' },
|
|
40
|
-
{ memberId: 6, url: 'firstNameLastName-5' },
|
|
41
|
-
{ memberId: 7, url: 'firstNameLastName-6' },
|
|
42
|
-
{ memberId: 8, url: 'firstNameLastName-7' },
|
|
43
|
-
{ memberId: 9, url: 'firstNameLastName-8' },
|
|
44
|
-
{ memberId: 10, url: 'firstNameLastName-9' },
|
|
45
|
-
{ memberId: 11, url: 'firstNameLastName-10' },
|
|
46
|
-
{ memberId: 12, url: 'firstNameLastName-11' },
|
|
47
|
-
{ memberId: 13, url: 'firstNameLastName-12' },
|
|
48
|
-
];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function buildLargeCounterMembers() {
|
|
52
|
-
const members = [];
|
|
53
|
-
for (let i = 0; i <= 105; i++) {
|
|
54
|
-
members.push({ memberId: i, url: i === 0 ? 'testUser' : `testUser-${i}` });
|
|
55
|
-
}
|
|
56
|
-
return members;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ─── Tests ───────────────────────────────────────────────────────────
|
|
60
|
-
|
|
61
|
-
describe('Production sort: getMemberBySlug must return the HIGHEST counter', () => {
|
|
62
|
-
test('should return -12 as the highest URL (not -9)', () => {
|
|
63
|
-
const members = buildMembersInDb();
|
|
64
|
-
const highest = simulateGetHighestMember(members, 'firstnamelastname');
|
|
65
|
-
|
|
66
|
-
expect(highest.url).toBe('firstNameLastName-12');
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test('should sort -12 above -9 in descending order', () => {
|
|
70
|
-
const members = buildMembersInDb();
|
|
71
|
-
const sorted = [...members]
|
|
72
|
-
.filter(m => normalizeUrlForComparison(m.url) === 'firstnamelastname')
|
|
73
|
-
.sort(sortByUrlCounterDescending);
|
|
74
|
-
const sortedUrls = sorted.map(m => m.url);
|
|
75
|
-
|
|
76
|
-
const indexOf9 = sortedUrls.indexOf('firstNameLastName-9');
|
|
77
|
-
const indexOf12 = sortedUrls.indexOf('firstNameLastName-12');
|
|
78
|
-
|
|
79
|
-
expect(indexOf12).toBeLessThan(indexOf9);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test('should handle large counters (100+) correctly', () => {
|
|
83
|
-
const members = buildLargeCounterMembers();
|
|
84
|
-
const highest = simulateGetHighestMember(members, 'testuser');
|
|
85
|
-
|
|
86
|
-
expect(highest.url).toBe('testUser-105');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test('should handle hyphenated names (mary-jane) correctly', () => {
|
|
90
|
-
const members = [
|
|
91
|
-
{ memberId: 1, url: 'mary-jane' },
|
|
92
|
-
{ memberId: 2, url: 'mary-jane-1' },
|
|
93
|
-
{ memberId: 3, url: 'mary-jane-2' },
|
|
94
|
-
{ memberId: 4, url: 'mary-jane-10' },
|
|
95
|
-
];
|
|
96
|
-
|
|
97
|
-
const highest = simulateGetHighestMember(members, 'mary-jane');
|
|
98
|
-
expect(highest.url).toBe('mary-jane-10');
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
describe('Production sort: ensureUniqueUrl must generate a truly unique URL', () => {
|
|
103
|
-
test('should generate -13 (not -10) when DB has URLs up to -12', () => {
|
|
104
|
-
const members = buildMembersInDb();
|
|
105
|
-
const highest = simulateGetHighestMember(members, 'firstnamelastname');
|
|
106
|
-
const newUrl = simulateEnsureUniqueUrl('firstNameLastName', highest);
|
|
107
|
-
|
|
108
|
-
expect(newUrl).toBe('firstNameLastName-13');
|
|
109
|
-
|
|
110
|
-
const existingUrls = members.map(m => m.url);
|
|
111
|
-
expect(existingUrls).not.toContain(newUrl);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test('repeated daily pulls should produce sequential unique URLs', () => {
|
|
115
|
-
const members = buildMembersInDb();
|
|
116
|
-
const generatedUrls = [];
|
|
117
|
-
|
|
118
|
-
for (let day = 0; day < 5; day++) {
|
|
119
|
-
const highest = simulateGetHighestMember(members, 'firstnamelastname');
|
|
120
|
-
const newUrl = simulateEnsureUniqueUrl('firstNameLastName', highest);
|
|
121
|
-
generatedUrls.push(newUrl);
|
|
122
|
-
members.push({ memberId: 100 + day, url: newUrl });
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
expect(new Set(generatedUrls).size).toBe(5);
|
|
126
|
-
expect(generatedUrls).toEqual([
|
|
127
|
-
'firstNameLastName-13',
|
|
128
|
-
'firstNameLastName-14',
|
|
129
|
-
'firstNameLastName-15',
|
|
130
|
-
'firstNameLastName-16',
|
|
131
|
-
'firstNameLastName-17',
|
|
132
|
-
]);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test('should NOT produce duplicates (the -10 bug)', () => {
|
|
136
|
-
const members = buildMembersInDb();
|
|
137
|
-
const highest = simulateGetHighestMember(members, 'firstnamelastname');
|
|
138
|
-
const newUrl = simulateEnsureUniqueUrl('firstNameLastName', highest);
|
|
139
|
-
|
|
140
|
-
expect(newUrl).not.toBe('firstNameLastName-10');
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
describe('Production sort: ensureUniqueUrlsInBatch single-member path', () => {
|
|
145
|
-
test('incrementUrlCounter should produce a unique URL from highest DB member', () => {
|
|
146
|
-
const members = buildMembersInDb();
|
|
147
|
-
const dbMember = simulateGetHighestMember(members, 'firstnamelastname');
|
|
148
|
-
const result = incrementUrlCounter(dbMember.url, 'firstNameLastName');
|
|
149
|
-
|
|
150
|
-
expect(result).toBe('firstNameLastName-13');
|
|
151
|
-
|
|
152
|
-
const existingUrls = members.map(m => m.url);
|
|
153
|
-
expect(existingUrls).not.toContain(result);
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
describe('Utility functions', () => {
|
|
158
|
-
test('normalizeUrlForComparison strips trailing counter', () => {
|
|
159
|
-
expect(normalizeUrlForComparison('firstNameLastName')).toBe('firstnamelastname');
|
|
160
|
-
expect(normalizeUrlForComparison('firstNameLastName-1')).toBe('firstnamelastname');
|
|
161
|
-
expect(normalizeUrlForComparison('firstNameLastName-9')).toBe('firstnamelastname');
|
|
162
|
-
expect(normalizeUrlForComparison('firstNameLastName-10')).toBe('firstnamelastname');
|
|
163
|
-
expect(normalizeUrlForComparison('firstNameLastName-100')).toBe('firstnamelastname');
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
test('extractUrlCounter returns numeric counter', () => {
|
|
167
|
-
expect(extractUrlCounter('firstNameLastName')).toBe(-1);
|
|
168
|
-
expect(extractUrlCounter('firstNameLastName-1')).toBe(1);
|
|
169
|
-
expect(extractUrlCounter('firstNameLastName-9')).toBe(9);
|
|
170
|
-
expect(extractUrlCounter('firstNameLastName-10')).toBe(10);
|
|
171
|
-
expect(extractUrlCounter('firstNameLastName-100')).toBe(100);
|
|
172
|
-
expect(extractUrlCounter(null)).toBe(-1);
|
|
173
|
-
expect(extractUrlCounter('')).toBe(-1);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
test('extractBaseUrl strips numeric counter', () => {
|
|
177
|
-
expect(extractBaseUrl('firstNameLastName-10')).toBe('firstNameLastName');
|
|
178
|
-
expect(extractBaseUrl('firstNameLastName-1')).toBe('firstNameLastName');
|
|
179
|
-
expect(extractBaseUrl('firstNameLastName')).toBe('firstNameLastName');
|
|
180
|
-
expect(extractBaseUrl('john-doe-3')).toBe('john-doe');
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
test('incrementUrlCounter increments correctly', () => {
|
|
184
|
-
expect(incrementUrlCounter('firstNameLastName-9', 'firstNameLastName')).toBe(
|
|
185
|
-
'firstNameLastName-10'
|
|
186
|
-
);
|
|
187
|
-
expect(incrementUrlCounter('firstNameLastName-10', 'firstNameLastName')).toBe(
|
|
188
|
-
'firstNameLastName-11'
|
|
189
|
-
);
|
|
190
|
-
expect(incrementUrlCounter('firstNameLastName', 'firstNameLastName')).toBe(
|
|
191
|
-
'firstNameLastName-1'
|
|
192
|
-
);
|
|
193
|
-
});
|
|
194
|
-
});
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
const { isNotValidUrl } = require('../../public/Utils/personalDetailsUtils');
|
|
2
|
-
|
|
3
|
-
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
4
|
-
|
|
5
|
-
const isValid = url => !isNotValidUrl(url);
|
|
6
|
-
|
|
7
|
-
// ─── Regression: digits in the hostname ──────────────────────────────
|
|
8
|
-
// Monday bug 12663709539 - a member could not save the booking link
|
|
9
|
-
// https://patty-10439.square.site because the host character class was
|
|
10
|
-
// written `[da-z.-]` instead of `[\da-z.-]`, rejecting every domain
|
|
11
|
-
// containing a digit.
|
|
12
|
-
|
|
13
|
-
describe('isNotValidUrl - digits in hostname', () => {
|
|
14
|
-
it('accepts the exact URL from the bug report', () => {
|
|
15
|
-
expect(isValid('https://patty-10439.square.site')).toBe(true);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it.each([
|
|
19
|
-
'https://my-spa123.com',
|
|
20
|
-
'https://massage4u.net',
|
|
21
|
-
'https://booksy.com/en-us/698924_therapist_health-fitness_119607_city',
|
|
22
|
-
'https://www.genbook.com/bookings/slot/reservation/30241562?bookingSourceId=1000',
|
|
23
|
-
'www.spa2go.com',
|
|
24
|
-
'https://123.example.com',
|
|
25
|
-
])('accepts %s', url => {
|
|
26
|
-
expect(isValid(url)).toBe(true);
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe('isNotValidUrl - case insensitivity', () => {
|
|
31
|
-
it.each(['https://Patty-10439.Square.Site', 'HTTPS://EXAMPLE.COM', 'WWW.Example.Com'])(
|
|
32
|
-
'accepts %s',
|
|
33
|
-
url => {
|
|
34
|
-
expect(isValid(url)).toBe(true);
|
|
35
|
-
}
|
|
36
|
-
);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// ─── Guard against regressions in the previously-working cases ───────
|
|
40
|
-
|
|
41
|
-
describe('isNotValidUrl - previously valid URLs stay valid', () => {
|
|
42
|
-
it.each([
|
|
43
|
-
'https://square.site',
|
|
44
|
-
'https://patty.square.site',
|
|
45
|
-
'http://healinghut.massagetherapy.com',
|
|
46
|
-
'www.example.com',
|
|
47
|
-
'https://example.co.uk',
|
|
48
|
-
'https://example.com/path/to/page',
|
|
49
|
-
'https://example.com?foo=bar',
|
|
50
|
-
'https://example.com#anchor',
|
|
51
|
-
])('accepts %s', url => {
|
|
52
|
-
expect(isValid(url)).toBe(true);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe('isNotValidUrl - invalid input is still rejected', () => {
|
|
57
|
-
it.each(['not a url', 'example', 'ftp://example.com', 'justtext.c', 'http://'])(
|
|
58
|
-
'rejects %s',
|
|
59
|
-
url => {
|
|
60
|
-
expect(isValid(url)).toBe(false);
|
|
61
|
-
}
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
it('treats an empty value as valid because the field is optional', () => {
|
|
65
|
-
expect(isValid('')).toBe(true);
|
|
66
|
-
expect(isValid(undefined)).toBe(true);
|
|
67
|
-
expect(isValid(null)).toBe(true);
|
|
68
|
-
});
|
|
69
|
-
});
|