bulltrackers-module 1.0.360 → 1.0.362

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.
@@ -257,6 +257,10 @@ async function getPopularInvestorsToUpdate(dependencies, config) {
257
257
  username: item.UserName
258
258
  }));
259
259
 
260
+ if (targets.length > 0) {
261
+ logger.log('INFO', `[Core Utils Debug] First PI Target Mapped: ${JSON.stringify(targets[0])}`);
262
+ }
263
+
260
264
  logger.log('INFO', `[Core Utils] Found ${targets.length} Popular Investors from ${today} ranking.`);
261
265
  return targets;
262
266
 
@@ -2,13 +2,16 @@
2
2
  * @fileoverview Logic for handling Popular Investor and On-Demand User updates.
3
3
  * REFACTORED: Uses FirestoreBatchManager for sharded storage.
4
4
  * UPDATED: On-Demand User Update now fetches Trade History (1 Year).
5
+ * FIXED: Added client_request_id to all PI fetches to prevent 500 errors.
6
+ * FIXED: Changed Deep Dive limit to Top 10 assets.
7
+ * DEBUG: Added verbose URL logging.
5
8
  */
6
9
 
7
10
  const crypto = require('crypto');
8
11
 
9
12
  /**
10
13
  * Handles the update task for a Popular Investor.
11
- * Fetches: Overall Portfolio, Top 20 Deep Positions, Trade History.
14
+ * Fetches: Overall Portfolio, Top 10 Deep Positions, Trade History.
12
15
  * Stores them via BatchManager to ensure sharding.
13
16
  * @param {object} taskData - Contains { cid, username }.
14
17
  * @param {object} config - Task Engine configuration.
@@ -29,13 +32,20 @@ async function handlePopularInvestorUpdate(taskData, config, dependencies) {
29
32
  const today = new Date().toISOString().split('T')[0];
30
33
  const blockId = '19M';
31
34
 
35
+ // Generate Request UUID (Critical for preventing 500s on PI endpoints)
36
+ const uuid = crypto.randomUUID ? crypto.randomUUID() : `req-${Date.now()}-${Math.floor(Math.random()*10000)}`;
37
+
32
38
  try {
33
- // 1. Fetch Overall Portfolio
34
- const portfolioUrl = `${ETORO_API_PORTFOLIO_URL}?cid=${cid}`;
39
+ // --- 1. Fetch Overall Portfolio ---
40
+ const portfolioUrl = `${ETORO_API_PORTFOLIO_URL}?cid=${cid}&client_request_id=${uuid}`;
41
+
42
+ // [DEBUG] Log the EXACT URL being used
43
+ logger.log('INFO', `[PI DEBUG] Fetching Portfolio for ${username} (${cid}) URL: ${portfolioUrl}`);
44
+
35
45
  const portfolioRes = await proxyManager.fetch(portfolioUrl, { method: 'GET' });
36
46
 
37
47
  if (!portfolioRes.ok) {
38
- throw new Error(`Failed to fetch portfolio for ${cid}: ${portfolioRes.status}`);
48
+ throw new Error(`Failed to fetch portfolio for ${cid}. Status: ${portfolioRes.status} URL: ${portfolioUrl}`);
39
49
  }
40
50
  const portfolioData = await portfolioRes.json();
41
51
 
@@ -46,22 +56,28 @@ async function handlePopularInvestorUpdate(taskData, config, dependencies) {
46
56
  // Batch Write (Overall)
47
57
  await batchManager.addToPortfolioBatch(String(cid), blockId, today, portfolioData, 'popular_investor');
48
58
 
49
- // 2. Identify Top Assets for Deep Dive
59
+ // --- 2. Identify Top Assets for Deep Dive ---
50
60
  const aggregatedPositions = portfolioData.AggregatedPositions || [];
51
61
  const topPositions = aggregatedPositions
52
62
  .sort((a, b) => b.Invested - a.Invested)
53
- .slice(0, 20);
63
+ .slice(0, 10); // Limit to Top 10
54
64
 
55
65
  logger.log('INFO', `[PI Update] Fetching deep positions for top ${topPositions.length} assets for ${username}`);
56
66
 
57
67
  const deepPositions = [];
58
68
  for (const pos of topPositions) {
59
- const posUrl = `${ETORO_API_POSITIONS_URL}?cid=${cid}&InstrumentID=${pos.InstrumentID}`;
69
+ const posUrl = `${ETORO_API_POSITIONS_URL}?cid=${cid}&InstrumentID=${pos.InstrumentID}&client_request_id=${uuid}`;
70
+
71
+ // [DEBUG] Log the EXACT Deep Dive URL
72
+ logger.log('INFO', `[PI DEBUG] Fetching Position for ${username} InstID: ${pos.InstrumentID} URL: ${posUrl}`);
73
+
60
74
  try {
61
75
  const deepRes = await proxyManager.fetch(posUrl, { method: 'GET' });
62
76
  if (deepRes.ok) {
63
77
  const deepData = await deepRes.json();
64
78
  deepPositions.push({ instrumentId: pos.InstrumentID, ...deepData });
79
+ } else {
80
+ logger.log('WARN', `[PI DEBUG] Failed deep fetch. Status: ${deepRes.status} URL: ${posUrl}`);
65
81
  }
66
82
  } catch (err) {
67
83
  logger.log('WARN', `[PI Update] Failed deep fetch for inst ${pos.InstrumentID} user ${cid}`, err);
@@ -73,21 +89,23 @@ async function handlePopularInvestorUpdate(taskData, config, dependencies) {
73
89
  await batchManager.addToPortfolioBatch(String(cid), blockId, today, deepPayload, 'popular_investor_deep');
74
90
  }
75
91
 
76
- // 3. Fetch Trade History (Last 1 Year) with Pagination
77
- // [FIX] Implemented Pagination
92
+ // --- 3. Fetch Trade History (Last 1 Year) with Pagination ---
78
93
  const oneYearAgo = new Date();
79
94
  oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
80
- const uuid = crypto.randomUUID ? crypto.randomUUID() : '0205aca7-bd37-4884-8455-f28ce1add2de';
95
+
81
96
  const historyBaseUrl = ETORO_API_HISTORY_URL || 'https://www.etoro.com/sapi/trade-data-real/history/public/credit/flat';
82
97
 
83
98
  let allHistoryTrades = [];
84
99
  let page = 1;
85
100
  let hasMore = true;
86
- const MAX_PAGES = 10; // Safety limit (10k trades)
101
+ const MAX_PAGES = 10;
87
102
 
88
103
  while (hasMore && page <= MAX_PAGES) {
89
104
  const historyUrl = `${historyBaseUrl}?StartTime=${oneYearAgo.toISOString()}&PageNumber=${page}&ItemsPerPage=1000&PublicHistoryPortfolioFilter=&CID=${cid}&client_request_id=${uuid}`;
90
105
 
106
+ // [DEBUG] Log the EXACT History URL
107
+ logger.log('INFO', `[PI DEBUG] Fetching History Page ${page} for ${username} URL: ${historyUrl}`);
108
+
91
109
  try {
92
110
  const historyRes = await proxyManager.fetch(historyUrl, { method: 'GET' });
93
111
  if (historyRes.ok) {
@@ -97,12 +115,12 @@ async function handlePopularInvestorUpdate(taskData, config, dependencies) {
97
115
  if (trades.length > 0) {
98
116
  allHistoryTrades = allHistoryTrades.concat(trades);
99
117
  page++;
100
- hasMore = (trades.length === 1000); // If full page, assumes more
118
+ hasMore = (trades.length === 1000);
101
119
  } else {
102
120
  hasMore = false;
103
121
  }
104
122
  } else {
105
- logger.log('WARN', `[PI Update] History fetch failed on page ${page} for ${username}`);
123
+ logger.log('WARN', `[PI Update] History fetch failed on page ${page} for ${username}. Status: ${historyRes.status}`);
106
124
  hasMore = false;
107
125
  }
108
126
  } catch (e) {
@@ -130,8 +148,6 @@ async function handlePopularInvestorUpdate(taskData, config, dependencies) {
130
148
 
131
149
  /**
132
150
  * Handles the On-Demand update for a Signed-In User.
133
- * Fetches: Overall Portfolio AND Trade History.
134
- * Triggered by User Login/Verification.
135
151
  */
136
152
  async function handleOnDemandUserUpdate(taskData, config, dependencies) {
137
153
  const { cid, username } = taskData;
@@ -141,11 +157,15 @@ async function handleOnDemandUserUpdate(taskData, config, dependencies) {
141
157
  logger.log('INFO', `[On-Demand Update] Fetching Portfolio & History for Signed-In User: ${username} (${cid})`);
142
158
 
143
159
  const today = new Date().toISOString().split('T')[0];
144
- const blockId = '19M'; // Standard block for consistent indexing
160
+ const blockId = '19M';
161
+ const uuid = crypto.randomUUID ? crypto.randomUUID() : `req-${Date.now()}-${Math.floor(Math.random()*10000)}`;
145
162
 
146
163
  try {
147
- // --- 1. Fetch Portfolio ---
148
- const portfolioUrl = `${ETORO_API_PORTFOLIO_URL}?cid=${cid}`;
164
+ const portfolioUrl = `${ETORO_API_PORTFOLIO_URL}?cid=${cid}&client_request_id=${uuid}`;
165
+
166
+ // [DEBUG] Log URL for On-Demand
167
+ logger.log('INFO', `[On-Demand DEBUG] Fetching Portfolio URL: ${portfolioUrl}`);
168
+
149
169
  const portfolioRes = await proxyManager.fetch(portfolioUrl, { method: 'GET' });
150
170
 
151
171
  if (!portfolioRes.ok) {
@@ -156,43 +176,34 @@ async function handleOnDemandUserUpdate(taskData, config, dependencies) {
156
176
  portfolioData.lastUpdated = new Date();
157
177
  portfolioData.username = username;
158
178
 
159
- // Batch Write (Signed-In Portfolio)
160
179
  await batchManager.addToPortfolioBatch(String(cid), blockId, today, portfolioData, 'signed_in_user');
161
180
 
162
- // --- 2. Fetch Trade History (Last 1 Year) ---
163
- // This is critical for the new DAG requirements
181
+ // History Fetch
164
182
  const oneYearAgo = new Date();
165
183
  oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
166
-
167
- const uuid = crypto.randomUUID ? crypto.randomUUID() : '0205aca7-bd37-4884-8455-f28ce1add2de';
168
184
  const historyBaseUrl = ETORO_API_HISTORY_URL || 'https://www.etoro.com/sapi/trade-data-real/history/public/credit/flat';
169
185
  const historyUrl = `${historyBaseUrl}?StartTime=${oneYearAgo.toISOString()}&PageNumber=1&ItemsPerPage=1000&PublicHistoryPortfolioFilter=&CID=${cid}&client_request_id=${uuid}`;
170
186
 
187
+ logger.log('INFO', `[On-Demand DEBUG] Fetching History URL: ${historyUrl}`);
188
+
171
189
  const historyRes = await proxyManager.fetch(historyUrl, { method: 'GET' });
172
190
 
173
191
  if (historyRes.ok) {
174
192
  const historyData = await historyRes.json();
175
193
  historyData.fetchedAt = new Date();
176
194
 
177
- // [FIX] Apply History Filtering (remove CopyTrading noise)
178
- const VALID_REASONS = [0, 1, 5]; // 1=Close, 5=SL/TP etc. Remove 17/18 (Copy)
195
+ const VALID_REASONS = [0, 1, 5];
179
196
  if (historyData.PublicHistoryPositions) {
180
- const originalCount = historyData.PublicHistoryPositions.length;
181
197
  historyData.PublicHistoryPositions = historyData.PublicHistoryPositions.filter(
182
198
  p => VALID_REASONS.includes(p.CloseReason)
183
199
  );
184
- logger.log('INFO', `[On-Demand Update] Filtered history for ${username}: ${originalCount} -> ${historyData.PublicHistoryPositions.length}`);
185
200
  }
186
-
187
- // Batch Write (Signed-In History)
188
201
  await batchManager.addToTradingHistoryBatch(String(cid), blockId, today, historyData, 'signed_in_user');
189
-
190
202
  } else {
191
- logger.log('WARN', `[On-Demand Update] History fetch failed for ${username} (${historyRes.status}), proceeding with Portfolio only.`);
203
+ logger.log('WARN', `[On-Demand Update] History fetch failed for ${username} (${historyRes.status})`);
192
204
  }
193
205
 
194
206
  logger.log('SUCCESS', `[On-Demand Update] Complete for ${username}`);
195
-
196
207
  } catch (error) {
197
208
  logger.log('ERROR', `[On-Demand Update] Failed for ${username}`, error);
198
209
  throw error;
@@ -11,7 +11,9 @@
11
11
 
12
12
  const { handleDiscover } = require('../helpers/discover_helpers');
13
13
  const { handleVerify } = require('../helpers/verify_helpers');
14
- const { handleUpdate } = require('../helpers/update_helpers'); // Removed lookupUsernames import
14
+ const { handleUpdate } = require('../helpers/update_helpers');
15
+ // --- FIX: Import the handlers for the new task types ---
16
+ const { handlePopularInvestorUpdate, handleOnDemandUserUpdate } = require('../helpers/popular_investor_helpers');
15
17
  const pLimit = require('p-limit');
16
18
 
17
19
  /**
@@ -27,24 +29,21 @@ function parseTaskPayload(message, logger) {
27
29
  }
28
30
 
29
31
  /**
30
- * Sorts tasks into update and other (discover/verify).
31
- * REFACTORED: Simplified. No username lookup logic needed.
32
+ * Sorts tasks into update and other (discover/verify/PI/OnDemand).
32
33
  */
33
34
  async function prepareTaskBatches(tasks, batchManager, logger) {
34
35
  const tasksToRun = [], otherTasks = [];
35
36
 
36
37
  for (const task of tasks) {
37
38
  if (task.type === 'update') {
38
- // New API uses CID (userId), so we push directly to run.
39
+ // Standard portfolio updates (Normal/Speculator)
39
40
  tasksToRun.push(task);
40
41
  } else {
42
+ // Discover, Verify, Popular Investor, Signed-In User
41
43
  otherTasks.push(task);
42
44
  }
43
45
  }
44
46
 
45
- // We explicitly return empty structures for compatibility if handler_creator expects them,
46
- // though ideally handler_creator should also be simplified.
47
- // For now, we return compatible object structure.
48
47
  return { tasksToRun, cidsToLookup: new Map(), otherTasks };
49
48
  }
50
49
 
@@ -53,26 +52,53 @@ async function prepareTaskBatches(tasks, batchManager, logger) {
53
52
  * (REFACTORED: Concurrency limit set to 1)
54
53
  */
55
54
  async function executeTasks(tasksToRun, otherTasks, dependencies, config, taskId) {
56
- const { logger, batchManager } = dependencies;
55
+ const { logger } = dependencies;
57
56
 
58
- // --- REFACTOR 1: REMOVE CONCURRENCY ---
59
- // Set limit to 1 to serialize all tasks and prevent AppScript throttling.
60
57
  const limit = pLimit(1);
61
- // --- END REFACTOR 1 ---
62
58
 
63
59
  const allTaskPromises = [];
64
- let taskCounters = { update: 0, discover: 0, verify: 0, unknown: 0, failed: 0 };
60
+ let taskCounters = { update: 0, discover: 0, verify: 0, popular_investor: 0, on_demand: 0, unknown: 0, failed: 0 };
65
61
 
66
- // 1. Queue 'other' tasks (discover, verify)
62
+ // 1. Queue 'other' tasks
67
63
  for (const task of otherTasks) {
68
- const subTaskId = `${task.type}-${task.userType || 'unknown'}-${task.userId || task.cids?.[0] || 'sub'}`;
69
- const handler = { discover: handleDiscover, verify: handleVerify }[task.type];
64
+ const subTaskId = `${task.type}-${task.userType || 'unknown'}-${task.userId || task.cids?.[0] || task.cid || 'sub'}`;
65
+
66
+ // --- FIX START: Explicitly handle new task types ---
67
+ if (task.type === 'POPULAR_INVESTOR_UPDATE') {
68
+ allTaskPromises.push(limit(() =>
69
+ handlePopularInvestorUpdate(task, config, dependencies)
70
+ .then(() => taskCounters.popular_investor++)
71
+ .catch(err => {
72
+ logger.log('ERROR', `[TaskEngine/${taskId}] Error in POPULAR_INVESTOR_UPDATE for ${task.cid || task.username}`, { errorMessage: err.message });
73
+ taskCounters.failed++;
74
+ })
75
+ ));
76
+ continue;
77
+ }
78
+
79
+ if (task.type === 'ON_DEMAND_USER_UPDATE') {
80
+ allTaskPromises.push(limit(() =>
81
+ handleOnDemandUserUpdate(task, config, dependencies)
82
+ .then(() => taskCounters.on_demand++)
83
+ .catch(err => {
84
+ logger.log('ERROR', `[TaskEngine/${taskId}] Error in ON_DEMAND_USER_UPDATE for ${task.cid || task.username}`, { errorMessage: err.message });
85
+ taskCounters.failed++;
86
+ })
87
+ ));
88
+ continue;
89
+ }
90
+ // --- FIX END ---
91
+
92
+ // Handle legacy types (discover/verify)
93
+ // Normalize type to lowercase to handle potential mismatch if Orchestrator sends uppercase
94
+ const normalizedType = task.type.toLowerCase();
95
+ const handler = { discover: handleDiscover, verify: handleVerify }[normalizedType];
70
96
 
71
97
  if (handler) {
72
98
  allTaskPromises.push(
73
99
  limit(() =>
74
100
  handler(task, subTaskId, dependencies, config)
75
- .then(() => taskCounters[task.type]++)
101
+ .then(() => taskCounters[normalizedType]++)
76
102
  .catch(err => {
77
103
  logger.log('ERROR', `[TaskEngine/${taskId}] Error in ${task.type} for ${subTaskId}`, { errorMessage: err.message });
78
104
  taskCounters.failed++;
@@ -85,13 +111,8 @@ async function executeTasks(tasksToRun, otherTasks, dependencies, config, taskId
85
111
  }
86
112
  }
87
113
 
88
- // 2. Queue 'update' tasks
114
+ // 2. Queue 'update' tasks (Standard Normal/Speculator)
89
115
  for (const task of tasksToRun) {
90
- // We unpack 'task' directly now, no wrapping object {task, username}
91
- // However, we must ensure backward compatibility if the array was {task, username} before.
92
- // In prepareTaskBatches above, we pushed raw 'task'.
93
- // So we use 'task' directly.
94
-
95
116
  const subTaskId = `${task.type}-${task.userType || 'unknown'}-${task.userId}`;
96
117
  allTaskPromises.push(
97
118
  limit(() =>
@@ -105,15 +126,14 @@ async function executeTasks(tasksToRun, otherTasks, dependencies, config, taskId
105
126
  );
106
127
  }
107
128
 
108
- // 3. Wait for ALL tasks to complete (now sequentially)
129
+ // 3. Wait for ALL tasks to complete
109
130
  await Promise.all(allTaskPromises);
110
131
 
111
132
  // 4. Log final summary
112
133
  logger.log(
113
134
  taskCounters.failed > 0 ? 'WARN' : 'SUCCESS',
114
- `[TaskEngine/${taskId}] Processed all tasks. Updates: ${taskCounters.update}, Discovers: ${taskCounters.discover}, Verifies: ${taskCounters.verify}, Unknown: ${taskCounters.unknown}, Failed: ${taskCounters.failed}.`
135
+ `[TaskEngine/${taskId}] Processed all tasks. Updates: ${taskCounters.update}, Discovers: ${taskCounters.discover}, Verifies: ${taskCounters.verify}, PI: ${taskCounters.popular_investor}, OnDemand: ${taskCounters.on_demand}, Unknown: ${taskCounters.unknown}, Failed: ${taskCounters.failed}.`
115
136
  );
116
137
  }
117
138
 
118
- // Note: runUsernameLookups removed from exports
119
139
  module.exports = { parseTaskPayload, prepareTaskBatches, executeTasks };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.360",
3
+ "version": "1.0.362",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [