abmp-npm 10.0.57 → 10.0.59

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.
@@ -0,0 +1,129 @@
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
+ });
@@ -28,14 +28,13 @@ async function ensureUniqueUrlsInBatch(memberDataList) {
28
28
  const baseUrl = extractBaseUrl(member.url);
29
29
  const groupKey = baseUrl.toLowerCase();
30
30
  if (!urlGroups.has(groupKey)) {
31
- urlGroups.set(groupKey, []);
31
+ urlGroups.set(groupKey, { members: [], baseUrl });
32
32
  }
33
- urlGroups.get(groupKey).push(member);
33
+ urlGroups.get(groupKey).members.push(member);
34
34
  });
35
35
 
36
36
  // For each group, check database and assign unique URLs sequentially
37
- for (const [groupKey, members] of urlGroups.entries()) {
38
- const baseUrl = groupKey; // lowercase for consistent slug assignment
37
+ for (const [, { members, baseUrl }] of urlGroups.entries()) {
39
38
  if (members.length <= 1) {
40
39
  // Single member - still check DB to ensure it doesn't conflict with other pages
41
40
  const member = members[0];
package/backend/jobs.js CHANGED
@@ -82,6 +82,20 @@ async function scheduleFixPrimaryAddressForMembersTask() {
82
82
  }
83
83
  }
84
84
 
85
+ async function scheduleFixUrlsWithSpacesTask() {
86
+ try {
87
+ console.log('scheduleFixUrlsWithSpaces started!');
88
+ return await taskManager().schedule({
89
+ name: TASKS_NAMES.scheduleFixUrlsWithSpaces,
90
+ data: {},
91
+ type: 'scheduled',
92
+ });
93
+ } catch (error) {
94
+ console.error(`Failed to scheduleFixUrlsWithSpaces: ${error.message}`);
95
+ throw new Error(`Failed to scheduleFixUrlsWithSpaces: ${error.message}`);
96
+ }
97
+ }
98
+
85
99
  async function updateSiteMapS3() {
86
100
  try {
87
101
  return await taskManager().schedule({
@@ -100,4 +114,5 @@ module.exports = {
100
114
  updateSiteMapS3,
101
115
  scheduleCreateContactsFromMembersTask,
102
116
  scheduleFixPrimaryAddressForMembersTask,
117
+ scheduleFixUrlsWithSpacesTask,
103
118
  };
@@ -20,6 +20,8 @@ const TASKS_NAMES = {
20
20
  createContactsFromMembers: 'createContactsFromMembers',
21
21
  scheduleFixPrimaryAddressForMembers: 'scheduleFixPrimaryAddressForMembers',
22
22
  fixPrimaryAddressChunk: 'fixPrimaryAddressChunk',
23
+ scheduleFixUrlsWithSpaces: 'scheduleFixUrlsWithSpaces',
24
+ fixUrlsWithSpacesChunk: 'fixUrlsWithSpacesChunk',
23
25
  };
24
26
 
25
27
  module.exports = {
@@ -5,4 +5,5 @@ module.exports = {
5
5
  ...require('./migration-methods'),
6
6
  ...require('./url-migration-methods'),
7
7
  ...require('./address-primary-methods'),
8
+ ...require('./url-space-fix-methods'),
8
9
  };
@@ -29,6 +29,7 @@ const {
29
29
  scheduleGenerateMissingUrls,
30
30
  generateUrlsChunk,
31
31
  } = require('./url-migration-methods');
32
+ const { scheduleFixUrlsWithSpaces, fixUrlsWithSpacesChunk } = require('./url-space-fix-methods');
32
33
 
33
34
  const getDailyMembersDataSyncChildTasks = () => {
34
35
  // we don't want to sync none action as it means this members data hasn't changed and we don't need to sync it
@@ -187,6 +188,20 @@ const TASKS = {
187
188
  shouldSkipCheck: () => false,
188
189
  estimatedDurationSec: 80,
189
190
  },
191
+ [TASKS_NAMES.scheduleFixUrlsWithSpaces]: {
192
+ name: TASKS_NAMES.scheduleFixUrlsWithSpaces,
193
+ getIdentifier: () => 'SHOULD_NEVER_SKIP',
194
+ process: scheduleFixUrlsWithSpaces,
195
+ shouldSkipCheck: () => false,
196
+ estimatedDurationSec: 80,
197
+ },
198
+ [TASKS_NAMES.fixUrlsWithSpacesChunk]: {
199
+ name: TASKS_NAMES.fixUrlsWithSpacesChunk,
200
+ getIdentifier: task => task.data,
201
+ process: fixUrlsWithSpacesChunk,
202
+ shouldSkipCheck: () => false,
203
+ estimatedDurationSec: 80,
204
+ },
190
205
  };
191
206
 
192
207
  module.exports = { TASKS };
@@ -179,7 +179,7 @@ const updateSiteMapS3 = async () => {
179
179
  // const creds = await getNewStsSessionToken(tokens.AWS_ACCESS_KEY_ID, tokens.AWS_SECRET_ACCESS_KEY, 3600);
180
180
  // console.log("creds",creds); // verify it’s fresh
181
181
  try {
182
- const chunkSize = 50000;
182
+ const chunkSize = 10000;
183
183
  console.log('Total items will be split into', relevantMembers.length / chunkSize);
184
184
  const chunks = chunkArray(relevantMembers, chunkSize);
185
185
  console.log(`Uploading ${chunks.length} sitemap files...`);
@@ -0,0 +1,174 @@
1
+ const { taskManager } = require('psdev-task-manager');
2
+
3
+ const { COLLECTIONS } = require('../../public/consts');
4
+ const { ensureUniqueUrlsInBatch } = require('../daily-pull/bulk-process-methods');
5
+ const { ensureUniqueUrl } = require('../daily-pull/process-member-methods');
6
+ const { wixData } = require('../elevated-modules');
7
+ const { bulkSaveMembers, getMembersByIds } = require('../members-data-methods');
8
+ const { queryAllItems, chunkArray } = require('../utils');
9
+
10
+ const { TASKS_NAMES } = require('./consts');
11
+
12
+ const CHUNK_SIZE = 1000;
13
+
14
+ const hasSpace = value => typeof value === 'string' && /\s/.test(value);
15
+
16
+ const normalizeSlug = value => (value || '').replace(/\s+/g, '').trim();
17
+
18
+ /**
19
+ * Schedules tasks to fix member URLs that contain spaces.
20
+ */
21
+ async function scheduleFixUrlsWithSpaces() {
22
+ console.log('=== Scheduling Fix URLs With Spaces ===');
23
+
24
+ try {
25
+ const membersQuery = await wixData
26
+ .query(COLLECTIONS.MEMBERS_DATA)
27
+ .contains('url', ' ')
28
+ .limit(1000);
29
+ const members = await queryAllItems(membersQuery);
30
+ console.log(`Fetched ${members.length} members with spaces in url`);
31
+
32
+ const memberIds = [
33
+ ...new Set(
34
+ members
35
+ .map(member => Number(member.memberId))
36
+ .filter(memberId => Number.isFinite(memberId) && memberId > 0)
37
+ ),
38
+ ];
39
+ console.log(`Members to fix: ${memberIds.length}`);
40
+
41
+ if (memberIds.length === 0) {
42
+ console.log('No members need URL space fixes');
43
+ return {
44
+ success: true,
45
+ message: 'No members need URL space fixes',
46
+ totalMembers: 0,
47
+ tasksScheduled: 0,
48
+ };
49
+ }
50
+
51
+ const chunks = chunkArray(memberIds, CHUNK_SIZE);
52
+ for (let i = 0; i < chunks.length; i++) {
53
+ const chunk = chunks[i];
54
+ const task = {
55
+ name: TASKS_NAMES.fixUrlsWithSpacesChunk,
56
+ data: {
57
+ memberIds: chunk,
58
+ chunkIndex: i,
59
+ totalChunks: chunks.length,
60
+ },
61
+ type: 'scheduled',
62
+ };
63
+ await taskManager().schedule(task);
64
+ console.log(`Scheduled task ${i + 1}/${chunks.length} (${chunk.length} members)`);
65
+ }
66
+
67
+ const result = {
68
+ success: true,
69
+ message: `Scheduled ${chunks.length} tasks for ${memberIds.length} members`,
70
+ totalMembers: memberIds.length,
71
+ tasksScheduled: chunks.length,
72
+ };
73
+
74
+ console.log('=== Scheduling Complete ===');
75
+ console.log(`Sample memberIds: ${memberIds.slice(0, 10).join(', ')}`);
76
+ console.log(JSON.stringify(result, null, 2));
77
+
78
+ return result;
79
+ } catch (error) {
80
+ console.error('Error scheduling URL space fixes:', error);
81
+ throw error;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Processes a chunk of members and fixes URLs with spaces.
87
+ */
88
+ async function fixUrlsWithSpacesChunk(data) {
89
+ const { memberIds, chunkIndex, totalChunks } = data;
90
+ console.log(
91
+ `Processing URL space fix chunk ${chunkIndex + 1}/${totalChunks} (${memberIds.length} members)`
92
+ );
93
+
94
+ const result = {
95
+ successful: 0,
96
+ failed: 0,
97
+ skipped: 0,
98
+ errors: [],
99
+ skippedIds: [],
100
+ failedIds: [],
101
+ };
102
+ const updatedIds = [];
103
+
104
+ try {
105
+ const members = await getMembersByIds(memberIds);
106
+ console.log(`Loaded ${members.length} members for this chunk`);
107
+
108
+ const membersToUpdate = [];
109
+ for (const member of members) {
110
+ if (!hasSpace(member.url)) {
111
+ result.skipped++;
112
+ result.skippedIds.push(member.memberId);
113
+ continue;
114
+ }
115
+
116
+ const normalized = normalizeSlug(member.url);
117
+ const ensuredUrl = await ensureUniqueUrl({
118
+ url: normalized,
119
+ memberId: member.memberId,
120
+ fullName: member.fullName,
121
+ });
122
+
123
+ if (!ensuredUrl || ensuredUrl === member.url) {
124
+ result.skipped++;
125
+ result.skippedIds.push(member.memberId);
126
+ continue;
127
+ }
128
+
129
+ membersToUpdate.push({
130
+ ...member,
131
+ url: ensuredUrl,
132
+ });
133
+ }
134
+
135
+ if (membersToUpdate.length === 0) {
136
+ console.log('No members need updating in this batch');
137
+ return result;
138
+ }
139
+
140
+ const uniqueUpdates = await ensureUniqueUrlsInBatch(membersToUpdate);
141
+ uniqueUpdates.forEach(member => {
142
+ if (updatedIds.length < 20) {
143
+ updatedIds.push(member.memberId);
144
+ }
145
+ });
146
+
147
+ try {
148
+ await bulkSaveMembers(uniqueUpdates);
149
+ result.successful += uniqueUpdates.length;
150
+ console.log(`✅ Successfully updated ${uniqueUpdates.length} members`);
151
+ if (updatedIds.length > 0) {
152
+ console.log(`Updated memberIds (sample): ${updatedIds.join(', ')}`);
153
+ }
154
+ } catch (error) {
155
+ console.error('❌ Error bulk saving members:', error);
156
+ result.failed += uniqueUpdates.length;
157
+ result.failedIds.push(...uniqueUpdates.map(member => member.memberId));
158
+ result.errors.push({
159
+ error: error.message,
160
+ memberCount: uniqueUpdates.length,
161
+ });
162
+ }
163
+
164
+ return result;
165
+ } catch (error) {
166
+ console.error(`Error processing URL space fix chunk ${chunkIndex}:`, error);
167
+ throw error;
168
+ }
169
+ }
170
+
171
+ module.exports = {
172
+ scheduleFixUrlsWithSpaces,
173
+ fixUrlsWithSpacesChunk,
174
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abmp-npm",
3
- "version": "10.0.57",
3
+ "version": "10.0.59",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "check-cycles": "madge --circular .",
@@ -2033,14 +2033,29 @@ async function personalDetailsOnReady({
2033
2033
  });
2034
2034
 
2035
2035
  _$w('#showPhoneCheckbox').onChange(event => {
2036
- const data = _$w('#phoneNumbersList').data;
2036
+ const data = _$w('#phoneNumbersList').data || [];
2037
2037
  const clickedItemData = data.find(item => item._id === event.context.itemId);
2038
- const $item = _$w.at(event.context);
2038
+ if (!clickedItemData) {
2039
+ return;
2040
+ }
2039
2041
 
2040
- _$w('#showPhoneCheckbox').checked = false;
2041
- $item('#showPhoneCheckbox').checked = true;
2042
+ const isChecked = event.target.checked;
2043
+ let updated;
2044
+ if (isChecked) {
2045
+ updated = data.map(item =>
2046
+ item._id === clickedItemData._id
2047
+ ? { ...item, showPhone: true }
2048
+ : { ...item, showPhone: false }
2049
+ );
2050
+ updateShowPhoneSelection(clickedItemData._id, true);
2051
+ } else {
2052
+ updated = data.map(item =>
2053
+ item._id === clickedItemData._id ? { ...item, showPhone: false } : item
2054
+ );
2055
+ updateShowPhoneSelection(clickedItemData._id, false);
2056
+ }
2042
2057
 
2043
- updateShowPhoneSelection(clickedItemData._id, event.target.checked);
2058
+ renderPhonesList(updated);
2044
2059
  checkFormChanges(FORM_SECTION_HANDLER_MAP.CONTACT_BOOKING);
2045
2060
  });
2046
2061
 
@@ -2168,12 +2183,19 @@ async function personalDetailsOnReady({
2168
2183
  const currentData = _$w('#phoneNumbersList').data || [];
2169
2184
  const selectedItem = currentData.find(item => item._id === phoneId);
2170
2185
 
2171
- if (selectedItem && selectedItem.phoneNumber) {
2172
- if (isVisible) {
2173
- itemMemberObj.toShowPhone = selectedItem.phoneNumber;
2174
- } else {
2175
- itemMemberObj.toShowPhone = null;
2176
- }
2186
+ if (!selectedItem) {
2187
+ return;
2188
+ }
2189
+
2190
+ if (isVisible) {
2191
+ itemMemberObj.toShowPhone = selectedItem.phoneNumber?.trim()
2192
+ ? selectedItem.phoneNumber
2193
+ : null;
2194
+ return;
2195
+ }
2196
+
2197
+ if (selectedItem.phoneNumber) {
2198
+ itemMemberObj.toShowPhone = null;
2177
2199
  }
2178
2200
  }
2179
2201