bulltrackers-module 1.0.630 → 1.0.632
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.
- package/functions/alert-system/helpers/alert_helpers.js +69 -77
- package/functions/alert-system/index.js +19 -29
- package/functions/api-v2/helpers/notification_helpers.js +187 -0
- package/functions/computation-system/context/ContextFactory.js +39 -18
- package/functions/computation-system/data/AvailabilityChecker.js +27 -11
- package/functions/computation-system/executors/MetaExecutor.js +14 -3
- package/functions/computation-system/executors/StandardExecutor.js +3 -9
- package/functions/computation-system/helpers/computation_worker.js +1 -1
- package/functions/computation-system/persistence/ResultCommitter.js +4 -3
- package/functions/computation-system/utils/data_loader.js +105 -143
- package/functions/task-engine/helpers/popular_investor_helpers.js +11 -7
- package/package.json +1 -2
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
7
7
|
const zlib = require('zlib');
|
|
8
8
|
const { getAlertTypeByComputation, generateAlertMessage } = require('./alert_type_registry');
|
|
9
|
-
|
|
9
|
+
// Migration helpers removed - write directly to new path
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Process alerts for a specific PI from computation results
|
|
@@ -65,39 +65,30 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
|
|
|
65
65
|
};
|
|
66
66
|
|
|
67
67
|
// Write to alerts collection (not notifications) - alerts are separate from system notifications
|
|
68
|
-
//
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
{
|
|
92
|
-
isCollection: true,
|
|
93
|
-
merge: false,
|
|
94
|
-
dataType: 'alerts',
|
|
95
|
-
documentId: notificationId,
|
|
96
|
-
dualWrite: false, // Disable dual write - we're fully migrated to new path
|
|
97
|
-
config,
|
|
98
|
-
collectionRegistry
|
|
99
|
-
}
|
|
100
|
-
).catch(err => {
|
|
68
|
+
// Write directly to new path: SignedInUsers/{userCid}/alerts/{alertId}
|
|
69
|
+
const alertData = {
|
|
70
|
+
alertId: notificationId,
|
|
71
|
+
piCid: Number(piCid),
|
|
72
|
+
piUsername: piUsername,
|
|
73
|
+
alertType: alertType.id,
|
|
74
|
+
alertTypeName: alertType.name,
|
|
75
|
+
message: alertMessage,
|
|
76
|
+
severity: alertType.severity,
|
|
77
|
+
watchlistId: subscription.watchlistId,
|
|
78
|
+
watchlistName: subscription.watchlistName,
|
|
79
|
+
read: false,
|
|
80
|
+
createdAt: FieldValue.serverTimestamp(),
|
|
81
|
+
computationDate: computationDate,
|
|
82
|
+
computationName: alertType.computationName,
|
|
83
|
+
...(computationMetadata || {})
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const writePromise = db.collection('SignedInUsers')
|
|
87
|
+
.doc(String(userCid))
|
|
88
|
+
.collection('alerts')
|
|
89
|
+
.doc(notificationId)
|
|
90
|
+
.set(alertData)
|
|
91
|
+
.catch(err => {
|
|
101
92
|
logger.log('ERROR', `[processAlertForPI] Failed to write alert for CID ${userCid}: ${err.message}`, err);
|
|
102
93
|
throw err; // Re-throw so we know if writes are failing
|
|
103
94
|
});
|
|
@@ -114,9 +105,21 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
|
|
|
114
105
|
// 6. Update global rootdata collection for computation system
|
|
115
106
|
// (Wrap in try-catch to prevent crashing the alert if metrics fail)
|
|
116
107
|
try {
|
|
117
|
-
const {
|
|
108
|
+
const { runRootDataIndexer } = require('../../root-data-indexer/index');
|
|
118
109
|
const triggeredUserCids = subscriptions.map(s => s.userCid);
|
|
119
|
-
|
|
110
|
+
|
|
111
|
+
// Update alert history root data by running root data indexer for the specific date
|
|
112
|
+
// The indexer will detect and update PIAlertHistoryData availability
|
|
113
|
+
const indexerConfig = {
|
|
114
|
+
availabilityCollection: 'root_data_availability',
|
|
115
|
+
targetDate: computationDate,
|
|
116
|
+
collections: {
|
|
117
|
+
piAlertHistory: 'PIAlertHistoryData'
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
await runRootDataIndexer(indexerConfig, { db, logger });
|
|
122
|
+
logger.log('INFO', `[processAlertForPI] Updated root data indexer for date ${computationDate}`);
|
|
120
123
|
} catch (e) {
|
|
121
124
|
logger.log('WARN', `[processAlertForPI] Failed to update history rootdata: ${e.message}`);
|
|
122
125
|
}
|
|
@@ -158,14 +161,18 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
|
|
|
158
161
|
// Check for developer accounts with pretendSubscribedToAllAlerts flag enabled
|
|
159
162
|
// This allows developers to test the alert system without manually configuring subscriptions
|
|
160
163
|
try {
|
|
161
|
-
const {
|
|
162
|
-
const {
|
|
164
|
+
const { isDeveloper } = require('../../api-v2/helpers/data-fetchers/firestore.js');
|
|
165
|
+
const { fetchPopularInvestorMasterList } = require('../../api-v2/helpers/data-fetchers/firestore.js');
|
|
163
166
|
const config = dependencies.config || {};
|
|
164
|
-
const collectionRegistry = dependencies.collectionRegistry || null;
|
|
165
167
|
|
|
166
|
-
// Get
|
|
167
|
-
|
|
168
|
-
|
|
168
|
+
// Get PI username from master list
|
|
169
|
+
let piUsername = `PI-${piCid}`;
|
|
170
|
+
try {
|
|
171
|
+
const piData = await fetchPopularInvestorMasterList(db, String(piCid));
|
|
172
|
+
piUsername = piData.username || piUsername;
|
|
173
|
+
} catch (e) {
|
|
174
|
+
// PI not in master list, use fallback
|
|
175
|
+
}
|
|
169
176
|
|
|
170
177
|
// Default alert config with all alert types enabled (for dev override)
|
|
171
178
|
const allAlertsEnabledConfig = {
|
|
@@ -186,8 +193,9 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
|
|
|
186
193
|
for (const devOverrideDoc of devOverridesSnapshot.docs) {
|
|
187
194
|
const devUserCid = Number(devOverrideDoc.id);
|
|
188
195
|
|
|
189
|
-
// Verify this is actually a developer account (security check)
|
|
190
|
-
|
|
196
|
+
// Verify this is actually a developer account (security check) - using api-v2 helper
|
|
197
|
+
const isDev = await isDeveloper(db, String(devUserCid));
|
|
198
|
+
if (!isDev) {
|
|
191
199
|
continue;
|
|
192
200
|
}
|
|
193
201
|
|
|
@@ -244,44 +252,26 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
|
|
|
244
252
|
}
|
|
245
253
|
|
|
246
254
|
// Step 2: For each user, read their watchlists from SignedInUsers/{cid}/watchlists
|
|
247
|
-
|
|
248
|
-
// const { collectionRegistry } = dependencies; // Already destructured above
|
|
249
|
-
// const config = dependencies.config || {}; // Already defined
|
|
250
|
-
|
|
255
|
+
// Read directly from new path (no migration needed)
|
|
251
256
|
for (const userCidStr of userCids) {
|
|
252
257
|
try {
|
|
253
258
|
const userCid = Number(userCidStr);
|
|
254
259
|
|
|
255
260
|
// Read all watchlists for this user from new path
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
'
|
|
259
|
-
|
|
260
|
-
{ cid: userCid },
|
|
261
|
-
{
|
|
262
|
-
isCollection: true,
|
|
263
|
-
dataType: 'watchlists',
|
|
264
|
-
config,
|
|
265
|
-
logger,
|
|
266
|
-
collectionRegistry: collectionRegistry
|
|
267
|
-
}
|
|
268
|
-
);
|
|
261
|
+
const watchlistsSnapshot = await db.collection('SignedInUsers')
|
|
262
|
+
.doc(String(userCid))
|
|
263
|
+
.collection('watchlists')
|
|
264
|
+
.get();
|
|
269
265
|
|
|
270
|
-
if (
|
|
266
|
+
if (watchlistsSnapshot.empty) {
|
|
271
267
|
continue;
|
|
272
268
|
}
|
|
273
269
|
|
|
274
|
-
// Get watchlists from snapshot
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
...doc.data()
|
|
280
|
-
}));
|
|
281
|
-
} else if (watchlistsResult.data) {
|
|
282
|
-
// If it's a single document, wrap it
|
|
283
|
-
watchlists = [watchlistsResult.data];
|
|
284
|
-
}
|
|
270
|
+
// Get watchlists from snapshot
|
|
271
|
+
const watchlists = watchlistsSnapshot.docs.map(doc => ({
|
|
272
|
+
id: doc.id,
|
|
273
|
+
...doc.data()
|
|
274
|
+
}));
|
|
285
275
|
|
|
286
276
|
// Step 3: Check each watchlist for the PI and alert config
|
|
287
277
|
for (const watchlistData of watchlists) {
|
|
@@ -337,11 +327,13 @@ function shouldTriggerAlert(subscription, alertTypeId) {
|
|
|
337
327
|
*/
|
|
338
328
|
async function getPIUsername(db, piCid) {
|
|
339
329
|
try {
|
|
340
|
-
// Try to get from master list first (single source of truth)
|
|
341
|
-
const {
|
|
342
|
-
const
|
|
330
|
+
// Try to get from master list first (single source of truth) - using api-v2 helper
|
|
331
|
+
const { fetchPopularInvestorMasterList } = require('../../api-v2/helpers/data-fetchers/firestore.js');
|
|
332
|
+
const piData = await fetchPopularInvestorMasterList(db, String(piCid));
|
|
343
333
|
|
|
344
|
-
if (username)
|
|
334
|
+
if (piData && piData.username) {
|
|
335
|
+
return piData.username;
|
|
336
|
+
}
|
|
345
337
|
|
|
346
338
|
// Fallback: try to get from any subscription
|
|
347
339
|
const subscriptionsSnapshot = await db.collection('watchlist_subscriptions')
|
|
@@ -427,35 +427,25 @@ async function sendAllClearNotification(db, logger, userCid, piCid, piUsername,
|
|
|
427
427
|
};
|
|
428
428
|
|
|
429
429
|
// Write to alerts collection (not notifications) - alerts are separate from system notifications
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
{
|
|
450
|
-
isCollection: true,
|
|
451
|
-
merge: false,
|
|
452
|
-
dataType: 'alerts',
|
|
453
|
-
documentId: notificationId,
|
|
454
|
-
dualWrite: false, // Disable dual write - we're fully migrated to new path
|
|
455
|
-
config,
|
|
456
|
-
collectionRegistry
|
|
457
|
-
}
|
|
458
|
-
);
|
|
430
|
+
// Write directly to new path: SignedInUsers/{userCid}/alerts/{alertId}
|
|
431
|
+
const alertData = {
|
|
432
|
+
alertId: notificationId,
|
|
433
|
+
piCid: Number(piCid),
|
|
434
|
+
piUsername: piUsername,
|
|
435
|
+
alertType: 'all_clear',
|
|
436
|
+
alertTypeName: 'All Clear',
|
|
437
|
+
message: `${piUsername} was processed, all clear today!`,
|
|
438
|
+
severity: 'info',
|
|
439
|
+
read: false,
|
|
440
|
+
createdAt: FieldValue.serverTimestamp(),
|
|
441
|
+
computationDate: date
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
await db.collection('SignedInUsers')
|
|
445
|
+
.doc(String(userCid))
|
|
446
|
+
.collection('alerts')
|
|
447
|
+
.doc(notificationId)
|
|
448
|
+
.set(alertData);
|
|
459
449
|
|
|
460
450
|
logger.log('INFO', `[sendAllClearNotification] Sent all-clear notification to user ${userCid} for PI ${piCid}`);
|
|
461
451
|
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification helpers for task engine and computation system
|
|
3
|
+
* Replaces old-generic-api notification helpers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { FieldValue } = require('@google-cloud/firestore');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Notify task engine completion
|
|
10
|
+
* @param {Firestore} db - Firestore instance
|
|
11
|
+
* @param {Object} logger - Logger instance
|
|
12
|
+
* @param {string|number} userCid - User CID
|
|
13
|
+
* @param {string} requestId - Request ID
|
|
14
|
+
* @param {string} username - Username
|
|
15
|
+
* @param {boolean} success - Whether the task succeeded
|
|
16
|
+
* @param {string|null} errorMessage - Error message if failed
|
|
17
|
+
* @param {Object} options - Additional options (collectionRegistry, config, etc.)
|
|
18
|
+
*/
|
|
19
|
+
async function notifyTaskEngineComplete(db, logger, userCid, requestId, username, success, errorMessage, options = {}) {
|
|
20
|
+
try {
|
|
21
|
+
const notificationData = {
|
|
22
|
+
type: 'sync',
|
|
23
|
+
subType: 'complete',
|
|
24
|
+
title: success ? 'Sync Complete' : 'Sync Failed',
|
|
25
|
+
message: success
|
|
26
|
+
? `Data sync for ${username} completed successfully`
|
|
27
|
+
: `Data sync for ${username} failed: ${errorMessage || 'Unknown error'}`,
|
|
28
|
+
read: false,
|
|
29
|
+
timestamp: FieldValue.serverTimestamp(),
|
|
30
|
+
createdAt: FieldValue.serverTimestamp(),
|
|
31
|
+
metadata: {
|
|
32
|
+
requestId,
|
|
33
|
+
username,
|
|
34
|
+
success,
|
|
35
|
+
errorMessage: errorMessage || null
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
await db.collection('SignedInUsers')
|
|
40
|
+
.doc(String(userCid))
|
|
41
|
+
.collection('notifications')
|
|
42
|
+
.doc(requestId)
|
|
43
|
+
.set(notificationData);
|
|
44
|
+
|
|
45
|
+
logger?.log('INFO', `[notifyTaskEngineComplete] Notification sent for user ${userCid}, request ${requestId}`);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
logger?.log('WARN', `[notifyTaskEngineComplete] Failed to send notification: ${error.message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Notify task engine progress
|
|
53
|
+
* @param {Firestore} db - Firestore instance
|
|
54
|
+
* @param {Object} logger - Logger instance
|
|
55
|
+
* @param {string|number} userCid - User CID
|
|
56
|
+
* @param {string} requestId - Request ID
|
|
57
|
+
* @param {string} username - Username
|
|
58
|
+
* @param {string} stage - Progress stage (e.g., 'started', 'portfolio_complete')
|
|
59
|
+
* @param {string|null} dataType - Data type (e.g., 'portfolio', 'tradeHistory')
|
|
60
|
+
* @param {Object} options - Additional options
|
|
61
|
+
*/
|
|
62
|
+
async function notifyTaskEngineProgress(db, logger, userCid, requestId, username, stage, dataType, options = {}) {
|
|
63
|
+
try {
|
|
64
|
+
const stageMessages = {
|
|
65
|
+
'started': 'Data sync started',
|
|
66
|
+
'portfolio_complete': 'Portfolio data fetched',
|
|
67
|
+
'history_complete': 'Trade history fetched',
|
|
68
|
+
'social_complete': 'Social posts fetched',
|
|
69
|
+
'indexing': 'Indexing data',
|
|
70
|
+
'computing': 'Running computations'
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const notificationData = {
|
|
74
|
+
type: 'sync',
|
|
75
|
+
subType: 'progress',
|
|
76
|
+
title: 'Sync Progress',
|
|
77
|
+
message: stageMessages[stage] || `Sync ${stage}`,
|
|
78
|
+
read: false,
|
|
79
|
+
timestamp: FieldValue.serverTimestamp(),
|
|
80
|
+
createdAt: FieldValue.serverTimestamp(),
|
|
81
|
+
metadata: {
|
|
82
|
+
requestId,
|
|
83
|
+
username,
|
|
84
|
+
stage,
|
|
85
|
+
dataType: dataType || null
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
await db.collection('SignedInUsers')
|
|
90
|
+
.doc(String(userCid))
|
|
91
|
+
.collection('notifications')
|
|
92
|
+
.doc(`${requestId}_${stage}`)
|
|
93
|
+
.set(notificationData);
|
|
94
|
+
|
|
95
|
+
logger?.log('INFO', `[notifyTaskEngineProgress] Progress notification sent for user ${userCid}, stage ${stage}`);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
logger?.log('WARN', `[notifyTaskEngineProgress] Failed to send notification: ${error.message}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Notify that PI data was refreshed
|
|
103
|
+
* @param {Firestore} db - Firestore instance
|
|
104
|
+
* @param {Object} logger - Logger instance
|
|
105
|
+
* @param {Object} collectionRegistry - Collection registry
|
|
106
|
+
* @param {string|number} piCid - PI CID
|
|
107
|
+
* @param {string} username - Username
|
|
108
|
+
* @param {Object} config - Config object
|
|
109
|
+
*/
|
|
110
|
+
async function notifyPIDataRefreshed(db, logger, collectionRegistry, piCid, username, config) {
|
|
111
|
+
try {
|
|
112
|
+
// This is a non-critical notification - just log it
|
|
113
|
+
logger?.log('INFO', `[notifyPIDataRefreshed] PI ${piCid} (${username}) data refreshed`);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
logger?.log('WARN', `[notifyPIDataRefreshed] Failed: ${error.message}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Notify computation completion
|
|
121
|
+
* @param {Firestore} db - Firestore instance
|
|
122
|
+
* @param {Object} logger - Logger instance
|
|
123
|
+
* @param {string|number} userCid - User CID
|
|
124
|
+
* @param {string} requestId - Request ID
|
|
125
|
+
* @param {string} computation - Computation name
|
|
126
|
+
* @param {string} displayName - Display name for computation
|
|
127
|
+
* @param {boolean} success - Whether computation succeeded
|
|
128
|
+
* @param {string|null} errorMessage - Error message if failed
|
|
129
|
+
* @param {Object} options - Additional options
|
|
130
|
+
*/
|
|
131
|
+
async function notifyComputationComplete(db, logger, userCid, requestId, computation, displayName, success, errorMessage, options = {}) {
|
|
132
|
+
try {
|
|
133
|
+
const notificationData = {
|
|
134
|
+
type: 'computation',
|
|
135
|
+
subType: 'complete',
|
|
136
|
+
title: success ? 'Computation Complete' : 'Computation Failed',
|
|
137
|
+
message: success
|
|
138
|
+
? `${displayName} completed successfully`
|
|
139
|
+
: `${displayName} failed: ${errorMessage || 'Unknown error'}`,
|
|
140
|
+
read: false,
|
|
141
|
+
timestamp: FieldValue.serverTimestamp(),
|
|
142
|
+
createdAt: FieldValue.serverTimestamp(),
|
|
143
|
+
metadata: {
|
|
144
|
+
requestId,
|
|
145
|
+
computation,
|
|
146
|
+
displayName,
|
|
147
|
+
success,
|
|
148
|
+
errorMessage: errorMessage || null
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
await db.collection('SignedInUsers')
|
|
153
|
+
.doc(String(userCid))
|
|
154
|
+
.collection('notifications')
|
|
155
|
+
.doc(`${requestId}_${computation}`)
|
|
156
|
+
.set(notificationData);
|
|
157
|
+
|
|
158
|
+
logger?.log('INFO', `[notifyComputationComplete] Notification sent for user ${userCid}, computation ${computation}`);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
logger?.log('WARN', `[notifyComputationComplete] Failed to send notification: ${error.message}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get computation display name
|
|
166
|
+
* @param {string} computation - Computation name
|
|
167
|
+
* @returns {string} Display name
|
|
168
|
+
*/
|
|
169
|
+
function getComputationDisplayName(computation) {
|
|
170
|
+
const displayNames = {
|
|
171
|
+
'SignedInUserProfileMetrics': 'Profile Metrics',
|
|
172
|
+
'PopularInvestorProfileMetrics': 'PI Profile Metrics',
|
|
173
|
+
'SignedInUserPIPersonalizedMetrics': 'Personalized PI Metrics',
|
|
174
|
+
'SimilarInvestors': 'Similar Investors',
|
|
175
|
+
'RecommendedHedges': 'Recommended Hedges',
|
|
176
|
+
'PopularInvestorRankings': 'PI Rankings'
|
|
177
|
+
};
|
|
178
|
+
return displayNames[computation] || computation;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
notifyTaskEngineComplete,
|
|
183
|
+
notifyTaskEngineProgress,
|
|
184
|
+
notifyPIDataRefreshed,
|
|
185
|
+
notifyComputationComplete,
|
|
186
|
+
getComputationDisplayName
|
|
187
|
+
};
|
|
@@ -20,17 +20,32 @@ class ContextFactory {
|
|
|
20
20
|
|
|
21
21
|
static buildPerUserContext(options) {
|
|
22
22
|
const {
|
|
23
|
-
todayPortfolio,
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
todayPortfolio,
|
|
24
|
+
yesterdayPortfolio,
|
|
25
|
+
todayHistory,
|
|
26
|
+
yesterdayHistory,
|
|
27
|
+
userId,
|
|
28
|
+
userType,
|
|
29
|
+
dateStr,
|
|
30
|
+
metadata,
|
|
31
|
+
mappings,
|
|
32
|
+
insights,
|
|
33
|
+
socialData,
|
|
34
|
+
computedDependencies,
|
|
35
|
+
previousComputedDependencies,
|
|
36
|
+
config,
|
|
37
|
+
deps,
|
|
26
38
|
verification,
|
|
27
|
-
rankings,
|
|
28
|
-
|
|
39
|
+
rankings,
|
|
40
|
+
yesterdayRankings,
|
|
41
|
+
allRankings,
|
|
42
|
+
allRankingsYesterday,
|
|
29
43
|
allVerifications,
|
|
30
|
-
|
|
31
|
-
|
|
44
|
+
ratings,
|
|
45
|
+
pageViews,
|
|
46
|
+
watchlistMembership,
|
|
47
|
+
alertHistory,
|
|
32
48
|
piMasterList,
|
|
33
|
-
// [NEW] Series Data (Lookback for Root Data or Computation Results)
|
|
34
49
|
seriesData
|
|
35
50
|
} = options;
|
|
36
51
|
|
|
@@ -56,14 +71,11 @@ class ContextFactory {
|
|
|
56
71
|
rankings: allRankings || [],
|
|
57
72
|
rankingsYesterday: allRankingsYesterday || [],
|
|
58
73
|
verifications: allVerifications || {},
|
|
59
|
-
// [NEW] New Root Data Types for Profile Metrics
|
|
60
74
|
ratings: ratings || {},
|
|
61
75
|
pageViews: pageViews || {},
|
|
62
76
|
watchlistMembership: watchlistMembership || {},
|
|
63
77
|
alertHistory: alertHistory || {},
|
|
64
78
|
piMasterList: piMasterList || {},
|
|
65
|
-
// [NEW] Expose Series Data
|
|
66
|
-
// Structure: { root: { [type]: { [date]: data } }, results: { [date]: { [calcName]: data } } }
|
|
67
79
|
series: seriesData || {}
|
|
68
80
|
}
|
|
69
81
|
};
|
|
@@ -71,13 +83,23 @@ class ContextFactory {
|
|
|
71
83
|
|
|
72
84
|
static buildMetaContext(options) {
|
|
73
85
|
const {
|
|
74
|
-
dateStr,
|
|
75
|
-
|
|
76
|
-
|
|
86
|
+
dateStr,
|
|
87
|
+
metadata,
|
|
88
|
+
mappings,
|
|
89
|
+
insights,
|
|
90
|
+
socialData,
|
|
91
|
+
prices,
|
|
92
|
+
computedDependencies,
|
|
93
|
+
previousComputedDependencies,
|
|
94
|
+
config,
|
|
95
|
+
deps,
|
|
96
|
+
allRankings,
|
|
97
|
+
allRankingsYesterday,
|
|
77
98
|
allVerifications,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
99
|
+
ratings,
|
|
100
|
+
pageViews,
|
|
101
|
+
watchlistMembership,
|
|
102
|
+
alertHistory,
|
|
81
103
|
seriesData
|
|
82
104
|
} = options;
|
|
83
105
|
|
|
@@ -99,7 +121,6 @@ class ContextFactory {
|
|
|
99
121
|
pageViews: pageViews || {},
|
|
100
122
|
watchlistMembership: watchlistMembership || {},
|
|
101
123
|
alertHistory: alertHistory || {},
|
|
102
|
-
// [NEW] Expose Series Data
|
|
103
124
|
series: seriesData || {}
|
|
104
125
|
}
|
|
105
126
|
};
|
|
@@ -30,11 +30,11 @@ function checkRootDependencies(calcManifest, rootDataStatus) {
|
|
|
30
30
|
let isAvailable = false;
|
|
31
31
|
|
|
32
32
|
if (dep === 'portfolio') {
|
|
33
|
-
if (userType === 'speculator'
|
|
34
|
-
else if (userType === 'normal'
|
|
33
|
+
if (userType === 'speculator' && rootDataStatus.speculatorPortfolio) isAvailable = true;
|
|
34
|
+
else if (userType === 'normal' && rootDataStatus.normalPortfolio) isAvailable = true;
|
|
35
35
|
else if (userType === 'popular_investor' && rootDataStatus.piPortfolios) isAvailable = true;
|
|
36
|
-
else if (userType === 'signed_in_user'
|
|
37
|
-
else if (userType === 'all'
|
|
36
|
+
else if (userType === 'signed_in_user' && rootDataStatus.signedInUserPortfolio) isAvailable = true;
|
|
37
|
+
else if (userType === 'all' && rootDataStatus.hasPortfolio) isAvailable = true;
|
|
38
38
|
|
|
39
39
|
if (!isAvailable) {
|
|
40
40
|
// [OPTIMIZATION] Optimistic Series Check
|
|
@@ -284,12 +284,28 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
|
|
|
284
284
|
logger.log('WARN', `[Availability] Index not found for ${dateStr}. Assuming NO data.`);
|
|
285
285
|
return {
|
|
286
286
|
status: {
|
|
287
|
-
hasPortfolio: false,
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
287
|
+
hasPortfolio: false,
|
|
288
|
+
hasHistory: false,
|
|
289
|
+
hasSocial: false,
|
|
290
|
+
hasInsights: false,
|
|
291
|
+
hasPrices: false,
|
|
292
|
+
speculatorPortfolio: false,
|
|
293
|
+
normalPortfolio: false,
|
|
294
|
+
speculatorHistory: false,
|
|
295
|
+
normalHistory: false,
|
|
296
|
+
piRankings: false,
|
|
297
|
+
piPortfolios: false,
|
|
298
|
+
piDeepPortfolios: false,
|
|
299
|
+
piHistory: false,
|
|
300
|
+
signedInUserPortfolio: false,
|
|
301
|
+
signedInUserHistory: false,
|
|
302
|
+
signedInUserVerification: false,
|
|
303
|
+
hasPISocial: false,
|
|
304
|
+
hasSignedInSocial: false,
|
|
305
|
+
piRatings: false,
|
|
306
|
+
piPageViews: false,
|
|
307
|
+
watchlistMembership: false,
|
|
308
|
+
piAlertHistory: false
|
|
293
309
|
}
|
|
294
310
|
};
|
|
295
311
|
}
|
|
@@ -361,5 +377,5 @@ module.exports = {
|
|
|
361
377
|
checkRootDependencies,
|
|
362
378
|
checkRootDataAvailability,
|
|
363
379
|
getViableCalculations,
|
|
364
|
-
getAvailabilityWindow
|
|
380
|
+
getAvailabilityWindow
|
|
365
381
|
};
|
|
@@ -10,7 +10,7 @@ const { normalizeName } = require('../utils/utils');
|
|
|
10
10
|
const { CachedDataLoader } = require('../data/CachedDataLoader');
|
|
11
11
|
const { ContextFactory } = require('../context/ContextFactory');
|
|
12
12
|
const { commitResults } = require('../persistence/ResultCommitter');
|
|
13
|
-
const { fetchResultSeries } = require('../data/DependencyFetcher');
|
|
13
|
+
const { fetchResultSeries } = require('../data/DependencyFetcher');
|
|
14
14
|
|
|
15
15
|
class MetaExecutor {
|
|
16
16
|
static async run(date, calcs, passName, config, deps, rootData, fetchedDeps, previousFetchedDeps) {
|
|
@@ -47,6 +47,8 @@ class MetaExecutor {
|
|
|
47
47
|
let ratings = null, pageViews = null, watchlistMembership = null, alertHistory = null;
|
|
48
48
|
if (needsNewRootData) {
|
|
49
49
|
const loadPromises = [];
|
|
50
|
+
|
|
51
|
+
// Ratings
|
|
50
52
|
if (calcs.some(c => c.rootDataDependencies?.includes('ratings'))) {
|
|
51
53
|
loadPromises.push(loader.loadRatings(dStr).then(r => { ratings = r; }).catch(e => {
|
|
52
54
|
// Only catch if ALL calcs allow missing roots
|
|
@@ -61,6 +63,8 @@ class MetaExecutor {
|
|
|
61
63
|
}
|
|
62
64
|
}));
|
|
63
65
|
}
|
|
66
|
+
|
|
67
|
+
// PageViews
|
|
64
68
|
if (calcs.some(c => c.rootDataDependencies?.includes('pageViews'))) {
|
|
65
69
|
loadPromises.push(loader.loadPageViews(dStr).then(pv => { pageViews = pv; }).catch(e => {
|
|
66
70
|
const allAllowMissing = calcs.every(c => {
|
|
@@ -74,6 +78,8 @@ class MetaExecutor {
|
|
|
74
78
|
}
|
|
75
79
|
}));
|
|
76
80
|
}
|
|
81
|
+
|
|
82
|
+
// Watchlist
|
|
77
83
|
if (calcs.some(c => c.rootDataDependencies?.includes('watchlist'))) {
|
|
78
84
|
loadPromises.push(loader.loadWatchlistMembership(dStr).then(w => { watchlistMembership = w; }).catch(e => {
|
|
79
85
|
const allAllowMissing = calcs.every(c => {
|
|
@@ -87,6 +93,8 @@ class MetaExecutor {
|
|
|
87
93
|
}
|
|
88
94
|
}));
|
|
89
95
|
}
|
|
96
|
+
|
|
97
|
+
// Alerts
|
|
90
98
|
if (calcs.some(c => c.rootDataDependencies?.includes('alerts'))) {
|
|
91
99
|
loadPromises.push(loader.loadAlertHistory(dStr).then(a => { alertHistory = a; }).catch(e => {
|
|
92
100
|
const allAllowMissing = calcs.every(c => {
|
|
@@ -100,6 +108,7 @@ class MetaExecutor {
|
|
|
100
108
|
}
|
|
101
109
|
}));
|
|
102
110
|
}
|
|
111
|
+
|
|
103
112
|
await Promise.all(loadPromises);
|
|
104
113
|
|
|
105
114
|
// [FIX] Enforce canHaveMissingRoots - validate after loading
|
|
@@ -163,7 +172,8 @@ class MetaExecutor {
|
|
|
163
172
|
else if (type === 'insights') loaderMethod = 'loadInsights';
|
|
164
173
|
else if (type === 'ratings') loaderMethod = 'loadRatings';
|
|
165
174
|
else if (type === 'watchlist') loaderMethod = 'loadWatchlistMembership';
|
|
166
|
-
// Add
|
|
175
|
+
// [CRITICAL UPDATE] Add rankings support for Meta lookbacks
|
|
176
|
+
else if (type === 'rankings') loaderMethod = 'loadRankings';
|
|
167
177
|
|
|
168
178
|
if (loaderMethod) {
|
|
169
179
|
logger.log('INFO', `[MetaExecutor] Loading ${days}-day series for Root Data '${type}'...`);
|
|
@@ -222,7 +232,8 @@ class MetaExecutor {
|
|
|
222
232
|
}
|
|
223
233
|
}
|
|
224
234
|
|
|
225
|
-
|
|
235
|
+
// CRITICAL FIX: Pass 'isInitialWrite: true' to ensure proper cleanup of old meta data
|
|
236
|
+
return await commitResults(state, dStr, passName, config, deps, false, { isInitialWrite: true });
|
|
226
237
|
}
|
|
227
238
|
|
|
228
239
|
static async executeOncePerDay(calcInstance, metadata, dateStr, computedDeps, prevDeps, config, deps, loader) {
|
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* {
|
|
3
|
-
* type: uploaded file
|
|
4
|
-
* fileName: computation-system/executors/StandardExecutor.js
|
|
5
|
-
* }
|
|
6
|
-
*/
|
|
7
1
|
const { normalizeName, getEarliestDataDates } = require('../utils/utils');
|
|
8
2
|
const { streamPortfolioData, streamHistoryData, getPortfolioPartRefs, getHistoryPartRefs } = require('../utils/data_loader');
|
|
9
3
|
const { CachedDataLoader } = require('../data/CachedDataLoader');
|
|
10
4
|
const { ContextFactory } = require('../context/ContextFactory');
|
|
11
5
|
const { commitResults } = require('../persistence/ResultCommitter');
|
|
12
|
-
// [NEW] Import series fetcher for computation results
|
|
13
6
|
const { fetchResultSeries } = require('../data/DependencyFetcher');
|
|
14
7
|
const mathLayer = require('../layers/index');
|
|
15
8
|
const { performance } = require('perf_hooks');
|
|
@@ -167,7 +160,8 @@ class StandardExecutor {
|
|
|
167
160
|
else if (type === 'insights') loaderMethod = 'loadInsights';
|
|
168
161
|
else if (type === 'ratings') loaderMethod = 'loadRatings';
|
|
169
162
|
else if (type === 'watchlist') loaderMethod = 'loadWatchlistMembership';
|
|
170
|
-
// Add
|
|
163
|
+
// [CRITICAL UPDATE] Add rankings support for AUM lookback
|
|
164
|
+
else if (type === 'rankings') loaderMethod = 'loadRankings';
|
|
171
165
|
|
|
172
166
|
if (loaderMethod) {
|
|
173
167
|
logger.log('INFO', `[StandardExecutor] Loading ${days}-day series for Root Data '${type}'...`);
|
|
@@ -487,4 +481,4 @@ class StandardExecutor {
|
|
|
487
481
|
}
|
|
488
482
|
}
|
|
489
483
|
|
|
490
|
-
module.exports = { StandardExecutor };
|
|
484
|
+
module.exports = { StandardExecutor };
|
|
@@ -232,7 +232,7 @@ async function handleComputationTask(message, config, dependencies) {
|
|
|
232
232
|
await db.doc(ledgerPath).update({ status: 'COMPLETED', completedAt: new Date() });
|
|
233
233
|
await recordRunAttempt(db, { date, computation, pass }, 'SUCCESS', null, metrics, triggerReason, resourceTier);
|
|
234
234
|
|
|
235
|
-
const { notifyComputationComplete, getComputationDisplayName } = require('../../
|
|
235
|
+
const { notifyComputationComplete, getComputationDisplayName } = require('../../api-v2/helpers/notification_helpers.js');
|
|
236
236
|
// Send notification if this was an on-demand computation
|
|
237
237
|
if (metadata?.onDemand && metadata?.requestId && metadata?.requestingUserCid) {
|
|
238
238
|
try {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* UPDATED: Added support for 'isPage' mode to store per-user data in subcollections.
|
|
5
5
|
* UPDATED: Implemented TTL retention policy. Defaults to 90 days from the computation date.
|
|
6
6
|
* UPDATED: Fixed issue where switching to 'isPage' mode didn't clean up old sharded/raw data.
|
|
7
|
+
* CRITICAL FIX: Fixed sharding logic to prevent wiping existing shards during INTERMEDIATE flushes.
|
|
7
8
|
*/
|
|
8
9
|
const { commitBatchInChunks, generateDataHash, FieldValue } = require('../utils/utils')
|
|
9
10
|
const { updateComputationStatus } = require('./StatusRepository');
|
|
@@ -138,8 +139,6 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
138
139
|
continue;
|
|
139
140
|
}
|
|
140
141
|
|
|
141
|
-
// [NEW] Page Computation Logic (Fan-Out) with TTL
|
|
142
|
-
// Bypasses standard compression/sharding to write per-user documents
|
|
143
142
|
// [NEW] Page Computation Logic (Fan-Out) with TTL
|
|
144
143
|
// Bypasses standard compression/sharding to write per-user documents
|
|
145
144
|
if (isPageComputation && !isEmpty) {
|
|
@@ -429,7 +428,9 @@ async function writeSingleResult(result, docRef, name, dateContext, logger, conf
|
|
|
429
428
|
let finalStats = { totalSize: 0, isSharded: false, shardCount: 1, nextShardIndex: startShardIndex };
|
|
430
429
|
let rootMergeOption = !isInitialWrite;
|
|
431
430
|
|
|
432
|
-
|
|
431
|
+
// CRITICAL FIX: Only wipe existing shards if this is the INITIAL write for this batch run.
|
|
432
|
+
// If we are flushing intermediate chunks, we should NOT wipe the shards created by previous chunks!
|
|
433
|
+
let shouldWipeShards = wasSharded && isInitialWrite;
|
|
433
434
|
|
|
434
435
|
for (let attempt = 0; attempt < strategies.length; attempt++) {
|
|
435
436
|
if (committed) break;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Data loader sub-pipes for the Computation System.
|
|
3
3
|
* REFACTORED: Now stateless and receive dependencies.
|
|
4
|
-
* FIXED: Added strict userType filtering to prevent fetching unnecessary data
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* FIXED: Added strict userType filtering to prevent fetching unnecessary data.
|
|
5
|
+
* UPDATED: Verification now uses CollectionGroup query due to per-user storage.
|
|
6
|
+
* UPDATED: Ratings now correctly handles flattened top-level schema (keys like "reviews.ID").
|
|
7
|
+
* REMOVED: Redundant Price Shard Indexing logic.
|
|
8
8
|
*/
|
|
9
9
|
const zlib = require('zlib');
|
|
10
10
|
|
|
@@ -23,11 +23,9 @@ function tryDecompress(data) {
|
|
|
23
23
|
|
|
24
24
|
/** --- Data Loader Sub-Pipes (Stateless, Dependency-Injection) --- */
|
|
25
25
|
|
|
26
|
-
/**
|
|
27
|
-
* [UPDATED] Accepts requiredUserTypes to filter collections.
|
|
28
|
-
*/
|
|
26
|
+
/** Stage 1: Get portfolio part document references for a given date */
|
|
29
27
|
async function getPortfolioPartRefs(config, deps, dateString, requiredUserTypes = null) {
|
|
30
|
-
const { db, logger, calculationUtils
|
|
28
|
+
const { db, logger, calculationUtils } = deps;
|
|
31
29
|
const { withRetry } = calculationUtils;
|
|
32
30
|
|
|
33
31
|
// Normalize required types. If null/empty or contains 'ALL', fetch everything.
|
|
@@ -250,7 +248,6 @@ async function loadDailyInsights(config, deps, dateString) {
|
|
|
250
248
|
async function loadDailySocialPostInsights(config, deps, dateString) {
|
|
251
249
|
const { db, logger, calculationUtils, collectionRegistry } = deps;
|
|
252
250
|
const { withRetry } = calculationUtils;
|
|
253
|
-
const { getCollectionPath } = collectionRegistry || {};
|
|
254
251
|
|
|
255
252
|
logger.log('INFO', `Loading and partitioning social data for ${dateString}`);
|
|
256
253
|
|
|
@@ -262,7 +259,6 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
|
|
|
262
259
|
};
|
|
263
260
|
|
|
264
261
|
// NEW STRUCTURE: Read from date-based collections
|
|
265
|
-
// Structure: Collection/{date}/{cid}/{cid} for user social, Collection/{date}/posts/{postId} for instrument
|
|
266
262
|
try {
|
|
267
263
|
// Signed-In User Social: SignedInUserSocialPostData/{date}/{cid}/{cid}
|
|
268
264
|
const signedInSocialCollectionName = 'SignedInUserSocialPostData';
|
|
@@ -279,7 +275,6 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
|
|
|
279
275
|
const cidData = tryDecompress(cidDoc.data());
|
|
280
276
|
if (cidData.posts && typeof cidData.posts === 'object') {
|
|
281
277
|
if (!result.signedIn[cid]) result.signedIn[cid] = {};
|
|
282
|
-
// Posts are stored as a map in the document
|
|
283
278
|
Object.assign(result.signedIn[cid], cidData.posts);
|
|
284
279
|
}
|
|
285
280
|
}
|
|
@@ -328,13 +323,10 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
|
|
|
328
323
|
const PI_COL_NAME = config.piSocialCollectionName || config.piSocialCollection || 'pi_social_posts';
|
|
329
324
|
const SIGNED_IN_COL_NAME = config.signedInUserSocialCollection || 'signed_in_users_social';
|
|
330
325
|
|
|
331
|
-
// 2. Define Time Range (UTC Day)
|
|
332
326
|
const startDate = new Date(dateString + 'T00:00:00Z');
|
|
333
327
|
const endDate = new Date(dateString + 'T23:59:59Z');
|
|
334
328
|
|
|
335
329
|
try {
|
|
336
|
-
// 3. Fetch ALL with CollectionGroup
|
|
337
|
-
// NOTE: Requires Firestore Index: CollectionId 'posts', Field 'fetchedAt' (ASC/DESC)
|
|
338
330
|
const postsQuery = db.collectionGroup('posts')
|
|
339
331
|
.where('fetchedAt', '>=', startDate)
|
|
340
332
|
.where('fetchedAt', '<=', endDate);
|
|
@@ -346,9 +338,7 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
|
|
|
346
338
|
const data = tryDecompress(doc.data());
|
|
347
339
|
const path = doc.ref.path;
|
|
348
340
|
|
|
349
|
-
// 4. Partition Logic based on Path
|
|
350
341
|
if (path.includes(PI_COL_NAME)) {
|
|
351
|
-
// Path format: .../pi_social_posts/{userId}/posts/{postId}
|
|
352
342
|
const parts = path.split('/');
|
|
353
343
|
const colIndex = parts.indexOf(PI_COL_NAME);
|
|
354
344
|
if (colIndex !== -1 && parts[colIndex + 1]) {
|
|
@@ -358,7 +348,6 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
|
|
|
358
348
|
}
|
|
359
349
|
}
|
|
360
350
|
else if (path.includes(SIGNED_IN_COL_NAME)) {
|
|
361
|
-
// Path format: .../signed_in_users_social/{userId}/posts/{postId}
|
|
362
351
|
const parts = path.split('/');
|
|
363
352
|
const colIndex = parts.indexOf(SIGNED_IN_COL_NAME);
|
|
364
353
|
if (colIndex !== -1 && parts[colIndex + 1]) {
|
|
@@ -368,7 +357,6 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
|
|
|
368
357
|
}
|
|
369
358
|
}
|
|
370
359
|
else {
|
|
371
|
-
// Default: Generic Instrument Posts
|
|
372
360
|
result.generic[doc.id] = data;
|
|
373
361
|
}
|
|
374
362
|
});
|
|
@@ -384,11 +372,9 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
|
|
|
384
372
|
return result;
|
|
385
373
|
}
|
|
386
374
|
|
|
387
|
-
/**
|
|
388
|
-
* [UPDATED] Accepts requiredUserTypes to filter collections.
|
|
389
|
-
*/
|
|
375
|
+
/** Stage 6: Get history part references for a given date */
|
|
390
376
|
async function getHistoryPartRefs(config, deps, dateString, requiredUserTypes = null) {
|
|
391
|
-
const { db, logger, calculationUtils
|
|
377
|
+
const { db, logger, calculationUtils } = deps;
|
|
392
378
|
const { withRetry } = calculationUtils;
|
|
393
379
|
|
|
394
380
|
// Normalize required types
|
|
@@ -478,9 +464,7 @@ async function getHistoryPartRefs(config, deps, dateString, requiredUserTypes =
|
|
|
478
464
|
return allPartRefs;
|
|
479
465
|
}
|
|
480
466
|
|
|
481
|
-
/**
|
|
482
|
-
* [UPDATED] Passes requiredUserTypes to getPortfolioPartRefs
|
|
483
|
-
*/
|
|
467
|
+
/** Stage 7: Stream portfolio data in chunks */
|
|
484
468
|
async function* streamPortfolioData(config, deps, dateString, providedRefs = null, requiredUserTypes = null) {
|
|
485
469
|
const { logger } = deps;
|
|
486
470
|
const refs = providedRefs || (await getPortfolioPartRefs(config, deps, dateString, requiredUserTypes));
|
|
@@ -497,9 +481,7 @@ async function* streamPortfolioData(config, deps, dateString, providedRefs = nul
|
|
|
497
481
|
logger.log('INFO', `[streamPortfolioData] Finished streaming for ${dateString}.`);
|
|
498
482
|
}
|
|
499
483
|
|
|
500
|
-
/**
|
|
501
|
-
* [UPDATED] Passes requiredUserTypes to getHistoryPartRefs
|
|
502
|
-
*/
|
|
484
|
+
/** Stage 8: Stream history data in chunks */
|
|
503
485
|
async function* streamHistoryData(config, deps, dateString, providedRefs = null, requiredUserTypes = null) {
|
|
504
486
|
const { logger } = deps;
|
|
505
487
|
const refs = providedRefs || (await getHistoryPartRefs(config, deps, dateString, requiredUserTypes));
|
|
@@ -531,80 +513,22 @@ async function getPriceShardRefs(config, deps) {
|
|
|
531
513
|
}
|
|
532
514
|
}
|
|
533
515
|
|
|
534
|
-
/** Stage 10: Smart Shard Lookup System */
|
|
516
|
+
/** Stage 10: Smart Shard Lookup System (DEPRECATED/SIMPLIFIED) */
|
|
535
517
|
async function ensurePriceShardIndex(config, deps) {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
// 1. Try to fetch existing index
|
|
541
|
-
const snap = await indexDocRef.get();
|
|
542
|
-
if (snap.exists) {
|
|
543
|
-
const data = snap.data();
|
|
544
|
-
const lastUpdated = data.lastUpdated ? new Date(data.lastUpdated).getTime() : 0;
|
|
545
|
-
const now = Date.now();
|
|
546
|
-
const oneDayMs = 24 * 60 * 60 * 1000;
|
|
547
|
-
if ((now - lastUpdated) < oneDayMs) { return data.index || {}; }
|
|
548
|
-
logger.log('INFO', '[ShardIndex] Index is stale (>24h). Rebuilding...');
|
|
549
|
-
} else {
|
|
550
|
-
logger.log('INFO', '[ShardIndex] Index not found. Building new Price Shard Index...');
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// 2. Build Index
|
|
554
|
-
const collection = config.priceCollection || 'asset_prices';
|
|
555
|
-
const snapshot = await db.collection(collection).get();
|
|
556
|
-
|
|
557
|
-
const index = {};
|
|
558
|
-
let shardCount = 0;
|
|
559
|
-
|
|
560
|
-
snapshot.forEach(doc => {
|
|
561
|
-
shardCount++;
|
|
562
|
-
const rawData = doc.data();
|
|
563
|
-
const data = tryDecompress(rawData);
|
|
564
|
-
|
|
565
|
-
if (data.history) {
|
|
566
|
-
Object.keys(data.history).forEach(instId => {
|
|
567
|
-
index[instId] = doc.id;
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
// 3. Save Index
|
|
573
|
-
await indexDocRef.set({
|
|
574
|
-
index: index,
|
|
575
|
-
lastUpdated: new Date().toISOString(),
|
|
576
|
-
shardCount: shardCount
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
logger.log('INFO', `[ShardIndex] Built index for ${Object.keys(index).length} instruments across ${shardCount} shards.`);
|
|
580
|
-
return index;
|
|
518
|
+
// [DEPRECATED] This function previously built an index in 'system_metadata/price_shard_index'.
|
|
519
|
+
// It has been removed to avoid performing computation/indexing in the data loader.
|
|
520
|
+
// Use 'Fetch All' strategy in Stage 9 instead.
|
|
521
|
+
return {};
|
|
581
522
|
}
|
|
582
523
|
|
|
583
524
|
async function getRelevantShardRefs(config, deps, targetInstrumentIds) {
|
|
584
|
-
const {
|
|
585
|
-
|
|
586
|
-
if (!targetInstrumentIds || targetInstrumentIds.length === 0) {
|
|
587
|
-
return getPriceShardRefs(config, deps);
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
logger.log('INFO', `[ShardLookup] Resolving shards for ${targetInstrumentIds.length} specific instruments...`);
|
|
591
|
-
|
|
592
|
-
const index = await ensurePriceShardIndex(config, deps);
|
|
593
|
-
const uniqueShardIds = new Set();
|
|
594
|
-
const collection = config.priceCollection || 'asset_prices';
|
|
595
|
-
|
|
596
|
-
let foundCount = 0;
|
|
597
|
-
for (const id of targetInstrumentIds) {
|
|
598
|
-
const shardId = index[id];
|
|
599
|
-
if (shardId) {
|
|
600
|
-
uniqueShardIds.add(shardId);
|
|
601
|
-
foundCount++;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
logger.log('INFO', `[ShardLookup] Mapped ${foundCount}/${targetInstrumentIds.length} instruments to ${uniqueShardIds.size} unique shards.`);
|
|
525
|
+
const { logger } = deps;
|
|
606
526
|
|
|
607
|
-
|
|
527
|
+
// [UPDATED] Smart shard lookup is disabled due to missing index infrastructure
|
|
528
|
+
// and to avoid computing indexes during load time.
|
|
529
|
+
// Falling back to Stage 9 (Fetch All Shards).
|
|
530
|
+
logger.log('INFO', `[ShardLookup] Smart indexing disabled. Fetching all price shards for ${targetInstrumentIds ? targetInstrumentIds.length : 'all'} instruments.`);
|
|
531
|
+
return getPriceShardRefs(config, deps);
|
|
608
532
|
}
|
|
609
533
|
|
|
610
534
|
/** Stage 11: Load Popular Investor Rankings */
|
|
@@ -625,40 +549,58 @@ async function loadPopularInvestorRankings(config, deps, dateString) {
|
|
|
625
549
|
}
|
|
626
550
|
|
|
627
551
|
const data = tryDecompress(docSnap.data());
|
|
628
|
-
return data.Items || [];
|
|
552
|
+
return data.Items || [];
|
|
629
553
|
} catch (error) {
|
|
630
554
|
logger.log('ERROR', `Failed to load Rankings for ${dateString}: ${error.message}`);
|
|
631
555
|
return null;
|
|
632
556
|
}
|
|
633
557
|
}
|
|
634
558
|
|
|
635
|
-
/** Stage 12: Load User Verification Profiles
|
|
559
|
+
/** Stage 12: Load User Verification Profiles
|
|
560
|
+
* [UPDATED] Scans global verification data via CollectionGroup since it's now stored per-user.
|
|
561
|
+
*/
|
|
636
562
|
async function loadVerificationProfiles(config, deps) {
|
|
637
563
|
const { db, logger, calculationUtils } = deps;
|
|
638
564
|
const { withRetry } = calculationUtils;
|
|
639
|
-
const collectionName = config.verificationCollection || 'verified_users';
|
|
640
565
|
|
|
641
|
-
|
|
566
|
+
// Verification is now stored at /SignedInUsers/{cid}/verification/data
|
|
567
|
+
// To fetch globally, we must use a CollectionGroup query on 'verification'
|
|
568
|
+
// and filter for the document ID 'data'.
|
|
569
|
+
|
|
570
|
+
logger.log('INFO', `Loading Verification Profiles (CollectionGroup: verification/data)`);
|
|
642
571
|
|
|
643
572
|
try {
|
|
644
|
-
|
|
573
|
+
// Warning: This requires a Firestore Index if used with complex filters, but basic get() usually works.
|
|
574
|
+
const snapshot = await withRetry(() => db.collectionGroup('verification').get(), 'getVerificationsGroup');
|
|
645
575
|
|
|
646
576
|
if (snapshot.empty) return {};
|
|
647
577
|
|
|
648
578
|
const profiles = {};
|
|
579
|
+
let count = 0;
|
|
580
|
+
|
|
649
581
|
snapshot.forEach(doc => {
|
|
582
|
+
if (doc.id !== 'data') return; // Enforce specific document ID from schema
|
|
583
|
+
|
|
650
584
|
const raw = tryDecompress(doc.data());
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
585
|
+
|
|
586
|
+
// Map new schema fields to internal profile structure
|
|
587
|
+
// New Schema: { etoroCID, etoroUsername, verifiedAt, setupCompletedAt ... }
|
|
588
|
+
if (raw.etoroCID) {
|
|
589
|
+
profiles[raw.etoroCID] = {
|
|
590
|
+
cid: raw.etoroCID,
|
|
591
|
+
username: raw.etoroUsername,
|
|
592
|
+
// 'aboutMe' and 'restrictions' are NOT present in the new schema.
|
|
593
|
+
// Defaulting to empty values to preserve downstream compatibility.
|
|
594
|
+
aboutMe: "",
|
|
595
|
+
aboutMeShort: "",
|
|
596
|
+
isVerified: !!(raw.verifiedAt), // Using existence of verifiedAt as flag
|
|
597
|
+
restrictions: []
|
|
598
|
+
};
|
|
599
|
+
count++;
|
|
600
|
+
}
|
|
660
601
|
});
|
|
661
602
|
|
|
603
|
+
logger.log('INFO', `Loaded ${count} verification profiles.`);
|
|
662
604
|
return profiles;
|
|
663
605
|
} catch (error) {
|
|
664
606
|
logger.log('ERROR', `Failed to load Verification Profiles: ${error.message}`);
|
|
@@ -666,30 +608,60 @@ async function loadVerificationProfiles(config, deps) {
|
|
|
666
608
|
}
|
|
667
609
|
}
|
|
668
610
|
|
|
669
|
-
/** Stage 13: Load PI Ratings Data
|
|
611
|
+
/** Stage 13: Load PI Ratings Data
|
|
612
|
+
* [UPDATED] Reads from /PiReviews/{date}/shards/daily_log.
|
|
613
|
+
* [FIXED] Handles FLATTENED schema where keys like "reviews.ID" are at the top level.
|
|
614
|
+
* Returns RAW logs grouped by PI. NO COMPUTATION.
|
|
615
|
+
*/
|
|
670
616
|
async function loadPIRatings(config, deps, dateString) {
|
|
671
617
|
const { db, logger, calculationUtils } = deps;
|
|
672
618
|
const { withRetry } = calculationUtils;
|
|
673
|
-
const collectionName = config.piRatingsCollection || 'PIRatingsData';
|
|
674
619
|
|
|
675
|
-
|
|
620
|
+
// New Path: /PiReviews/{date}/shards/daily_log
|
|
621
|
+
|
|
622
|
+
logger.log('INFO', `Loading PI Ratings (Raw Logs) for ${dateString}`);
|
|
676
623
|
|
|
677
624
|
try {
|
|
678
|
-
const
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
if (!
|
|
682
|
-
logger.log('WARN', `
|
|
683
|
-
return
|
|
625
|
+
const shardsColRef = db.collection('PiReviews').doc(dateString).collection('shards');
|
|
626
|
+
const shardDocs = await withRetry(() => shardsColRef.listDocuments(), `listRatingShards(${dateString})`);
|
|
627
|
+
|
|
628
|
+
if (!shardDocs || shardDocs.length === 0) {
|
|
629
|
+
logger.log('WARN', `No rating shards found for ${dateString} at ${shardsColRef.path}`);
|
|
630
|
+
return {};
|
|
684
631
|
}
|
|
685
|
-
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
const
|
|
689
|
-
|
|
632
|
+
|
|
633
|
+
const rawReviewsByPi = {};
|
|
634
|
+
|
|
635
|
+
for (const docRef of shardDocs) {
|
|
636
|
+
const docSnap = await docRef.get();
|
|
637
|
+
if (!docSnap.exists) continue;
|
|
638
|
+
|
|
639
|
+
const rawData = tryDecompress(docSnap.data());
|
|
640
|
+
|
|
641
|
+
// SCHEMA HANDLING:
|
|
642
|
+
// Keys at the root of the document are the review IDs (e.g. "reviews.29312236_31075566").
|
|
643
|
+
// We iterate over all values and check if they look like review objects.
|
|
644
|
+
|
|
645
|
+
Object.values(rawData).forEach(entry => {
|
|
646
|
+
// Check for valid review object structure
|
|
647
|
+
if (entry && typeof entry === 'object' && entry.piCid && entry.rating !== undefined) {
|
|
648
|
+
|
|
649
|
+
if (!rawReviewsByPi[entry.piCid]) {
|
|
650
|
+
rawReviewsByPi[entry.piCid] = [];
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Store the raw entry directly.
|
|
654
|
+
rawReviewsByPi[entry.piCid].push(entry);
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
logger.log('INFO', `Loaded raw reviews for ${Object.keys(rawReviewsByPi).length} PIs.`);
|
|
660
|
+
return rawReviewsByPi;
|
|
661
|
+
|
|
690
662
|
} catch (error) {
|
|
691
663
|
logger.log('ERROR', `Failed to load PI Ratings for ${dateString}: ${error.message}`);
|
|
692
|
-
return
|
|
664
|
+
return {};
|
|
693
665
|
}
|
|
694
666
|
}
|
|
695
667
|
|
|
@@ -711,9 +683,8 @@ async function loadPIPageViews(config, deps, dateString) {
|
|
|
711
683
|
}
|
|
712
684
|
|
|
713
685
|
const data = tryDecompress(docSnap.data());
|
|
714
|
-
// Remove the date key and lastUpdated, return just the PI data
|
|
715
686
|
const { date, lastUpdated, ...piPageViews } = data;
|
|
716
|
-
return piPageViews;
|
|
687
|
+
return piPageViews;
|
|
717
688
|
} catch (error) {
|
|
718
689
|
logger.log('ERROR', `Failed to load PI Page Views for ${dateString}: ${error.message}`);
|
|
719
690
|
return null;
|
|
@@ -738,9 +709,8 @@ async function loadWatchlistMembership(config, deps, dateString) {
|
|
|
738
709
|
}
|
|
739
710
|
|
|
740
711
|
const data = tryDecompress(docSnap.data());
|
|
741
|
-
// Remove the date key and lastUpdated, return just the PI data
|
|
742
712
|
const { date, lastUpdated, ...watchlistMembership } = data;
|
|
743
|
-
return watchlistMembership;
|
|
713
|
+
return watchlistMembership;
|
|
744
714
|
} catch (error) {
|
|
745
715
|
logger.log('ERROR', `Failed to load Watchlist Membership for ${dateString}: ${error.message}`);
|
|
746
716
|
return null;
|
|
@@ -765,19 +735,15 @@ async function loadPIAlertHistory(config, deps, dateString) {
|
|
|
765
735
|
}
|
|
766
736
|
|
|
767
737
|
const data = tryDecompress(docSnap.data());
|
|
768
|
-
// Remove the date key and lastUpdated, return just the PI data
|
|
769
738
|
const { date, lastUpdated, ...piAlertHistory } = data;
|
|
770
|
-
return piAlertHistory;
|
|
739
|
+
return piAlertHistory;
|
|
771
740
|
} catch (error) {
|
|
772
741
|
logger.log('ERROR', `Failed to load PI Alert History for ${dateString}: ${error.message}`);
|
|
773
742
|
return null;
|
|
774
743
|
}
|
|
775
744
|
}
|
|
776
745
|
|
|
777
|
-
/** Stage 17: Load PI-Centric Watchlist Data
|
|
778
|
-
* Loads watchlist data from PopularInvestors/{piCid}/watchlistData/current
|
|
779
|
-
* This provides time-series data of watchlist additions per PI
|
|
780
|
-
*/
|
|
746
|
+
/** Stage 17: Load PI-Centric Watchlist Data */
|
|
781
747
|
async function loadPIWatchlistData(config, deps, piCid) {
|
|
782
748
|
const { db, logger, calculationUtils } = deps;
|
|
783
749
|
const { withRetry } = calculationUtils;
|
|
@@ -799,19 +765,18 @@ async function loadPIWatchlistData(config, deps, piCid) {
|
|
|
799
765
|
}
|
|
800
766
|
|
|
801
767
|
const data = tryDecompress(docSnap.data());
|
|
802
|
-
return data;
|
|
768
|
+
return data;
|
|
803
769
|
} catch (error) {
|
|
804
770
|
logger.log('ERROR', `Failed to load PI Watchlist Data for PI ${piCidStr}: ${error.message}`);
|
|
805
771
|
return null;
|
|
806
772
|
}
|
|
807
773
|
}
|
|
808
774
|
|
|
809
|
-
//
|
|
775
|
+
// Load Popular Investor Master List
|
|
810
776
|
async function loadPopularInvestorMasterList(config, deps) {
|
|
811
777
|
const { db, logger, calculationUtils } = deps;
|
|
812
778
|
const { withRetry } = calculationUtils;
|
|
813
779
|
|
|
814
|
-
// Default to 'system_state' collection, 'popular_investor_master_list' doc
|
|
815
780
|
const collectionName = config.piMasterListCollection || 'system_state';
|
|
816
781
|
const docId = config.piMasterListDocId || 'popular_investor_master_list';
|
|
817
782
|
|
|
@@ -827,9 +792,6 @@ async function loadPopularInvestorMasterList(config, deps) {
|
|
|
827
792
|
}
|
|
828
793
|
|
|
829
794
|
const data = tryDecompress(docSnap.data());
|
|
830
|
-
// Structure is { investors: { cid: { username, ... } } } or direct map { cid: { ... } }
|
|
831
|
-
// Based on user input, it looks like a direct map of CIDs or a field holding the map.
|
|
832
|
-
// We return the raw object which acts as the map.
|
|
833
795
|
return data.investors || data;
|
|
834
796
|
} catch (error) {
|
|
835
797
|
logger.log('ERROR', `Failed to load PI Master List: ${error.message}`);
|
|
@@ -855,6 +817,6 @@ module.exports = {
|
|
|
855
817
|
loadPIPageViews,
|
|
856
818
|
loadWatchlistMembership,
|
|
857
819
|
loadPIAlertHistory,
|
|
858
|
-
loadPopularInvestorMasterList,
|
|
820
|
+
loadPopularInvestorMasterList,
|
|
859
821
|
loadPIWatchlistData,
|
|
860
822
|
};
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
const crypto = require('crypto');
|
|
8
8
|
const { shouldTryProxy, recordProxyOutcome, getFailureCount, getMaxFailures } = require('../utils/proxy_circuit_breaker');
|
|
9
9
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
10
|
-
const { notifyTaskEngineComplete, notifyTaskEngineProgress, notifyPIDataRefreshed } = require('../../
|
|
10
|
+
const { notifyTaskEngineComplete, notifyTaskEngineProgress, notifyPIDataRefreshed } = require('../../api-v2/helpers/notification_helpers');
|
|
11
11
|
const { conditionallyRunRootDataIndexer } = require('./root_data_indexer_helpers');
|
|
12
12
|
|
|
13
13
|
const {
|
|
@@ -405,7 +405,7 @@ async function finalizeOnDemandRequest(deps, config, taskData, isPI, success, to
|
|
|
405
405
|
// 2. Trigger Computations (only if root data indexer completed successfully)
|
|
406
406
|
if (indexerCompleted && pubsub && config.computationSystem) {
|
|
407
407
|
const { triggerComputationWithDependencies } = require('../../computation-system/helpers/on_demand_helpers');
|
|
408
|
-
const {
|
|
408
|
+
const { fetchPopularInvestorMasterList } = require('../../api-v2/helpers/data-fetchers/firestore.js');
|
|
409
409
|
|
|
410
410
|
// Use userType from metadata if available, otherwise fall back to isPI
|
|
411
411
|
const userType = metadata?.userType || (isPI ? 'POPULAR_INVESTOR' : 'SIGNED_IN_USER');
|
|
@@ -429,11 +429,15 @@ async function finalizeOnDemandRequest(deps, config, taskData, isPI, success, to
|
|
|
429
429
|
|
|
430
430
|
// IMPORTANT: Check if this signed-in user is also a Popular Investor
|
|
431
431
|
// If they are, we need to run PI computations as well
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
432
|
+
try {
|
|
433
|
+
const piData = await fetchPopularInvestorMasterList(db, String(cid));
|
|
434
|
+
if (piData) {
|
|
435
|
+
logger.log('INFO', `[On-Demand] Signed-in user ${cid} is also a Popular Investor. Adding PI computations.`);
|
|
436
|
+
compsSet.add('PopularInvestorProfileMetrics');
|
|
437
|
+
compsSet.add('SignedInUserPIPersonalizedMetrics');
|
|
438
|
+
}
|
|
439
|
+
} catch (e) {
|
|
440
|
+
// User is not a PI, continue with signed-in user computations only
|
|
437
441
|
}
|
|
438
442
|
} else {
|
|
439
443
|
// Fallback to isPI-based logic for backward compatibility
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bulltrackers-module",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.632",
|
|
4
4
|
"description": "Helper Functions for Bulltrackers.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
"functions/task-engine/",
|
|
10
10
|
"functions/core/",
|
|
11
11
|
"functions/computation-system/",
|
|
12
|
-
"functions/old-generic-api/",
|
|
13
12
|
"functions/api-v2/",
|
|
14
13
|
"functions/dispatcher/",
|
|
15
14
|
"functions/invalid-speculator-handler/",
|