bulltrackers-module 1.0.176 → 1.0.178

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.
@@ -147,28 +147,17 @@ class ComputationExecutor {
147
147
  */
148
148
  async executePerUser(calcInstance, metadata, dateStr, portfolioData, yesterdayPortfolioData, historyData, computedDeps, prevDeps) {
149
149
  const { logger } = this.deps;
150
-
151
- // Fix for the 'all' userType discrepancy:
152
- const targetUserType = metadata.userType; // 'all', 'normal', or 'speculator'
153
-
150
+ const targetUserType = metadata.userType;
154
151
  const mappings = await this.loader.loadMappings();
155
152
  const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await this.loader.loadInsights(dateStr) } : null;
156
-
157
153
  for (const [userId, todayPortfolio] of Object.entries(portfolioData)) {
158
- // 1. Get Yesterday's Portfolio (if available)
159
154
  const yesterdayPortfolio = yesterdayPortfolioData ? yesterdayPortfolioData[userId] : null;
160
-
161
- // 2. Get Today's Trading History (if available)
162
155
  const todayHistory = historyData ? historyData[userId] : null;
163
-
164
156
  const actualUserType = todayPortfolio.PublicPositions ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
165
-
166
- // Filtering Logic
167
157
  if (targetUserType !== 'all') {
168
158
  const mappedTarget = (targetUserType === 'speculator') ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
169
159
  if (mappedTarget !== actualUserType) continue;
170
160
  }
171
-
172
161
  const context = ContextBuilder.buildPerUserContext({
173
162
  todayPortfolio, yesterdayPortfolio,
174
163
  todayHistory,
@@ -177,7 +166,6 @@ class ComputationExecutor {
177
166
  previousComputedDependencies: prevDeps,
178
167
  config: this.config, deps: this.deps
179
168
  });
180
-
181
169
  try { await calcInstance.process(context); }
182
170
  catch (e) { logger.log('WARN', `Calc ${metadata.name} failed for user ${userId}: ${e.message}`); }
183
171
  }
@@ -187,14 +175,12 @@ class ComputationExecutor {
187
175
  const mappings = await this.loader.loadMappings();
188
176
  const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await this.loader.loadInsights(dateStr) } : null;
189
177
  const social = metadata.rootDataDependencies?.includes('social') ? { today: await this.loader.loadSocial(dateStr) } : null;
190
-
191
178
  const context = ContextBuilder.buildMetaContext({
192
179
  dateStr, metadata, mappings, insights, socialData: social,
193
180
  computedDependencies: computedDeps,
194
181
  previousComputedDependencies: prevDeps,
195
182
  config: this.config, deps: this.deps
196
183
  });
197
-
198
184
  return await calcInstance.process(context);
199
185
  }
200
186
  }
@@ -11,15 +11,6 @@
11
11
  * It has removed all logic for deprecated folder structures (e.g., /historical/).
12
12
  */
13
13
 
14
- const fs = require('fs');
15
- //Hacky solution to force tmp writes TODO : This Tmp write is really dodgy, not ideal but works, consider less hacky solutions to writing to filesystem
16
- process.env.TMPDIR = '/tmp';
17
- process.env.TMP = '/tmp';
18
- process.env.TEMP = '/tmp';
19
- const os = require('os');
20
-
21
- const path = require('path');
22
-
23
14
  /* --------------------------------------------------
24
15
  * Pretty Console Helpers
25
16
  * -------------------------------------------------- */
@@ -54,7 +45,7 @@ function suggestClosest(name, candidates, n = 3) {
54
45
  return dp[m][n];
55
46
  };
56
47
  const scores = candidates.map(c => [c, levenshtein(name, c)]);
57
- scores.sort((a, b) => a[1] - b[1]);
48
+ scores.sort((a, b) => a[1] - b[1]);
58
49
  return scores.slice(0, n).map(s => s[0]);
59
50
  }
60
51
 
@@ -206,17 +197,20 @@ function buildManifest(productLinesToRun = [], calculations) {
206
197
  const sortedManifest = [];
207
198
  const queue = [];
208
199
  let maxPass = 0;
200
+
209
201
  for (const [name, degree] of filteredInDegree) { if (degree === 0) { queue.push(name); filteredManifestMap.get(name).pass = 1; maxPass = 1; } }
210
202
  queue.sort();
211
203
  while (queue.length) {
212
204
  const currentName = queue.shift();
213
205
  const currentEntry = filteredManifestMap.get(currentName);
214
206
  sortedManifest.push(currentEntry);
207
+
215
208
  for (const neighborName of (filteredReverseAdjacency.get(currentName) || [])) { const newDegree = filteredInDegree.get(neighborName) - 1; filteredInDegree.set(neighborName, newDegree);
216
209
  const neighborEntry = filteredManifestMap.get(neighborName);
217
210
  if (neighborEntry.pass <= currentEntry.pass) { neighborEntry.pass = currentEntry.pass + 1; if (neighborEntry.pass > maxPass) maxPass = neighborEntry.pass; }
218
211
  if (newDegree === 0) { queue.push(neighborName); } }
219
212
  queue.sort(); }
213
+
220
214
  if (sortedManifest.length !== filteredManifestMap.size) {
221
215
  log.divider('Circular Dependency Detected');
222
216
  const cycles = findCycles(filteredManifestMap, adjacency);
@@ -62,8 +62,6 @@ async function runComputationPass(config, dependencies, computationManifest) {
62
62
  // Process a single date and RETURN updates (do not write)
63
63
  const processDate = async (dateStr) => {
64
64
  const dateToProcess = new Date(dateStr + 'T00:00:00Z');
65
-
66
- // Filter using in-memory status
67
65
  const standardToRun = standardCalcs.filter(c => shouldRun(c, dateStr));
68
66
  const metaToRun = metaCalcs.filter(c => shouldRun(c, dateStr));
69
67
 
@@ -84,21 +82,17 @@ async function runComputationPass(config, dependencies, computationManifest) {
84
82
  try {
85
83
  const calcsRunning = [...finalStandardToRun, ...finalMetaToRun];
86
84
  const existingResults = await fetchExistingResults(dateStr, calcsRunning, computationManifest, config, dependencies, false);
87
-
88
- const prevDate = new Date(dateToProcess); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
89
- const prevDateStr = prevDate.toISOString().slice(0, 10);
85
+ const prevDate = new Date(dateToProcess); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
86
+ const prevDateStr = prevDate.toISOString().slice(0, 10);
90
87
  const previousResults = await fetchExistingResults(prevDateStr, calcsRunning, computationManifest, config, dependencies, true);
91
-
92
88
  if (finalStandardToRun.length) {
93
89
  const updates = await runStandardComputationPass(dateToProcess, finalStandardToRun, `Pass ${passToRun} (Std)`, config, dependencies, rootData, existingResults, previousResults, true); // skipStatusWrite=true
94
90
  Object.assign(dateUpdates, updates);
95
91
  }
96
-
97
92
  if (finalMetaToRun.length) {
98
93
  const updates = await runMetaComputationPass(dateToProcess, finalMetaToRun, `Pass ${passToRun} (Meta)`, config, dependencies, existingResults, previousResults, rootData, true); // skipStatusWrite=true
99
94
  Object.assign(dateUpdates, updates);
100
95
  }
101
-
102
96
  } catch (err) {
103
97
  logger.log('ERROR', `[PassRunner] FAILED Pass ${passToRun} for ${dateStr}`, { errorMessage: err.message });
104
98
  // Mark failures
@@ -3,6 +3,7 @@
3
3
  * (REFACTORED: Removed all concurrency from `handleUpdate` and `lookupUsernames`)
4
4
  * (REFACTORED: Added node-fetch fallback for all API calls)
5
5
  * (REFACTORED: Added verbose, user-centric logging for all operations)
6
+ * (FIXED: Resolved ReferenceError 'instId is not defined' in final timestamp loops)
6
7
  */
7
8
 
8
9
  const { FieldValue } = require('@google-cloud/firestore');
@@ -15,54 +16,83 @@ async function lookupUsernames(cids, { logger, headerManager, proxyManager }, co
15
16
  if (!cids?.length) return [];
16
17
  logger.log('INFO', `[lookupUsernames] Looking up usernames for ${cids.length} CIDs.`);
17
18
 
18
- // --- Set concurrency to 1 because appscript gets really fucked up with undocumented rate limits if we try spam it concurrently, a shame but that's life. DO NOT CHANGE THIS
19
+ // --- Set concurrency to 1 to prevent AppScript rate limits. DO NOT CHANGE THIS.
19
20
  const limit = pLimit(1);
20
21
  const { USERNAME_LOOKUP_BATCH_SIZE, ETORO_API_RANKINGS_URL } = config;
21
22
  const batches = [];
22
- for (let i = 0; i < cids.length; i += USERNAME_LOOKUP_BATCH_SIZE) { batches.push(cids.slice(i, i + USERNAME_LOOKUP_BATCH_SIZE).map(Number)); }
23
- const batchPromises = batches.map((batch, index) => limit(async () => { const batchId = `batch-${index + 1}`;
23
+ for (let i = 0; i < cids.length; i += USERNAME_LOOKUP_BATCH_SIZE) {
24
+ batches.push(cids.slice(i, i + USERNAME_LOOKUP_BATCH_SIZE).map(Number));
25
+ }
26
+
27
+ const batchPromises = batches.map((batch, index) => limit(async () => {
28
+ const batchId = `batch-${index + 1}`;
24
29
  logger.log('INFO', `[lookupUsernames/${batchId}] Processing batch of ${batch.length} CIDs...`);
25
30
  const header = await headerManager.selectHeader();
26
31
  if (!header) { logger.log('ERROR', `[lookupUsernames/${batchId}] Could not select a header.`); return null; }
32
+
27
33
  let wasSuccess = false;
28
34
  let proxyUsed = true;
29
35
  let response;
30
36
  const url = `${ETORO_API_RANKINGS_URL}?Period=LastTwoYears`;
31
37
  const options = { method: 'POST', headers: { ...header.header, 'Content-Type': 'application/json' }, body: JSON.stringify(batch) };
38
+
32
39
  try {
33
40
  logger.log('TRACE', `[lookupUsernames/${batchId}] Attempting fetch via AppScript proxy...`);
34
41
  response = await proxyManager.fetch(url, options);
35
42
  if (!response.ok) throw new Error(`AppScript proxy failed with status ${response.status}`);
36
- wasSuccess = true; // Yay we win
43
+ wasSuccess = true;
37
44
  logger.log('INFO', `[lookupUsernames/${batchId}] AppScript proxy fetch successful.`);
38
- } catch (proxyError) { logger.log('WARN', `[lookupUsernames/${batchId}] AppScript proxy fetch FAILED. Error: ${proxyError.message}. Attempting direct node-fetch fallback.`, { error: proxyError.message, source: 'AppScript' }); // SHIT we failed...
39
- proxyUsed = false; // Don't penalize header for proxy failure
40
- try { response = await fetch(url, options); // Ok let's try again with node, using GCP IP pools
41
- if (!response.ok) { const errorText = await response.text(); throw new Error(`Direct fetch failed with status ${response.status}. Response: ${errorText.substring(0, 200)}`); }
42
- logger.log('INFO', `[lookupUsernames/${batchId}] Direct node-fetch fallback successful.`); // Yay we win
43
- } catch (fallbackError) { logger.log('ERROR', `[lookupUsernames/${batchId}] Direct node-fetch fallback FAILED. Giving up on this batch.`, { error: fallbackError.message, source: 'eToro/Network' }); // SHIT, we failed here too
44
- return null; // Give up on this batch
45
+ } catch (proxyError) {
46
+ logger.log('WARN', `[lookupUsernames/${batchId}] AppScript proxy fetch FAILED. Error: ${proxyError.message}. Attempting direct node-fetch fallback.`, { error: proxyError.message, source: 'AppScript' });
47
+ proxyUsed = false;
48
+ try {
49
+ response = await fetch(url, options);
50
+ if (!response.ok) {
51
+ const errorText = await response.text();
52
+ throw new Error(`Direct fetch failed with status ${response.status}. Response: ${errorText.substring(0, 200)}`);
53
+ }
54
+ logger.log('INFO', `[lookupUsernames/${batchId}] Direct node-fetch fallback successful.`);
55
+ } catch (fallbackError) {
56
+ logger.log('ERROR', `[lookupUsernames/${batchId}] Direct node-fetch fallback FAILED. Giving up on this batch.`, { error: fallbackError.message, source: 'eToro/Network' });
57
+ return null;
45
58
  }
46
- } finally { if (proxyUsed) { headerManager.updatePerformance(header.id, wasSuccess); } } // If we used Appscript IP Pool and not GCP IP Pool, record performance
47
- try { const data = await response.json(); return data;
48
- } catch (parseError) { logger.log('ERROR', `[lookupUsernames/${batchId}] Failed to parse JSON response.`, { error: parseError.message }); return null; } }));
59
+ } finally {
60
+ if (proxyUsed) { headerManager.updatePerformance(header.id, wasSuccess); }
61
+ }
62
+
63
+ try {
64
+ const data = await response.json();
65
+ return data;
66
+ } catch (parseError) {
67
+ logger.log('ERROR', `[lookupUsernames/${batchId}] Failed to parse JSON response.`, { error: parseError.message });
68
+ return null;
69
+ }
70
+ }));
49
71
 
50
72
  const results = await Promise.allSettled(batchPromises);
51
- const allUsers = results .filter(r => r.status === 'fulfilled' && r.value && Array.isArray(r.value)) .flatMap(r => r.value);
73
+ const allUsers = results
74
+ .filter(r => r.status === 'fulfilled' && r.value && Array.isArray(r.value))
75
+ .flatMap(r => r.value);
76
+
52
77
  logger.log('INFO', `[lookupUsernames] Found ${allUsers.length} public users out of ${cids.length}.`);
53
78
  return allUsers;
54
79
  }
55
80
 
56
81
 
57
82
  /**
58
- * (REFACTORED: Fully sequential, verbose logging, node-fetch fallback)
83
+ * (REFACTORED: Fully sequential, verbose logging, node-fetch fallback, FIXED SCOPING)
59
84
  */
60
85
  async function handleUpdate(task, taskId, { logger, headerManager, proxyManager, db, batchManager }, config, username) {
61
86
  const { userId, instruments, instrumentId, userType } = task;
62
- const instrumentsToProcess = userType === 'speculator' ? (instruments || [instrumentId]) : [undefined];
87
+
88
+ // For normal users, we pass [undefined] so the loop runs exactly once.
89
+ // For speculators, we pass the list of instruments to fetch individually.
90
+ const instrumentsToProcess = userType === 'speculator' ? (instruments || [instrumentId]) : [undefined];
91
+
63
92
  const today = new Date().toISOString().slice(0, 10);
64
93
  const portfolioBlockId = `${Math.floor(parseInt(userId) / 1000000)}M`;
65
94
  let isPrivate = false;
95
+
66
96
  logger.log('INFO', `[handleUpdate/${userId}] Starting update task. Type: ${userType}. Instruments: ${instrumentsToProcess.join(', ')}`);
67
97
 
68
98
  // --- 1. Process History Fetch (Sequentially) ---
@@ -74,52 +104,70 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
74
104
  if (!batchManager.checkAndSetHistoryFetched(userId)) {
75
105
  logger.log('INFO', `[handleUpdate/${userId}] Attempting history fetch.`);
76
106
  historyHeader = await headerManager.selectHeader();
77
- if (!historyHeader) { logger.log('WARN', `[handleUpdate/${userId}] Could not select history header. Skipping history.`);
107
+ if (!historyHeader) {
108
+ logger.log('WARN', `[handleUpdate/${userId}] Could not select history header. Skipping history.`);
78
109
  } else {
79
110
  const historyUrl = `${config.ETORO_API_USERSTATS_URL}${username}/trades/oneYearAgo?CopyAsAsset=true`;
80
111
  const options = { headers: historyHeader.header };
81
112
  let response;
82
- try { logger.log('TRACE', `[handleUpdate/${userId}] Attempting history fetch via AppScript proxy...`);
113
+
114
+ try {
115
+ logger.log('TRACE', `[handleUpdate/${userId}] Attempting history fetch via AppScript proxy...`);
83
116
  response = await proxyManager.fetch(historyUrl, options);
84
- if (!response.ok) throw new Error(`AppScript proxy failed with status ${response.status}`); // SHIT we failed here
85
- wasHistorySuccess = true; // Appscript worked, we are very smart
86
-
117
+ if (!response.ok) throw new Error(`AppScript proxy failed with status ${response.status}`);
118
+ wasHistorySuccess = true;
87
119
  } catch (proxyError) {
88
- logger.log('WARN', `[handleUpdate/${userId}] History fetch via AppScript proxy FAILED. Error: ${proxyError.message}. Attempting direct node-fetch fallback.`, { error: proxyError.message, source: 'AppScript' }); // SHIT we failed here
120
+ logger.log('WARN', `[handleUpdate/${userId}] History fetch via AppScript proxy FAILED. Error: ${proxyError.message}. Attempting direct node-fetch fallback.`, { error: proxyError.message, source: 'AppScript' });
89
121
  proxyUsedForHistory = false;
90
- try { response = await fetch(historyUrl, options);
91
- if (!response.ok) { const errorText = await response.text();
92
- throw new Error(`Direct fetch failed with status ${response.status}. Response: ${errorText.substring(0, 200)}`); } // SHIT we failed here too
93
- wasHistorySuccess = true; // Fallback succeeded, we are so smart
94
-
95
- } catch (fallbackError) { logger.log('ERROR', `[handleUpdate/${userId}] History fetch direct fallback FAILED.`, { error: fallbackError.message, source: 'eToro/Network' }); // We are dumb, everything failed
96
- wasHistorySuccess = false; // Nope we are dumb....
122
+ try {
123
+ response = await fetch(historyUrl, options);
124
+ if (!response.ok) {
125
+ const errorText = await response.text();
126
+ throw new Error(`Direct fetch failed with status ${response.status}. Response: ${errorText.substring(0, 200)}`);
127
+ }
128
+ wasHistorySuccess = true;
129
+ } catch (fallbackError) {
130
+ logger.log('ERROR', `[handleUpdate/${userId}] History fetch direct fallback FAILED.`, { error: fallbackError.message, source: 'eToro/Network' });
131
+ wasHistorySuccess = false;
97
132
  }
98
133
  }
99
134
 
100
- if (wasHistorySuccess) { logger.log('INFO', `[handleUpdate/${userId}] History fetch successful.`); // Some method worked, we are very smart
135
+ if (wasHistorySuccess) {
136
+ logger.log('INFO', `[handleUpdate/${userId}] History fetch successful.`);
101
137
  const data = await response.json();
102
- await batchManager.addToTradingHistoryBatch(userId, portfolioBlockId, today, data, userType); }
138
+ await batchManager.addToTradingHistoryBatch(userId, portfolioBlockId, today, data, userType);
139
+ }
103
140
  }
104
- } else { logger.log('TRACE', `[handleUpdate/${userId}] History fetch skipped (already fetched by this instance).`); }
105
- } catch (err) { logger.log('ERROR', `[handleUpdate/${userId}] Unhandled error during history processing.`, { error: err.message }); wasHistorySuccess = false; // We fucked up.
106
- } finally { if (historyHeader && proxyUsedForHistory) { headerManager.updatePerformance(historyHeader.id, wasHistorySuccess); } } // If we used appscript proxy, record performance, otherwise fuck off.
141
+ } else {
142
+ logger.log('TRACE', `[handleUpdate/${userId}] History fetch skipped (already fetched by this instance).`);
143
+ }
144
+ } catch (err) {
145
+ logger.log('ERROR', `[handleUpdate/${userId}] Unhandled error during history processing.`, { error: err.message });
146
+ wasHistorySuccess = false;
147
+ } finally {
148
+ if (historyHeader && proxyUsedForHistory) { headerManager.updatePerformance(historyHeader.id, wasHistorySuccess); }
149
+ }
107
150
 
108
151
  // --- 2. Process Portfolio Fetches (Sequentially) ---
109
152
  logger.log('INFO', `[handleUpdate/${userId}] Starting ${instrumentsToProcess.length} sequential portfolio fetches.`);
110
153
 
111
- for (const instId of instrumentsToProcess) {
154
+ // Renamed loop variable to currentInstrumentId to prevent 'instId' reference errors later
155
+ for (const currentInstrumentId of instrumentsToProcess) {
112
156
  if (isPrivate) {
113
157
  logger.log('INFO', `[handleUpdate/${userId}] Skipping remaining instruments because user was marked as private.`);
114
158
  break;
115
159
  }
116
160
 
117
161
  const portfolioHeader = await headerManager.selectHeader();
118
- if (!portfolioHeader) { logger.log('ERROR', `[handleUpdate/${userId}] Could not select portfolio header for instId ${instId}. Skipping this instrument.`);
162
+ if (!portfolioHeader) {
163
+ logger.log('ERROR', `[handleUpdate/${userId}] Could not select portfolio header. Skipping instrument.`);
119
164
  continue;
120
165
  }
121
166
 
122
- const portfolioUrl = userType === 'speculator' ? `${config.ETORO_API_POSITIONS_URL}?cid=${userId}&InstrumentID=${instId}` : `${config.ETORO_API_PORTFOLIO_URL}?cid=${userId}`;
167
+ // Only append InstrumentID if it is defined (i.e., for speculators)
168
+ const portfolioUrl = userType === 'speculator' && currentInstrumentId
169
+ ? `${config.ETORO_API_POSITIONS_URL}?cid=${userId}&InstrumentID=${currentInstrumentId}`
170
+ : `${config.ETORO_API_PORTFOLIO_URL}?cid=${userId}`;
123
171
 
124
172
  const options = { headers: portfolioHeader.header };
125
173
  let response;
@@ -127,48 +175,54 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
127
175
  let proxyUsedForPortfolio = true;
128
176
 
129
177
  try {
130
- // --- REFACTOR 3: ADD FALLBACK ---
131
- logger.log('TRACE', `[handleUpdate/${userId}] Attempting portfolio fetch for instId ${instId} via AppScript proxy...`);
178
+ logger.log('TRACE', `[handleUpdate/${userId}] Attempting portfolio fetch via AppScript proxy...`);
132
179
  response = await proxyManager.fetch(portfolioUrl, options);
133
- if (!response.ok) throw new Error(`AppScript proxy failed with status ${response.status}`); // SHIT we failed here
134
- wasPortfolioSuccess = true; // Oh we are smart, worked first time.
180
+ if (!response.ok) throw new Error(`AppScript proxy failed with status ${response.status}`);
181
+ wasPortfolioSuccess = true;
135
182
 
136
- } catch (proxyError) { // try fallback with local node fetch using GCP IP Pools
137
- logger.log('WARN', `[handleUpdate/${userId}] Portfolio fetch for instId ${instId} via AppScript proxy FAILED. Error: ${proxyError.message}. Attempting direct node-fetch fallback.`, { error: proxyError.message, source: 'AppScript' });
138
- proxyUsedForPortfolio = false; // We are not using Appscript proxy here as fallback is GCP based, so false
183
+ } catch (proxyError) {
184
+ logger.log('WARN', `[handleUpdate/${userId}] Portfolio fetch via AppScript proxy FAILED. Error: ${proxyError.message}. Attempting direct node-fetch fallback.`, { error: proxyError.message, source: 'AppScript' });
185
+ proxyUsedForPortfolio = false;
139
186
 
140
187
  try {
141
- response = await fetch(portfolioUrl, options); // Direct node-fetch
188
+ response = await fetch(portfolioUrl, options);
142
189
  if (!response.ok) {
143
190
  const errorText = await response.text();
144
- throw new Error(`Direct fetch failed with status ${response.status}. Response: ${errorText.substring(0, 200)}`); // SHIT we failed here
191
+ throw new Error(`Direct fetch failed with status ${response.status}. Response: ${errorText.substring(0, 200)}`);
145
192
  }
146
- wasPortfolioSuccess = true; // Fallback succeeded we are so smart
147
-
193
+ wasPortfolioSuccess = true;
148
194
  } catch (fallbackError) {
149
- logger.log('ERROR', `[handleUpdate/${userId}] Portfolio fetch for instId ${instId} direct fallback FAILED.`, { error: fallbackError.message, source: 'eToro/Network' });
195
+ logger.log('ERROR', `[handleUpdate/${userId}] Portfolio fetch direct fallback FAILED.`, { error: fallbackError.message, source: 'eToro/Network' });
150
196
  wasPortfolioSuccess = false;
151
197
  }
152
198
  }
153
199
 
154
- // --- 4. Process Portfolio Result (with verbose, raw logging) ---
155
200
  if (wasPortfolioSuccess) {
156
201
  const body = await response.text();
157
- if (body.includes("user is PRIVATE")) { isPrivate = true; logger.log('WARN', `[handleUpdate/${userId}] User is PRIVATE. Marking for removal.`);
158
- break; // Stop processing more portfolios for this private user
202
+ if (body.includes("user is PRIVATE")) {
203
+ isPrivate = true;
204
+ logger.log('WARN', `[handleUpdate/${userId}] User is PRIVATE. Marking for removal.`);
205
+ break;
159
206
  }
160
207
 
161
208
  try {
162
209
  const portfolioJson = JSON.parse(body);
163
- await batchManager.addToPortfolioBatch(userId, portfolioBlockId, today, portfolioJson, userType, instId);
164
- if (userType === 'speculator') { logger.log('INFO', `[handleUpdate/${userId}] Successfully processed portfolio for instId ${instId}.`); // Only speculators have an instid, so this is conditional
165
- } else { logger.log('INFO', `[handleUpdate/${userId}] Successfully processed full portfolio (normal user).`); } // Normal users
210
+ // Pass currentInstrumentId to batchManager. For normal users it's undefined, which is fine.
211
+ await batchManager.addToPortfolioBatch(userId, portfolioBlockId, today, portfolioJson, userType, currentInstrumentId);
212
+
213
+ if (userType === 'speculator') {
214
+ logger.log('INFO', `[handleUpdate/${userId}] Successfully processed portfolio for instId ${currentInstrumentId}.`);
215
+ } else {
216
+ logger.log('INFO', `[handleUpdate/${userId}] Successfully processed full portfolio (normal user).`);
217
+ }
166
218
 
167
- } catch (parseError) { // Idk why this would happen, but if it does....log.
168
- wasPortfolioSuccess = false; // Mark as failure
169
- logger.log('ERROR', `[handleUpdate/${userId}] FAILED TO PARSE JSON RESPONSE. RAW BODY:`, { url: portfolioUrl, parseErrorMessage: parseError.message, rawResponseText: body }); // Return full response
219
+ } catch (parseError) {
220
+ wasPortfolioSuccess = false;
221
+ logger.log('ERROR', `[handleUpdate/${userId}] FAILED TO PARSE JSON RESPONSE. RAW BODY:`, { url: portfolioUrl, parseErrorMessage: parseError.message, rawResponseText: body });
170
222
  }
171
- } else { logger.log('WARN', `[handleUpdate/${userId}] Portfolio fetch failed for instId ${instId}. No response to process.`); }
223
+ } else {
224
+ logger.log('WARN', `[handleUpdate/${userId}] Portfolio fetch failed. No response to process.`);
225
+ }
172
226
 
173
227
  if (proxyUsedForPortfolio) { headerManager.updatePerformance(portfolioHeader.id, wasPortfolioSuccess); }
174
228
  }
@@ -176,21 +230,32 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
176
230
  // --- 5. Handle Private Users & Timestamps ---
177
231
  if (isPrivate) {
178
232
  logger.log('WARN', `[handleUpdate/${userId}] Removing private user from updates.`);
179
- for (const instrumentId of instrumentsToProcess) { await batchManager.deleteFromTimestampBatch(userId, userType, instId); }
233
+ // Iterate again using correct scoping
234
+ for (const currentInstrumentId of instrumentsToProcess) {
235
+ await batchManager.deleteFromTimestampBatch(userId, userType, currentInstrumentId);
236
+ }
237
+
180
238
  const blockCountsRef = db.doc(config.FIRESTORE_DOC_SPECULATOR_BLOCK_COUNTS);
181
- for (const instrumentId of instrumentsToProcess) {
182
- const incrementField = `counts.${instrumentId}_${Math.floor(userId/1e6)*1e6}`;
183
- await blockCountsRef.set({ [incrementField]: FieldValue.increment(-1) }, { merge: true });
239
+ for (const currentInstrumentId of instrumentsToProcess) {
240
+ // Only update counts if we have a valid instrument ID (speculators)
241
+ if (currentInstrumentId) {
242
+ const incrementField = `counts.${currentInstrumentId}_${Math.floor(userId/1e6)*1e6}`;
243
+ await blockCountsRef.set({ [incrementField]: FieldValue.increment(-1) }, { merge: true });
244
+ }
184
245
  }
185
246
  return;
186
247
  }
187
248
 
188
249
  // If not private, update all timestamps
189
- for (const instrumentId of instrumentsToProcess) { await batchManager.updateUserTimestamp(userId, userType, instId); }
190
- if (userType === 'speculator') { await batchManager.addSpeculatorTimestampFix(userId, String(Math.floor(userId/1e6)*1e6)); }
250
+ for (const currentInstrumentId of instrumentsToProcess) {
251
+ await batchManager.updateUserTimestamp(userId, userType, currentInstrumentId);
252
+ }
253
+
254
+ if (userType === 'speculator') {
255
+ await batchManager.addSpeculatorTimestampFix(userId, String(Math.floor(userId/1e6)*1e6));
256
+ }
191
257
 
192
258
  logger.log('INFO', `[handleUpdate/${userId}] Update task finished successfully.`);
193
- // 'finally' block for header flushing is handled by the main handler_creator.js
194
259
  }
195
260
 
196
261
  module.exports = { handleUpdate, lookupUsernames };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.176",
3
+ "version": "1.0.178",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [