bulltrackers-module 1.0.451 → 1.0.453

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.
@@ -297,17 +297,47 @@ async function getPopularInvestorsToUpdate(dependencies, config) {
297
297
  const items = data.Items || [];
298
298
 
299
299
  // Map to { cid, username }
300
- const targets = items.map(item => ({
300
+ const allTargets = items.map(item => ({
301
301
  cid: String(item.CustomerId),
302
302
  username: item.UserName
303
303
  }));
304
304
 
305
- if (targets.length > 0) {
306
- logger.log('INFO', `[Core Utils Debug] First PI Target Mapped: ${JSON.stringify(targets[0])}`);
305
+ // Filter out PIs updated in the last 18 hours
306
+ const HOURS_THRESHOLD = 18;
307
+ const thresholdMs = HOURS_THRESHOLD * 60 * 60 * 1000;
308
+ const now = Date.now();
309
+
310
+ const filteredTargets = [];
311
+ let skippedCount = 0;
312
+
313
+ // Check each PI's last sync time
314
+ for (const target of allTargets) {
315
+ const syncRef = db.collection('user_sync_requests')
316
+ .doc(target.cid)
317
+ .collection('global')
318
+ .doc('latest');
319
+
320
+ const syncDoc = await syncRef.get();
321
+ if (syncDoc.exists) {
322
+ const syncData = syncDoc.data();
323
+ const lastRequestedAt = syncData.lastRequestedAt?.toDate?.()?.getTime() ||
324
+ syncData.lastRequestedAt?.toMillis?.() || null;
325
+
326
+ if (lastRequestedAt && (now - lastRequestedAt) < thresholdMs) {
327
+ skippedCount++;
328
+ continue; // Skip this PI
329
+ }
330
+ }
331
+
332
+ filteredTargets.push(target); // Include this PI
307
333
  }
308
334
 
309
- logger.log('INFO', `[Core Utils] Found ${targets.length} Popular Investors from ${rankingsDate} ranking${rankingsDate !== today ? ` (fallback from ${today})` : ''}.`);
310
- return targets;
335
+ if (filteredTargets.length > 0) {
336
+ logger.log('INFO', `[Core Utils Debug] First PI Target Mapped: ${JSON.stringify(filteredTargets[0])}`);
337
+ }
338
+
339
+ logger.log('INFO', `[Core Utils] Found ${allTargets.length} Popular Investors from ${rankingsDate} ranking${rankingsDate !== today ? ` (fallback from ${today})` : ''}. Skipped ${skippedCount} recently updated. ${filteredTargets.length} will be updated.`);
340
+ return filteredTargets;
311
341
 
312
342
  } catch (error) {
313
343
  logger.log('ERROR', '[Core Utils] Error getting Popular Investors', { errorMessage: error.message });
@@ -316,13 +346,19 @@ async function getPopularInvestorsToUpdate(dependencies, config) {
316
346
  }
317
347
 
318
348
  /**
319
- * [NEW] Fetches all Signed-In Users for daily update.
349
+ * [NEW] Fetches Signed-In Users for daily update.
350
+ * Skips users updated in the last 18 hours to avoid duplicate refreshes.
320
351
  */
321
352
  async function getSignedInUsersToUpdate(dependencies, config) {
322
353
  const { db, logger } = dependencies;
323
354
  const collectionName = config.signedInUsersCollection || process.env.FIRESTORE_COLLECTION_SIGNED_IN_USER_PORTFOLIOS || 'signed_in_users';
355
+
356
+ // 18 hours threshold - skip users updated more recently than this
357
+ const HOURS_THRESHOLD = 18;
358
+ const thresholdMs = HOURS_THRESHOLD * 60 * 60 * 1000;
359
+ const now = Date.now();
324
360
 
325
- logger.log('INFO', `[Core Utils] Getting Signed-In Users to update from ${collectionName}...`);
361
+ logger.log('INFO', `[Core Utils] Getting Signed-In Users to update from ${collectionName} (skipping users updated in last ${HOURS_THRESHOLD} hours)...`);
326
362
 
327
363
  try {
328
364
  const snapshot = await db.collection(collectionName).get();
@@ -333,17 +369,63 @@ async function getSignedInUsersToUpdate(dependencies, config) {
333
369
  }
334
370
 
335
371
  const targets = [];
372
+ let skippedCount = 0;
373
+
336
374
  snapshot.forEach(doc => {
337
375
  const data = doc.data();
338
- // Assuming the doc ID is the CID, or it's a field 'cid'
339
376
  const cid = data.cid || doc.id;
340
377
  const username = data.username || 'Unknown';
341
- // Only push if we have a valid CID
342
- if(cid) targets.push({ cid: String(cid), username });
378
+
379
+ if (!cid) return;
380
+
381
+ // Check if user was recently updated (check sync requests and portfolio timestamps)
382
+ // First check sync requests for last update time
383
+ let shouldSkip = false;
384
+
385
+ // Check user_sync_requests for last completed sync
386
+ // We'll check this asynchronously, but for now we'll check portfolio data timestamps
387
+ // The portfolio data is stored in snapshots with dates, so we can check the latest snapshot date
388
+ const portfolioCollection = collectionName; // Same collection
389
+ const blockId = '19M';
390
+ const today = new Date().toISOString().split('T')[0];
391
+
392
+ // For efficiency, we'll check the sync requests collection for the last update time
393
+ // This is a synchronous check we can do here
394
+ // Actually, we need to make this async, so we'll do a batch check after
395
+
396
+ // For now, include all users - we'll filter in the dispatcher
397
+ targets.push({ cid: String(cid), username });
398
+ });
399
+
400
+ // Now filter out users who were updated in the last 18 hours
401
+ // Check user_sync_requests for last completed sync
402
+ const filteredTargets = [];
403
+ const checkPromises = targets.map(async (target) => {
404
+ const syncRef = db.collection('user_sync_requests')
405
+ .doc(target.cid)
406
+ .collection('global')
407
+ .doc('latest');
408
+
409
+ const syncDoc = await syncRef.get();
410
+ if (syncDoc.exists) {
411
+ const syncData = syncDoc.data();
412
+ const lastRequestedAt = syncData.lastRequestedAt?.toDate?.()?.getTime() ||
413
+ syncData.lastRequestedAt?.toMillis?.() || null;
414
+
415
+ if (lastRequestedAt && (now - lastRequestedAt) < thresholdMs) {
416
+ skippedCount++;
417
+ return null; // Skip this user
418
+ }
419
+ }
420
+
421
+ return target; // Include this user
343
422
  });
423
+
424
+ const results = await Promise.all(checkPromises);
425
+ const finalTargets = results.filter(t => t !== null);
344
426
 
345
- logger.log('INFO', `[Core Utils] Found ${targets.length} Signed-In Users.`);
346
- return targets;
427
+ logger.log('INFO', `[Core Utils] Found ${targets.length} Signed-In Users. Skipped ${skippedCount} recently updated. ${finalTargets.length} will be updated.`);
428
+ return finalTargets;
347
429
 
348
430
  } catch (error) {
349
431
  logger.log('ERROR', '[Core Utils] Error getting Signed-In Users', { errorMessage: error.message });
@@ -0,0 +1,358 @@
1
+ /**
2
+ * @fileoverview User Sync Request Helpers
3
+ * Allows users to request on-demand sync for their own profile or Popular Investor profiles
4
+ * Rate limiting: 1 update per user per 6 hours (global, not per-requester)
5
+ * Developer accounts bypass rate limits
6
+ */
7
+
8
+ const { FieldValue } = require('@google-cloud/firestore');
9
+ const crypto = require('crypto');
10
+
11
+ const RATE_LIMIT_HOURS = 6;
12
+ const RATE_LIMIT_MS = RATE_LIMIT_HOURS * 60 * 60 * 1000;
13
+
14
+ /**
15
+ * Request on-demand sync for a user (signed-in user or Popular Investor)
16
+ * POST /user/:userCid/sync
17
+ */
18
+ async function requestUserSync(req, res, dependencies, config) {
19
+ const { db, logger, pubsub } = dependencies;
20
+ const { userCid: targetUserCid } = req.params;
21
+
22
+ // Get requesting userCid from query
23
+ const requestingUserCid = req.query?.userCid;
24
+
25
+ if (!requestingUserCid) {
26
+ return res.status(400).json({
27
+ success: false,
28
+ error: "Missing userCid",
29
+ message: "Please provide userCid as a query parameter"
30
+ });
31
+ }
32
+
33
+ // Check for dev override impersonation
34
+ const { getEffectiveCid, getDevOverride } = require('./dev_helpers');
35
+ const effectiveCid = await getEffectiveCid(db, requestingUserCid, config, logger);
36
+ const devOverride = await getDevOverride(db, requestingUserCid, config, logger);
37
+ const isImpersonating = devOverride && devOverride.enabled && devOverride.impersonateCid && effectiveCid !== Number(requestingUserCid);
38
+
39
+ const targetCidNum = Number(targetUserCid);
40
+ if (isNaN(targetCidNum) || targetCidNum <= 0) {
41
+ return res.status(400).json({
42
+ success: false,
43
+ error: "Invalid user CID",
44
+ message: "Please provide a valid user CID"
45
+ });
46
+ }
47
+
48
+ try {
49
+ // Check if this is a developer account (bypass rate limits for developers)
50
+ const { isDeveloperAccount } = require('./dev_helpers');
51
+ const isDeveloper = isDeveloperAccount(requestingUserCid);
52
+
53
+ let rateLimitCheck = { allowed: true }; // Default to allowed for developers
54
+
55
+ if (!isDeveloper) {
56
+ // Check global rate limit (applies to target user, not requester)
57
+ rateLimitCheck = await checkRateLimits(db, targetCidNum, logger);
58
+
59
+ if (!rateLimitCheck.allowed) {
60
+ return res.status(429).json({
61
+ success: false,
62
+ error: "Rate limit exceeded",
63
+ message: rateLimitCheck.message,
64
+ rateLimit: {
65
+ canRequestAgainAt: rateLimitCheck.canRequestAgainAt,
66
+ lastRequestedAt: rateLimitCheck.lastRequestedAt
67
+ }
68
+ });
69
+ }
70
+ } else {
71
+ logger.log('INFO', `[requestUserSync] Developer account ${requestingUserCid} bypassing rate limits${isImpersonating ? ` (impersonating ${effectiveCid})` : ''}`);
72
+ }
73
+
74
+ // Determine user type (PI or signed-in user)
75
+ // Check if user is in rankings (PI) or has signed-in user data
76
+ const { checkIfUserIsPI } = require('./data_helpers');
77
+ const isPI = await checkIfUserIsPI(db, targetCidNum, config, logger);
78
+
79
+ // Get username - for PIs, get from rankings; for signed-in users, we'll need to fetch from their profile
80
+ let username = null;
81
+ if (isPI) {
82
+ // Get PI username from rankings
83
+ const { findLatestRankingsDate } = require('./data_helpers');
84
+ const rankingsCollection = config.popularInvestorRankingsCollection || 'popular_investor_rankings';
85
+ const rankingsDate = await findLatestRankingsDate(db, rankingsCollection, 30);
86
+
87
+ if (rankingsDate) {
88
+ const rankingsRef = db.collection(rankingsCollection).doc(rankingsDate);
89
+ const rankingsDoc = await rankingsRef.get();
90
+ if (rankingsDoc.exists) {
91
+ const rankingsData = rankingsDoc.data();
92
+ const rankingsItems = rankingsData.Items || [];
93
+ const rankingEntry = rankingsItems.find(item => Number(item.CustomerId) === targetCidNum);
94
+ if (rankingEntry) {
95
+ username = rankingEntry.UserName || rankingEntry.username || null;
96
+ }
97
+ }
98
+ }
99
+ } else {
100
+ // For signed-in users, try to get username from verification data
101
+ const signedInUsersCollection = config.signedInUsersCollection || 'signed_in_users';
102
+ const userDoc = await db.collection(signedInUsersCollection).doc(String(targetCidNum)).get();
103
+ if (userDoc.exists) {
104
+ const userData = userDoc.data();
105
+ username = userData.username || userData.verification?.username || null;
106
+ }
107
+ }
108
+
109
+ if (!username) {
110
+ logger.log('WARN', `[requestUserSync] Could not find username for user ${targetCidNum}, will use CID as fallback`);
111
+ username = String(targetCidNum);
112
+ }
113
+
114
+ // Create request document
115
+ const requestId = `sync_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
116
+ const now = new Date();
117
+
118
+ // Store request
119
+ const requestRef = db.collection('user_sync_requests')
120
+ .doc(String(targetCidNum))
121
+ .collection('requests')
122
+ .doc(requestId);
123
+
124
+ await requestRef.set({
125
+ requestId,
126
+ targetUserCid: targetCidNum,
127
+ requestedBy: Number(requestingUserCid),
128
+ effectiveRequestedBy: effectiveCid,
129
+ username,
130
+ userType: isPI ? 'POPULAR_INVESTOR' : 'SIGNED_IN_USER',
131
+ status: 'queued',
132
+ isImpersonating: isImpersonating || false,
133
+ createdAt: FieldValue.serverTimestamp(),
134
+ updatedAt: FieldValue.serverTimestamp()
135
+ });
136
+
137
+ // Update global rate limit (for target user)
138
+ const globalRef = db.collection('user_sync_requests')
139
+ .doc(String(targetCidNum))
140
+ .collection('global')
141
+ .doc('latest');
142
+
143
+ await globalRef.set({
144
+ targetUserCid: targetCidNum,
145
+ lastRequestedAt: FieldValue.serverTimestamp(),
146
+ lastRequestedBy: Number(requestingUserCid),
147
+ totalRequests: FieldValue.increment(1),
148
+ updatedAt: FieldValue.serverTimestamp()
149
+ }, { merge: true });
150
+
151
+ // Publish to task engine
152
+ const topicName = config.taskEngine?.PUBSUB_TOPIC_USER_FETCH || 'etoro-user-fetch-topic';
153
+ const topic = pubsub.topic(topicName);
154
+
155
+ const message = {
156
+ type: isPI ? 'POPULAR_INVESTOR_UPDATE' : 'ON_DEMAND_USER_UPDATE',
157
+ cid: targetCidNum,
158
+ username: username,
159
+ priority: 'high',
160
+ source: 'on_demand_sync',
161
+ requestId,
162
+ requestedBy: Number(requestingUserCid),
163
+ effectiveRequestedBy: effectiveCid,
164
+ metadata: {
165
+ onDemand: true,
166
+ requestedAt: now.toISOString(),
167
+ isImpersonating: isImpersonating || false
168
+ }
169
+ };
170
+
171
+ const messageId = await topic.publishMessage({
172
+ data: Buffer.from(JSON.stringify(message))
173
+ });
174
+
175
+ // Update request with message ID
176
+ await requestRef.update({
177
+ taskMessageId: messageId,
178
+ status: 'dispatched',
179
+ dispatchedAt: FieldValue.serverTimestamp(),
180
+ updatedAt: FieldValue.serverTimestamp()
181
+ });
182
+
183
+ logger.log('INFO', `[requestUserSync] User ${requestingUserCid}${isImpersonating ? ` (impersonating ${effectiveCid})` : ''} requested sync for ${isPI ? 'PI' : 'signed-in user'} ${targetCidNum} (${username}). Request ID: ${requestId}`);
184
+
185
+ return res.status(200).json({
186
+ success: true,
187
+ requestId,
188
+ status: 'dispatched',
189
+ message: 'Sync request queued successfully. Data will be updated shortly.',
190
+ estimatedTime: '2-5 minutes',
191
+ rateLimit: {
192
+ canRequestAgainAt: new Date(Date.now() + RATE_LIMIT_MS).toISOString()
193
+ }
194
+ });
195
+
196
+ } catch (error) {
197
+ logger.log('ERROR', `[requestUserSync] Error requesting sync for user ${targetCidNum}`, error);
198
+ return res.status(500).json({
199
+ success: false,
200
+ error: "Internal server error",
201
+ message: error.message
202
+ });
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Get sync status for a user
208
+ * GET /user/:userCid/sync-status
209
+ */
210
+ async function getUserSyncStatus(req, res, dependencies, config) {
211
+ const { db, logger } = dependencies;
212
+ const { userCid: targetUserCid } = req.params;
213
+ const requestingUserCid = req.query?.userCid; // Optional - for rate limit info
214
+
215
+ const targetCidNum = Number(targetUserCid);
216
+ if (isNaN(targetCidNum) || targetCidNum <= 0) {
217
+ return res.status(400).json({
218
+ success: false,
219
+ error: "Invalid user CID"
220
+ });
221
+ }
222
+
223
+ try {
224
+ // Check for latest request
225
+ const requestsRef = db.collection('user_sync_requests')
226
+ .doc(String(targetCidNum))
227
+ .collection('requests')
228
+ .orderBy('createdAt', 'desc')
229
+ .limit(1);
230
+
231
+ const requestsSnapshot = await requestsRef.get();
232
+
233
+ if (!requestsSnapshot.empty) {
234
+ const latestRequest = requestsSnapshot.docs[0].data();
235
+ let status = latestRequest.status || 'queued';
236
+
237
+ const response = {
238
+ success: true,
239
+ status,
240
+ requestId: latestRequest.requestId,
241
+ startedAt: latestRequest.startedAt?.toDate?.()?.toISOString() || null,
242
+ createdAt: latestRequest.createdAt?.toDate?.()?.toISOString() || null,
243
+ dispatchedAt: latestRequest.dispatchedAt?.toDate?.()?.toISOString() || null,
244
+ completedAt: latestRequest.completedAt?.toDate?.()?.toISOString() || null,
245
+ estimatedCompletion: latestRequest.dispatchedAt
246
+ ? new Date(new Date(latestRequest.dispatchedAt.toDate()).getTime() + 5 * 60 * 1000).toISOString() // 5 min estimate
247
+ : null
248
+ };
249
+
250
+ // Include error details if status is failed
251
+ if (status === 'failed') {
252
+ response.error = latestRequest.error || 'Unknown error occurred';
253
+ response.failedAt = latestRequest.failedAt?.toDate?.()?.toISOString() || null;
254
+ }
255
+
256
+ // Include raw data status if processing
257
+ if (status === 'processing' || status === 'indexing' || status === 'computing') {
258
+ response.rawDataStoredAt = latestRequest.rawDataStoredAt?.toDate?.()?.toISOString() || null;
259
+ response.message = status === 'indexing' ? 'Raw data stored, indexing data...' :
260
+ status === 'computing' ? 'Raw data stored, computation in progress...' :
261
+ 'Processing data...';
262
+ }
263
+
264
+ return res.status(200).json(response);
265
+ }
266
+
267
+ // No request found, check if user can request
268
+ let canRequest = true;
269
+ let rateLimitInfo = null;
270
+
271
+ if (requestingUserCid) {
272
+ // Check if this is a developer account (bypass rate limits for developers)
273
+ const { isDeveloperAccount } = require('./dev_helpers');
274
+ const isDeveloper = isDeveloperAccount(requestingUserCid);
275
+
276
+ if (isDeveloper) {
277
+ // Developer accounts bypass rate limits
278
+ canRequest = true;
279
+ rateLimitInfo = {
280
+ bypassed: true,
281
+ reason: 'developer_account'
282
+ };
283
+ } else {
284
+ const rateLimitCheck = await checkRateLimits(db, targetCidNum, logger);
285
+ canRequest = rateLimitCheck.allowed;
286
+ rateLimitInfo = {
287
+ canRequestAgainAt: rateLimitCheck.canRequestAgainAt
288
+ };
289
+ }
290
+ }
291
+
292
+ return res.status(200).json({
293
+ success: true,
294
+ status: 'not_requested',
295
+ canRequest,
296
+ rateLimit: rateLimitInfo
297
+ });
298
+
299
+ } catch (error) {
300
+ logger.log('ERROR', `[getUserSyncStatus] Error checking sync status for user ${targetCidNum}`, error);
301
+ return res.status(500).json({
302
+ success: false,
303
+ error: "Internal server error",
304
+ message: error.message
305
+ });
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Check global rate limits for a user
311
+ * NOTE: Developer accounts should bypass this check (check isDeveloperAccount before calling)
312
+ */
313
+ async function checkRateLimits(db, targetUserCid, logger) {
314
+ const now = Date.now();
315
+
316
+ // Check global rate limit (applies to target user, not requester)
317
+ const globalRef = db.collection('user_sync_requests')
318
+ .doc(String(targetUserCid))
319
+ .collection('global')
320
+ .doc('latest');
321
+
322
+ const globalDoc = await globalRef.get();
323
+ let canRequestAgainAt = null;
324
+ let blocked = false;
325
+ let lastRequestedAt = null;
326
+
327
+ if (globalDoc.exists) {
328
+ const globalData = globalDoc.data();
329
+ const globalLastRequestedAt = globalData.lastRequestedAt?.toDate?.()?.getTime() || globalData.lastRequestedAt?.toMillis?.() || null;
330
+
331
+ if (globalLastRequestedAt && (now - globalLastRequestedAt) < RATE_LIMIT_MS) {
332
+ blocked = true;
333
+ canRequestAgainAt = new Date(globalLastRequestedAt + RATE_LIMIT_MS).toISOString();
334
+ lastRequestedAt = new Date(globalLastRequestedAt).toISOString();
335
+ }
336
+ }
337
+
338
+ if (blocked) {
339
+ const hoursRemaining = Math.ceil((new Date(canRequestAgainAt).getTime() - now) / (60 * 60 * 1000));
340
+ return {
341
+ allowed: false,
342
+ message: `This user was recently synced. Please try again in ${hoursRemaining} hour${hoursRemaining !== 1 ? 's' : ''}.`,
343
+ canRequestAgainAt,
344
+ lastRequestedAt
345
+ };
346
+ }
347
+
348
+ return {
349
+ allowed: true,
350
+ canRequestAgainAt: new Date(now + RATE_LIMIT_MS).toISOString()
351
+ };
352
+ }
353
+
354
+ module.exports = {
355
+ requestUserSync,
356
+ getUserSyncStatus
357
+ };
358
+
@@ -11,6 +11,7 @@ const { subscribeToAlerts, updateSubscription, unsubscribeFromAlerts, getUserSub
11
11
  const { setDevOverride, getDevOverrideStatus } = require('./helpers/dev_helpers');
12
12
  const { getAlertTypes, getUserAlerts, getAlertCount, markAlertRead, markAllAlertsRead, deleteAlert } = require('./helpers/alert_helpers');
13
13
  const { requestPiFetch, getPiFetchStatus } = require('./helpers/on_demand_fetch_helpers');
14
+ const { requestUserSync, getUserSyncStatus } = require('./helpers/user_sync_helpers');
14
15
 
15
16
  module.exports = (dependencies, config) => {
16
17
  const router = express.Router();
@@ -34,6 +35,10 @@ module.exports = (dependencies, config) => {
34
35
  router.post('/pi/:piCid/request-fetch', (req, res) => requestPiFetch(req, res, dependencies, config));
35
36
  router.get('/pi/:piCid/fetch-status', (req, res) => getPiFetchStatus(req, res, dependencies, config));
36
37
 
38
+ // --- User Sync (for profile pages) ---
39
+ router.post('/:userCid/sync', (req, res) => requestUserSync(req, res, dependencies, config));
40
+ router.get('/:userCid/sync-status', (req, res) => getUserSyncStatus(req, res, dependencies, config));
41
+
37
42
  // Signed-In User Personalized Routes
38
43
  // Note: Router is mounted at /user, so these routes should be /me/* not /user/me/*
39
44
  router.get('/me/hedges', (req, res) => getUserRecommendations(req, res, dependencies, config, 'hedges'));
@@ -46,19 +46,37 @@ async function handlePopularInvestorUpdate(taskData, config, dependencies) {
46
46
 
47
47
  logger.log('INFO', `[PI Update] Starting update for User: ${username || 'unknown'} (${cid || 'unknown'})${requestId ? ` [Request: ${requestId}]` : ''}`);
48
48
 
49
- // Update request status if this is an on-demand request
50
- if (requestId && source === 'on_demand' && db) {
49
+ // Update request status if this is an on-demand request (PI fetch or sync)
50
+ if (requestId && (source === 'on_demand' || source === 'on_demand_sync') && db) {
51
51
  try {
52
- const requestRef = db.collection('pi_fetch_requests')
52
+ // Check if it's a sync request first
53
+ const syncRequestRef = db.collection('user_sync_requests')
53
54
  .doc(String(cid))
54
55
  .collection('requests')
55
56
  .doc(requestId);
56
57
 
57
- await requestRef.update({
58
- status: 'processing',
59
- startedAt: require('@google-cloud/firestore').FieldValue.serverTimestamp(),
60
- updatedAt: require('@google-cloud/firestore').FieldValue.serverTimestamp()
61
- });
58
+ const syncRequestDoc = await syncRequestRef.get();
59
+
60
+ if (syncRequestDoc.exists) {
61
+ // This is a sync request
62
+ await syncRequestRef.update({
63
+ status: 'processing',
64
+ startedAt: require('@google-cloud/firestore').FieldValue.serverTimestamp(),
65
+ updatedAt: require('@google-cloud/firestore').FieldValue.serverTimestamp()
66
+ });
67
+ } else {
68
+ // This is a PI fetch request
69
+ const requestRef = db.collection('pi_fetch_requests')
70
+ .doc(String(cid))
71
+ .collection('requests')
72
+ .doc(requestId);
73
+
74
+ await requestRef.update({
75
+ status: 'processing',
76
+ startedAt: require('@google-cloud/firestore').FieldValue.serverTimestamp(),
77
+ updatedAt: require('@google-cloud/firestore').FieldValue.serverTimestamp()
78
+ });
79
+ }
62
80
  } catch (err) {
63
81
  logger.log('WARN', `[PI Update] Failed to update request status for ${requestId}`, err);
64
82
  }
@@ -299,14 +317,31 @@ async function handlePopularInvestorUpdate(taskData, config, dependencies) {
299
317
 
300
318
  logger.log('SUCCESS', `[PI Update] Completed full update for ${username}`);
301
319
 
302
- // Update request status and trigger computation if this is an on-demand request
303
- if (requestId && source === 'on_demand' && db) {
320
+ // Update request status and trigger computation if this is an on-demand request (PI fetch or sync)
321
+ if (requestId && (source === 'on_demand' || source === 'on_demand_sync') && db) {
304
322
  try {
305
- const requestRef = db.collection('pi_fetch_requests')
323
+ // Check if it's a sync request first
324
+ const syncRequestRef = db.collection('user_sync_requests')
306
325
  .doc(String(cid))
307
326
  .collection('requests')
308
327
  .doc(requestId);
309
328
 
329
+ const syncRequestDoc = await syncRequestRef.get();
330
+ let requestRef = null;
331
+ let isSyncRequest = false;
332
+
333
+ if (syncRequestDoc.exists) {
334
+ // This is a sync request
335
+ requestRef = syncRequestRef;
336
+ isSyncRequest = true;
337
+ } else {
338
+ // This is a PI fetch request
339
+ requestRef = db.collection('pi_fetch_requests')
340
+ .doc(String(cid))
341
+ .collection('requests')
342
+ .doc(requestId);
343
+ }
344
+
310
345
  // Update status to indicate raw data is stored, indexing and computation will be triggered
311
346
  await requestRef.update({
312
347
  status: 'indexing',
@@ -392,14 +427,29 @@ async function handlePopularInvestorUpdate(taskData, config, dependencies) {
392
427
  } catch (error) {
393
428
  logger.log('ERROR', `[PI Update] Failed for ${username}`, error);
394
429
 
395
- // Update request status to failed if this is an on-demand request
396
- if (requestId && source === 'on_demand' && db) {
430
+ // Update request status to failed if this is an on-demand request (PI fetch or sync)
431
+ if (requestId && (source === 'on_demand' || source === 'on_demand_sync') && db) {
397
432
  try {
398
- const requestRef = db.collection('pi_fetch_requests')
433
+ // Check if it's a sync request first
434
+ const syncRequestRef = db.collection('user_sync_requests')
399
435
  .doc(String(cid))
400
436
  .collection('requests')
401
437
  .doc(requestId);
402
438
 
439
+ const syncRequestDoc = await syncRequestRef.get();
440
+ let requestRef = null;
441
+
442
+ if (syncRequestDoc.exists) {
443
+ // This is a sync request
444
+ requestRef = syncRequestRef;
445
+ } else {
446
+ // This is a PI fetch request
447
+ requestRef = db.collection('pi_fetch_requests')
448
+ .doc(String(cid))
449
+ .collection('requests')
450
+ .doc(requestId);
451
+ }
452
+
403
453
  await requestRef.update({
404
454
  status: 'failed',
405
455
  error: error.message || 'Unknown error',
@@ -423,13 +473,40 @@ async function handlePopularInvestorUpdate(taskData, config, dependencies) {
423
473
  * Handles the On-Demand update for a Signed-In User.
424
474
  */
425
475
  async function handleOnDemandUserUpdate(taskData, config, dependencies) {
426
- const { cid, username } = taskData;
427
- const { ETORO_API_PORTFOLIO_URL, ETORO_API_HISTORY_URL } = config;
476
+ const { cid, username, requestId, source } = taskData;
477
+
478
+ // [FIX] Destructure dependencies first
479
+ const { logger, proxyManager, batchManager, headerManager, db } = dependencies;
480
+
481
+ // Validate and set API URLs with defaults and fallbacks
482
+ const ETORO_API_PORTFOLIO_URL = config.ETORO_API_PORTFOLIO_URL || process.env.ETORO_API_PORTFOLIO_URL || 'https://www.etoro.com/sapi/portfolios/portfolio';
483
+ const ETORO_API_HISTORY_URL = config.ETORO_API_HISTORY_URL || process.env.ETORO_API_HISTORY_URL || 'https://www.etoro.com/sapi/trade-data-real/history/public/credit/flat';
428
484
 
429
- // [FIX] Destructure headerManager
430
- const { logger, proxyManager, batchManager, headerManager } = dependencies;
485
+ if (!ETORO_API_PORTFOLIO_URL || ETORO_API_PORTFOLIO_URL === 'undefined') {
486
+ const errorMsg = 'ETORO_API_PORTFOLIO_URL is not configured. Check taskEngine config.';
487
+ logger.log('ERROR', `[On-Demand Update] ${errorMsg}`);
488
+ throw new Error(errorMsg);
489
+ }
431
490
 
432
- logger.log('INFO', `[On-Demand Update] Fetching Portfolio & History for Signed-In User: ${username} (${cid})`);
491
+ logger.log('INFO', `[On-Demand Update] Fetching Portfolio & History for Signed-In User: ${username} (${cid})${requestId ? ` [Request: ${requestId}]` : ''}`);
492
+
493
+ // Update request status if this is a sync request
494
+ if (requestId && source === 'on_demand_sync' && db) {
495
+ try {
496
+ const requestRef = db.collection('user_sync_requests')
497
+ .doc(String(cid))
498
+ .collection('requests')
499
+ .doc(requestId);
500
+
501
+ await requestRef.update({
502
+ status: 'processing',
503
+ startedAt: require('@google-cloud/firestore').FieldValue.serverTimestamp(),
504
+ updatedAt: require('@google-cloud/firestore').FieldValue.serverTimestamp()
505
+ });
506
+ } catch (err) {
507
+ logger.log('WARN', `[On-Demand Update] Failed to update request status for ${requestId}`, err);
508
+ }
509
+ }
433
510
 
434
511
  const today = new Date().toISOString().split('T')[0];
435
512
  const blockId = '19M';
@@ -568,8 +645,49 @@ async function handleOnDemandUserUpdate(taskData, config, dependencies) {
568
645
  }
569
646
 
570
647
  logger.log('SUCCESS', `[On-Demand Update] Complete for ${username}`);
648
+
649
+ // Update request status to completed if this is a sync request
650
+ if (requestId && source === 'on_demand_sync' && db) {
651
+ try {
652
+ const requestRef = db.collection('user_sync_requests')
653
+ .doc(String(cid))
654
+ .collection('requests')
655
+ .doc(requestId);
656
+
657
+ await requestRef.update({
658
+ status: 'completed',
659
+ completedAt: require('@google-cloud/firestore').FieldValue.serverTimestamp(),
660
+ updatedAt: require('@google-cloud/firestore').FieldValue.serverTimestamp()
661
+ });
662
+ logger.log('INFO', `[On-Demand Update] Updated sync request ${requestId} to completed`);
663
+ } catch (err) {
664
+ logger.log('WARN', `[On-Demand Update] Failed to update request status to completed for ${requestId}`, err);
665
+ }
666
+ }
571
667
  } catch (error) {
572
668
  logger.log('ERROR', `[On-Demand Update] Failed for ${username}`, error);
669
+
670
+ // Update request status to failed if this is a sync request
671
+ if (requestId && source === 'on_demand_sync' && db) {
672
+ try {
673
+ const requestRef = db.collection('user_sync_requests')
674
+ .doc(String(cid))
675
+ .collection('requests')
676
+ .doc(requestId);
677
+
678
+ const errorMessage = error.message || 'Unknown error occurred';
679
+ await requestRef.update({
680
+ status: 'failed',
681
+ error: errorMessage,
682
+ failedAt: require('@google-cloud/firestore').FieldValue.serverTimestamp(),
683
+ updatedAt: require('@google-cloud/firestore').FieldValue.serverTimestamp()
684
+ });
685
+ logger.log('INFO', `[On-Demand Update] Updated sync request ${requestId} to failed: ${errorMessage}`);
686
+ } catch (err) {
687
+ logger.log('WARN', `[On-Demand Update] Failed to update request status to failed for ${requestId}`, err);
688
+ }
689
+ }
690
+
573
691
  throw error;
574
692
  } finally {
575
693
  if (headerManager && headerId !== 'fallback') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.451",
3
+ "version": "1.0.453",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [