bulltrackers-module 1.0.127 → 1.0.129

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.
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  * FILENAME: CloudFunctions/NpmWrappers/bulltrackers-module/functions/task-engine/helpers/update_helpers.js
3
- * (FIXED: Enhanced error logging for portfolio and history fetches)
3
+ * (MODIFIED: To conditionally fetch history API once per user per batch)
4
4
  */
5
5
 
6
6
  /**
@@ -9,227 +9,132 @@
9
9
  * OPTIMIZED: Removed immediate batch commit for speculator timestamp fix.
10
10
  * --- MODIFIED: Fetches portfolio AND trade history in parallel. ---
11
11
  * --- MODIFIED: Includes helper to look up usernames from CIDs. ---
12
+ * --- MODIFIED: Conditionally fetches history only once per user per batch. ---
12
13
  */
13
14
  const { FieldValue } = require('@google-cloud/firestore');
14
15
 
15
- /**
16
- * --- NEW HELPER ---
17
- * Fetches user details (including username) from the rankings API for a batch of CIDs.
18
- * @param {Array<string>} cids - An array of user CIDs (as strings or numbers).
19
- * @param {object} dependencies - Contains proxyManager, headerManager, logger.
20
- * @param {object} config - The configuration object.
21
- * @returns {Promise<Array<object>>} An array of the raw user objects from the API.
22
- */
23
- async function lookupUsernames(cids, dependencies, config) {
24
- const { logger, headerManager, proxyManager } = dependencies;
25
- const { USERNAME_LOOKUP_BATCH_SIZE, ETORO_API_RANKINGS_URL } = config;
26
-
27
- if (!cids || cids.length === 0) {
28
- return [];
29
- }
30
-
16
+ async function lookupUsernames(cids, { logger, headerManager, proxyManager }, { USERNAME_LOOKUP_BATCH_SIZE, ETORO_API_RANKINGS_URL }) {
17
+ if (!cids?.length) return [];
31
18
  logger.log('INFO', `[lookupUsernames] Looking up usernames for ${cids.length} CIDs.`);
32
- const allPublicUsers = [];
33
-
19
+ const allUsers = [];
34
20
  for (let i = 0; i < cids.length; i += USERNAME_LOOKUP_BATCH_SIZE) {
35
- const batchCids = cids.slice(i, i + USERNAME_LOOKUP_BATCH_SIZE).map(Number);
36
-
37
- const url = `${ETORO_API_RANKINGS_URL}?Period=LastTwoYears`;
38
- const selectedHeader = await headerManager.selectHeader();
39
- if (!selectedHeader) {
40
- logger.log('ERROR', '[lookupUsernames] Could not select a header.');
41
- continue; // Skip this batch
42
- }
43
-
44
- let wasSuccess = false;
21
+ const batch = cids.slice(i, i + USERNAME_LOOKUP_BATCH_SIZE).map(Number);
22
+ const header = await headerManager.selectHeader();
23
+ if (!header) { logger.log('ERROR', '[lookupUsernames] Could not select a header.'); continue; }
24
+ let success = false;
45
25
  try {
46
- const response = await proxyManager.fetch(url, {
47
- method: 'POST',
48
- headers: { ...selectedHeader.header, 'Content-Type': 'application/json' },
49
- body: JSON.stringify(batchCids),
26
+ const res = await proxyManager.fetch(`${ETORO_API_RANKINGS_URL}?Period=LastTwoYears`, {
27
+ method: 'POST', headers: { ...header.header, 'Content-Type': 'application/json' }, body: JSON.stringify(batch)
50
28
  });
51
-
52
- if (!response.ok) {
53
- throw new Error(`[lookupUsernames] API status ${response.status}`);
54
- }
55
- wasSuccess = true;
56
-
57
- const publicUsers = await response.json();
58
- if (Array.isArray(publicUsers)) {
59
- allPublicUsers.push(...publicUsers);
60
- }
61
- } catch (error) {
62
- logger.log('WARN', `[lookupUsernames] Failed to fetch batch of usernames.`, { errorMessage: error.message });
63
- } finally {
64
- if (selectedHeader) {
65
- headerManager.updatePerformance(selectedHeader.id, wasSuccess);
66
- }
67
- }
29
+ if (!res.ok) throw new Error(`API status ${res.status}`);
30
+ const data = await res.json();
31
+ if (Array.isArray(data)) allUsers.push(...data);
32
+ success = true;
33
+ } catch (err) {
34
+ logger.log('WARN', `[lookupUsernames] Failed batch`, { error: err.message });
35
+ } finally { headerManager.updatePerformance(header.id, success); }
68
36
  }
69
-
70
- logger.log('INFO', `[lookupUsernames] Found ${allPublicUsers.length} public users out of ${cids.length}.`);
71
- return allPublicUsers;
37
+ logger.log('INFO', `[lookupUsernames] Found ${allUsers.length} public users out of ${cids.length}.`);
38
+ return allUsers;
72
39
  }
73
40
 
41
+ // --- START MODIFICATION: Added historyFetchedForUser argument ---
42
+ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager, db, batchManager }, config, username, historyFetchedForUser) {
43
+ // --- END MODIFICATION ---
74
44
 
75
- /**
76
- * Sub-pipe: pipe.taskEngine.handleUpdate
77
- * @param {object} task The Pub/Sub task payload.
78
- * @param {string} taskId A unique ID for logging.
79
- * @param {object} dependencies - Contains db, pubsub, logger, headerManager, proxyManager, batchManager.
80
- * @param {object} config The configuration object.
81
- * @param {string} username - The user's eToro username.
82
- */
83
- async function handleUpdate(task, taskId, dependencies, config, username) {
84
- const { logger, headerManager, proxyManager, db, batchManager } = dependencies;
85
45
  const { userId, instrumentId, userType } = task;
86
-
87
- // --- START MODIFICATION: Define both URLs ---
88
- const portfolioUrl = userType === 'speculator'
89
- ? `${config.ETORO_API_POSITIONS_URL}?cid=${userId}&InstrumentID=${instrumentId}`
46
+ const portfolioUrl = userType === 'speculator'
47
+ ? `${config.ETORO_API_POSITIONS_URL}?cid=${userId}&InstrumentID=${instrumentId}`
90
48
  : `${config.ETORO_API_PORTFOLIO_URL}?cid=${userId}`;
91
-
92
- const historyUrl = `${config.ETORO_API_USERSTATS_URL}${username}/trades/oneYearAgo?CopyAsAsset=true`;
93
49
 
50
+ // --- MODIFICATION: Moved historyUrl definition inside conditional ---
94
51
  const today = new Date().toISOString().slice(0, 10);
95
- const portfolioStorageBlockId = `${Math.floor(parseInt(userId) / 1000000)}M`;
96
-
97
- let portfolioHeader = null;
98
- let historyHeader = null;
99
- let wasPortfolioSuccess = false;
100
- let wasHistorySuccess = false;
101
- let isPrivate = false;
52
+ const portfolioBlockId = `${Math.floor(parseInt(userId) / 1000000)}M`;
102
53
 
103
- try {
104
- logger.log('INFO', `[UPDATE] Fetching data for user ${userId} (${username}, ${userType})`);
54
+ // --- MODIFICATION: Select history header only if needed ---
55
+ let portfolioHeader = await headerManager.selectHeader();
56
+ let historyHeader = null; // Will be selected if needed
57
+ if (!portfolioHeader) throw new Error("Could not select portfolio header.");
105
58
 
106
- // Select headers for both requests
107
- portfolioHeader = await headerManager.selectHeader();
108
- historyHeader = await headerManager.selectHeader();
109
- if (!portfolioHeader || !historyHeader) {
110
- throw new Error("Could not select headers for update requests.");
111
- }
59
+ let wasPortfolioSuccess = false, wasHistorySuccess = false, isPrivate = false;
112
60
 
113
- // --- Fetch both endpoints in parallel ---
114
- const [portfolioResult, historyResult] = await Promise.allSettled([
115
- proxyManager.fetch(portfolioUrl, { headers: portfolioHeader.header }),
116
- proxyManager.fetch(historyUrl, { headers: historyHeader.header })
117
- ]);
118
-
119
- // --- Process Portfolio Result ---
120
- if (portfolioResult.status === 'fulfilled' && portfolioResult.value.ok) {
121
- const responseBody = await portfolioResult.value.text();
122
-
123
- // Check for private user
124
- if (responseBody.includes("user is PRIVATE")) {
125
- isPrivate = true;
61
+ try {
62
+ // --- START MODIFICATION: Build promises conditionally ---
63
+ const promisesToRun = [];
64
+
65
+ // 1. Always fetch portfolio
66
+ promisesToRun.push(proxyManager.fetch(portfolioUrl, { headers: portfolioHeader.header }));
67
+
68
+ // 2. Conditionally fetch history
69
+ let fetchHistory = false;
70
+ if (!historyFetchedForUser.has(userId)) {
71
+ historyHeader = await headerManager.selectHeader(); // Select header just-in-time
72
+ if (!historyHeader) {
73
+ logger.log('WARN', `[UPDATE] Could not select history header for ${userId}, skipping history fetch for this batch.`);
74
+ historyFetchedForUser.add(userId); // Add to set to prevent retries in this batch
126
75
  } else {
127
- wasPortfolioSuccess = true;
128
- const portfolioData = JSON.parse(responseBody);
129
- // Add to portfolio batch
130
- console.log('DEBUG', `[UPDATE] SUCCESS Adding portfolio data for user ${userId} to batch.`);
131
- await batchManager.addToPortfolioBatch(userId, portfolioStorageBlockId, today, portfolioData, userType, instrumentId);
132
-
76
+ fetchHistory = true;
77
+ historyFetchedForUser.add(userId); // Mark as fetched for this batch
78
+ const historyUrl = `${config.ETORO_API_USERSTATS_URL}${username}/trades/oneYearAgo?CopyAsAsset=true`;
79
+ promisesToRun.push(proxyManager.fetch(historyUrl, { headers: historyHeader.header }));
133
80
  }
81
+ }
82
+ // --- END MODIFICATION ---
83
+
84
+ // --- Run Promises ---
85
+ const results = await Promise.allSettled(promisesToRun);
86
+ const portfolioRes = results[0];
87
+ const historyRes = fetchHistory ? results[1] : null; // History is only index 1 if we fetched it
88
+
89
+ // --- Process Portfolio (results[0]) ---
90
+ if (portfolioRes.status === 'fulfilled' && portfolioRes.value.ok) {
91
+ const body = await portfolioRes.value.text();
92
+ if (body.includes("user is PRIVATE")) isPrivate = true;
93
+ else { wasPortfolioSuccess = true; await batchManager.addToPortfolioBatch(userId, portfolioBlockId, today, JSON.parse(body), userType, instrumentId); }
134
94
  } else {
135
- // --- FIX: ENHANCED LOGGING ---
136
- let errorMsg = 'Unknown fetch error';
137
- let rawErrorText = 'N/A';
138
-
139
- if (portfolioResult.status === 'rejected') {
140
- errorMsg = portfolioResult.reason.message;
141
- } else if (portfolioResult.value) {
142
- errorMsg = portfolioResult.value.error?.message || `API status ${portfolioResult.value.status}`;
143
- // Get the raw text from the proxy manager's response
144
- if (typeof portfolioResult.value.text === 'function') {
145
- rawErrorText = await portfolioResult.value.text();
146
- }
147
- }
148
-
149
- logger.log('WARN', `[UPDATE] Failed to fetch PORTFOLIO for user ${userId}.`, {
150
- error: errorMsg,
151
- proxyResponse: rawErrorText // This will contain "urlfetch" or other quota messages
152
- });
153
- // --- END FIX ---
95
+ let errMsg = portfolioRes.status === 'rejected' ? portfolioRes.reason.message : `API status ${portfolioRes.value.status}`;
96
+ let rawText = portfolioRes.value?.text ? await portfolioRes.value.text() : 'N/A';
97
+ logger.log('WARN', `[UPDATE] Portfolio fetch failed for ${userId}`, { error: errMsg, proxyResponse: rawText });
154
98
  }
155
99
 
156
- // --- Process History Result ---
157
- if (historyResult.status === 'fulfilled' && historyResult.value.ok) {
158
- const historyData = await historyResult.value.json();
159
- wasHistorySuccess = true;
160
- // Add to history batch
161
- console.log('DEBUG', `[UPDATE] SUCCESS Adding history data for user ${userId} to batch.`);
162
- await batchManager.addToTradingHistoryBatch(userId, portfolioStorageBlockId, today, historyData, userType);
163
- } else {
164
- // --- FIX: ENHANCED LOGGING ---
165
- let errorMsg = 'Unknown fetch error';
166
- let rawErrorText = 'N/A';
167
-
168
- if (historyResult.status === 'rejected') {
169
- errorMsg = historyResult.reason.message;
170
- } else if (historyResult.value) {
171
- errorMsg = historyResult.value.error?.message || `API status ${historyResult.value.status}`;
172
- // Get the raw text from the proxy manager's response
173
- if (typeof historyResult.value.text === 'function') {
174
- rawErrorText = await historyResult.value.text();
175
- }
100
+ // --- Process History (results[1], if it exists) ---
101
+ if (fetchHistory && historyRes) { // Check if we ran this promise
102
+ if (historyRes.status === 'fulfilled' && historyRes.value.ok) {
103
+ const data = await historyRes.value.json();
104
+ wasHistorySuccess = true;
105
+ await batchManager.addToTradingHistoryBatch(userId, portfolioBlockId, today, data, userType);
106
+ } else {
107
+ // History fetch failed
108
+ let errMsg = historyRes.status === 'rejected' ? historyRes.reason.message : `API status ${historyRes.value.status}`;
109
+ let rawText = historyRes.value?.text ? await historyRes.value.text() : 'N/A';
110
+ logger.log('WARN', `[UPDATE] History fetch failed for ${userId} (${username})`, { error: errMsg, proxyResponse: rawText });
176
111
  }
177
-
178
- logger.log('WARN', `[UPDATE] Failed to fetch HISTORY for user ${userId} (${username}).`, {
179
- error: errorMsg,
180
- proxyResponse: rawErrorText // This will contain "urlfetch" or other quota messages
181
- });
182
- // --- END FIX ---
183
112
  }
113
+ // --- END MODIFICATION ---
184
114
 
185
- // --- Handle Private User (if detected) ---
186
- if (isPrivate) {
187
- logger.log('WARN', `User ${userId} is private. Removing from future updates and decrementing block count.`);
188
- batchManager.deleteFromTimestampBatch(userId, userType, instrumentId);
189
115
 
190
- let blockId;
191
- let incrementField;
116
+ // --- Private user handling ---
117
+ if (isPrivate) {
118
+ logger.log('WARN', `User ${userId} is private. Removing from updates.`);
119
+ await batchManager.deleteFromTimestampBatch(userId, userType, instrumentId);
192
120
 
193
- if (userType === 'speculator') {
194
- blockId = String(Math.floor(parseInt(userId) / 1000000) * 1000000);
195
- incrementField = `counts.${instrumentId}_${blockId}`;
196
- } else {
197
- blockId = String(Math.floor(parseInt(userId) / 1000000) * 1000000);
198
- incrementField = `counts.${blockId}`;
199
- }
200
-
201
- const blockCountsRef = db.doc(userType === 'speculator'
202
- ? config.FIRESTORE_DOC_SPECULATOR_BLOCK_COUNTS
203
- : config.FIRESTORE_DOC_BLOCK_COUNTS);
204
-
205
- // This is a single, immediate write, which is fine for this rare case.
206
- // We use .set here because batchManager.flushBatches() might not run if this is the only user.
207
- // To be safer, this should also be added to a batch, but this is an edge case.
121
+ const blockCountsRef = db.doc(userType === 'speculator' ? config.FIRESTORE_DOC_SPECULATOR_BLOCK_COUNTS : config.FIRESTORE_DOC_BLOCK_COUNTS);
122
+ const incrementField = userType === 'speculator' ? `counts.${instrumentId}_${Math.floor(userId/1e6)*1e6}` : `counts.${Math.floor(userId/1e6)*1e6}`;
208
123
  await blockCountsRef.set({ [incrementField]: FieldValue.increment(-1) }, { merge: true });
209
124
  return;
210
125
  }
211
126
 
212
- // --- If either fetch was successful, update timestamps ---
213
127
  if (wasPortfolioSuccess || wasHistorySuccess) {
214
- // This call is correct for 'normal' users.
215
128
  await batchManager.updateUserTimestamp(userId, userType, instrumentId);
216
-
217
- // This is the fix for speculator timestamps
218
- if (userType === 'speculator') {
219
- const orchestratorBlockId = String(Math.floor(parseInt(userId) / 1000000) * 1000000);
220
- logger.log('INFO', `[UPDATE] Applying speculator timestamp fix for user ${userId} in block ${orchestratorBlockId}`);
221
- await batchManager.addSpeculatorTimestampFix(userId, orchestratorBlockId);
222
- }
129
+ if (userType === 'speculator') await batchManager.addSpeculatorTimestampFix(userId, String(Math.floor(userId/1e6)*1e6));
223
130
  }
224
-
225
131
  } finally {
226
- // Update performance for both headers used
227
132
  if (portfolioHeader) headerManager.updatePerformance(portfolioHeader.id, wasPortfolioSuccess);
228
- if (historyHeader) headerManager.updatePerformance(historyHeader.id, wasHistorySuccess);
133
+
134
+ // --- MODIFICATION: Only update history header if we used it ---
135
+ if (fetchHistory && historyHeader) headerManager.updatePerformance(historyHeader.id, wasHistorySuccess);
136
+ // --- END MODIFICATION ---
229
137
  }
230
138
  }
231
139
 
232
- module.exports = {
233
- handleUpdate,
234
- lookupUsernames // <-- EXPORT NEW HELPER
235
- };
140
+ module.exports = { handleUpdate, lookupUsernames };
@@ -1,188 +1,72 @@
1
- /**
2
- * @fileoverview Sub-pipe: pipe.taskEngine.handleVerify
3
- * REFACTORED: Now stateless and receives dependencies.
4
- * OPTIMIZED: Fetches all user portfolios in parallel to reduce function runtime.
5
- * --- MODIFIED: Saves username to cid_username_map collection. ---
6
- */
7
1
  const { FieldValue } = require('@google-cloud/firestore');
8
2
 
9
- /**
10
- * Internal helper to fetch and process a single user's portfolio.
11
- * @param {object} user - The user object { cid, isBronze, username }
12
- * @param {object} dependencies - Contains proxyManager, headerManager
13
- * @param {object} config - The configuration object
14
- * @param {Set<number>} speculatorInstrumentSet - Pre-built Set of instrument IDs
15
- * @returns {Promise<object|null>} A result object for batching, or null on failure.
16
- */
17
- async function fetchAndVerifyUser(user, dependencies, config, speculatorInstrumentSet) {
18
- const { logger, headerManager, proxyManager } = dependencies;
19
- const userId = user.cid;
20
-
21
- const portfolioUrl = `${config.ETORO_API_PORTFOLIO_URL}?cid=${userId}`;
3
+ async function fetchAndVerifyUser(user, { logger, headerManager, proxyManager }, { userType, SPECULATOR_INSTRUMENTS_ARRAY }) {
22
4
  const selectedHeader = await headerManager.selectHeader();
23
- if (!selectedHeader) return null; // Cannot fetch
5
+ if (!selectedHeader) return null;
24
6
 
25
7
  let wasSuccess = false;
26
8
  try {
27
- const response = await proxyManager.fetch(portfolioUrl, { headers: selectedHeader.header });
28
- if (!response.ok) {
29
- wasSuccess = false;
30
- return null; // API error or private user
31
- }
32
- wasSuccess = true;
33
-
34
- const portfolioData = await response.json();
35
-
36
- // Process verification logic *within* the parallel task
37
- if (config.userType === 'speculator') {
38
- const matchingInstruments = portfolioData.AggregatedPositions
39
- .map(p => p.InstrumentID)
40
- .filter(id => speculatorInstrumentSet.has(id));
9
+ const res = await proxyManager.fetch(`${process.env.ETORO_API_PORTFOLIO_URL}?cid=${user.cid}`, { headers: selectedHeader.header });
10
+ if (!res.ok) return null;
41
11
 
42
- if (matchingInstruments.length > 0) {
43
- logger.log('INFO', `[VERIFY] Speculator user ${userId} holds assets: ${matchingInstruments.join(', ')}`);
44
- // Return data needed for the speculator batch update
45
- return {
46
- type: 'speculator',
47
- userId: userId,
48
- isBronze: user.isBronze,
49
- username: user.username, // <-- PASS USERNAME
50
- updateData: {
51
- instruments: matchingInstruments,
52
- lastVerified: new Date(),
53
- lastHeldSpeculatorAsset: new Date()
54
- }
55
- };
56
- } else {
57
- logger.log('INFO', `[VERIFY] Speculator user ${userId} does not hold any speculator assets.`);
58
- return null; // Valid fetch, but failed verification
59
- }
60
- } else { // Normal user verification
61
- // Return data needed for the normal user batch update
62
- return {
63
- type: 'normal',
64
- userId: userId,
65
- isBronze: user.isBronze,
66
- username: user.username, // <-- PASS USERNAME
67
- updateData: {
68
- lastVerified: new Date()
69
- }
70
- };
12
+ wasSuccess = true;
13
+ const portfolioData = await res.json();
14
+ if (userType === 'speculator') {
15
+ const instruments = portfolioData.AggregatedPositions.map(p => p.InstrumentID)
16
+ .filter(id => SPECULATOR_INSTRUMENTS_ARRAY.includes(id));
17
+ if (!instruments.length) return null;
18
+ return { type: 'speculator', userId: user.cid, isBronze: user.isBronze, username: user.username, updateData: { instruments, lastVerified: new Date(), lastHeldSpeculatorAsset: new Date() } };
71
19
  }
72
- } catch (error) {
73
- logger.log('WARN', `[VERIFY] Error processing user ${userId}`, { errorMessage: error.message });
20
+ return { type: 'normal', userId: user.cid, isBronze: user.isBronze, username: user.username, updateData: { lastVerified: new Date() } };
21
+ } catch (err) {
22
+ logger.log('WARN', `[VERIFY] Error processing user ${user.cid}`, { errorMessage: err.message });
74
23
  return null;
75
24
  } finally {
76
25
  if (selectedHeader) headerManager.updatePerformance(selectedHeader.id, wasSuccess);
77
26
  }
78
27
  }
79
28
 
80
-
81
- /**
82
- * Sub-pipe: pipe.taskEngine.handleVerify
83
- * @param {object} task The Pub/Sub task payload.
84
- * @param {string} taskId A unique ID for logging.
85
- * @param {object} dependencies - Contains db, pubsub, logger, headerManager, proxyManager, batchManager.
86
- * @param {object} config The configuration object.
87
- */
88
- async function handleVerify(task, taskId, dependencies, config) {
89
- const { logger, db } = dependencies;
29
+ async function handleVerify(task, taskId, { db, logger, ...dependencies }, config) {
90
30
  const { users, blockId, instrument, userType } = task;
91
-
92
- // Use db from dependencies
93
31
  const batch = db.batch();
94
- let validUserCount = 0;
95
- const speculatorUpdates = {};
96
- const normalUserUpdates = {};
97
- const bronzeStateUpdates = {}; // This can be one object
98
-
99
- // --- NEW: Object to store username updates ---
100
- const usernameMapUpdates = {};
101
- // --- END NEW ---
102
-
103
- const speculatorInstrumentSet = new Set(config.SPECULATOR_INSTRUMENTS_ARRAY);
104
-
105
- // --- OPTIMIZATION: Run all user fetches in parallel ---
106
- const verificationPromises = users.map(user =>
107
- fetchAndVerifyUser(
108
- user,
109
- dependencies,
110
- { ...config, userType: userType }, // Pass userType into the helper config
111
- speculatorInstrumentSet
112
- )
113
- );
114
-
115
- const results = await Promise.allSettled(verificationPromises);
116
- // --- END OPTIMIZATION ---
117
-
118
- // Process results (this is fast, no I/O)
119
- results.forEach(result => {
120
- if (result.status === 'rejected' || !result.value) {
121
- // Log rejection reason if needed: result.reason
122
- return;
123
- }
32
+ const speculatorUpdates = {}, normalUpdates = {}, bronzeStates = {}, usernameMap = {};
33
+ const specSet = new Set(config.SPECULATOR_INSTRUMENTS_ARRAY);
124
34
 
125
- const data = result.value;
126
-
127
- // --- NEW: Capture username ---
128
- if (data.username) {
129
- // We use the CID as the key for the username map
130
- usernameMapUpdates[data.userId] = { username: data.username };
131
- }
132
- // --- END NEW ---
35
+ const results = await Promise.allSettled(users.map(u => fetchAndVerifyUser(u, dependencies, { ...config, userType })));
133
36
 
134
- if (data.type === 'speculator') {
135
- speculatorUpdates[`users.${data.userId}`] = data.updateData;
136
- bronzeStateUpdates[data.userId] = data.isBronze;
137
- validUserCount++;
138
- } else if (data.type === 'normal') {
139
- normalUserUpdates[`users.${data.userId}`] = data.updateData;
140
- bronzeStateUpdates[data.userId] = data.isBronze;
37
+ let validUserCount = 0;
38
+ results.forEach(r => {
39
+ if (r.status === 'fulfilled' && r.value) {
40
+ const d = r.value;
41
+ usernameMap[d.userId] = { username: d.username };
42
+ bronzeStates[d.userId] = d.isBronze;
141
43
  validUserCount++;
44
+ if (d.type === 'speculator') speculatorUpdates[`users.${d.userId}`] = d.updateData;
45
+ else normalUpdates[`users.${d.userId}`] = d.updateData;
142
46
  }
143
47
  });
144
48
 
49
+ if (Object.keys(speculatorUpdates).length || Object.keys(normalUpdates).length) {
50
+ const blockRef = db.collection(userType === 'speculator' ? config.FIRESTORE_COLLECTION_SPECULATOR_BLOCKS : config.FIRESTORE_COLLECTION_NORMAL_PORTFOLIOS).doc(String(blockId));
51
+ batch.set(blockRef, userType === 'speculator' ? speculatorUpdates : normalUpdates, { merge: true });
52
+ const bronzeRef = db.collection(userType === 'speculator' ? config.FIRESTORE_COLLECTION_BRONZE_SPECULATORS : config.FIRESTORE_COLLECTION_BRONZE_NORMAL).doc(String(blockId));
53
+ batch.set(bronzeRef, bronzeStates, { merge: true });
145
54
 
146
- if (Object.keys(speculatorUpdates).length > 0 || Object.keys(normalUserUpdates).length > 0) {
147
- if (userType === 'speculator') {
148
- const speculatorBlockRef = db.collection(config.FIRESTORE_COLLECTION_SPECULATOR_BLOCKS).doc(String(blockId));
149
- batch.set(speculatorBlockRef, speculatorUpdates, { merge: true });
150
- const bronzeStateRef = db.collection(config.FIRESTORE_COLLECTION_BRONZE_SPECULATORS).doc(String(blockId));
151
- batch.set(bronzeStateRef, bronzeStateUpdates, {merge: true});
152
-
153
- } else {
154
- const normalBlockRef = db.collection(config.FIRESTORE_COLLECTION_NORMAL_PORTFOLIOS).doc(String(blockId));
155
- batch.set(normalBlockRef, normalUserUpdates, { merge: true });
156
- const bronzeStateRef = db.collection(config.FIRESTORE_COLLECTION_BRONZE_NORMAL).doc(String(blockId));
157
- batch.set(bronzeStateRef, bronzeStateUpdates, {merge: true});
158
- }
159
-
160
- if (validUserCount > 0) {
161
- const blockCountsRef = db.doc(userType === 'speculator'
162
- ? config.FIRESTORE_DOC_SPECULATOR_BLOCK_COUNTS
163
- : config.FIRESTORE_DOC_BLOCK_COUNTS);
164
-
165
- const incrementField = userType === 'speculator' ? `counts.${instrument}_${blockId}` : `counts.${blockId}`;
166
- batch.set(blockCountsRef, { [incrementField]: FieldValue.increment(validUserCount) }, { merge: true });
55
+ if (validUserCount) {
56
+ const countsRef = db.doc(userType === 'speculator' ? config.FIRESTORE_DOC_SPECULATOR_BLOCK_COUNTS : config.FIRESTORE_DOC_BLOCK_COUNTS);
57
+ const field = userType === 'speculator' ? `counts.${instrument}_${blockId}` : `counts.${blockId}`;
58
+ batch.set(countsRef, { [field]: FieldValue.increment(validUserCount) }, { merge: true });
167
59
  }
168
60
  }
169
-
170
- // --- NEW: Add username map updates to the batch ---
171
- // We will use a simple sharding strategy (e.g., by first letter of CID)
172
- // For simplicity here, we'll write to one document.
173
- // A better production strategy would shard.
174
- if (Object.keys(usernameMapUpdates).length > 0) {
175
- // Simple, non-scalable approach:
176
- const mapDocRef = db.collection(config.FIRESTORE_COLLECTION_USERNAME_MAP).doc('cid_map_shard_1');
177
- batch.set(mapDocRef, usernameMapUpdates, { merge: true });
178
- logger.log('INFO', `[VERIFY] Staging ${Object.keys(usernameMapUpdates).length} username updates.`);
61
+
62
+ if (Object.keys(usernameMap).length) {
63
+ const mapRef = db.collection(config.FIRESTORE_COLLECTION_USERNAME_MAP).doc('cid_map_shard_1');
64
+ batch.set(mapRef, usernameMap, { merge: true });
65
+ logger.log('INFO', `[VERIFY] Staging ${Object.keys(usernameMap).length} username updates.`);
179
66
  }
180
- // --- END NEW ---
181
-
67
+
182
68
  await batch.commit();
183
- if(validUserCount > 0) {
184
- logger.log('INFO', `[VERIFY] Verified and stored ${validUserCount} new ${userType} users.`);
185
- }
69
+ if (validUserCount) logger.log('INFO', `[VERIFY] Verified and stored ${validUserCount} new ${userType} users.`);
186
70
  }
187
71
 
188
- module.exports = { handleVerify };
72
+ module.exports = { handleVerify };