bulltrackers-module 1.0.592 → 1.0.593
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/old-generic-api/admin-api/index.js +895 -0
- package/functions/old-generic-api/helpers/api_helpers.js +457 -0
- package/functions/old-generic-api/index.js +204 -0
- package/functions/old-generic-api/user-api/helpers/alerts/alert_helpers.js +355 -0
- package/functions/old-generic-api/user-api/helpers/alerts/subscription_helpers.js +327 -0
- package/functions/old-generic-api/user-api/helpers/alerts/test_alert_helpers.js +212 -0
- package/functions/old-generic-api/user-api/helpers/collection_helpers.js +193 -0
- package/functions/old-generic-api/user-api/helpers/core/compression_helpers.js +68 -0
- package/functions/old-generic-api/user-api/helpers/core/data_lookup_helpers.js +256 -0
- package/functions/old-generic-api/user-api/helpers/core/path_resolution_helpers.js +640 -0
- package/functions/old-generic-api/user-api/helpers/core/user_status_helpers.js +195 -0
- package/functions/old-generic-api/user-api/helpers/data/computation_helpers.js +503 -0
- package/functions/old-generic-api/user-api/helpers/data/instrument_helpers.js +55 -0
- package/functions/old-generic-api/user-api/helpers/data/portfolio_helpers.js +245 -0
- package/functions/old-generic-api/user-api/helpers/data/social_helpers.js +174 -0
- package/functions/old-generic-api/user-api/helpers/data_helpers.js +87 -0
- package/functions/old-generic-api/user-api/helpers/dev/dev_helpers.js +336 -0
- package/functions/old-generic-api/user-api/helpers/fetch/on_demand_fetch_helpers.js +615 -0
- package/functions/old-generic-api/user-api/helpers/metrics/personalized_metrics_helpers.js +231 -0
- package/functions/old-generic-api/user-api/helpers/notifications/notification_helpers.js +641 -0
- package/functions/old-generic-api/user-api/helpers/profile/pi_profile_helpers.js +182 -0
- package/functions/old-generic-api/user-api/helpers/profile/profile_view_helpers.js +137 -0
- package/functions/old-generic-api/user-api/helpers/profile/user_profile_helpers.js +190 -0
- package/functions/old-generic-api/user-api/helpers/recommendations/recommendation_helpers.js +66 -0
- package/functions/old-generic-api/user-api/helpers/reviews/review_helpers.js +550 -0
- package/functions/old-generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers.js +378 -0
- package/functions/old-generic-api/user-api/helpers/search/pi_request_helpers.js +295 -0
- package/functions/old-generic-api/user-api/helpers/search/pi_search_helpers.js +162 -0
- package/functions/old-generic-api/user-api/helpers/sync/user_sync_helpers.js +677 -0
- package/functions/old-generic-api/user-api/helpers/verification/verification_helpers.js +323 -0
- package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_analytics_helpers.js +96 -0
- package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_data_helpers.js +141 -0
- package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_generation_helpers.js +310 -0
- package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_management_helpers.js +829 -0
- package/functions/old-generic-api/user-api/index.js +109 -0
- package/package.json +2 -2
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Logic for User Verification (OTP Bio Check) and On-Demand Data Triggers.
|
|
3
|
+
*/
|
|
4
|
+
const { IntelligentProxyManager } = require('../../../../core/utils/intelligent_proxy_manager');
|
|
5
|
+
const { IntelligentHeaderManager } = require('../../../../core/utils/intelligent_header_manager');
|
|
6
|
+
const { PubSubUtils } = require('../../../../core/utils/pubsub_utils');
|
|
7
|
+
const { FieldValue } = require('@google-cloud/firestore');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 1. Initiate Verification
|
|
11
|
+
* Generates an OTP and returns it to the user to place in their bio.
|
|
12
|
+
*/
|
|
13
|
+
async function initiateVerification(req, res, dependencies, config) {
|
|
14
|
+
const { db, logger } = dependencies;
|
|
15
|
+
const { username } = req.body;
|
|
16
|
+
|
|
17
|
+
// Safety check: ensure config exists
|
|
18
|
+
if (!config) {
|
|
19
|
+
logger?.log('ERROR', '[Verification] Config is undefined or null in initiateVerification');
|
|
20
|
+
return res.status(500).json({ error: "Configuration not initialized." });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { verificationsCollection } = config;
|
|
24
|
+
|
|
25
|
+
// Safety check: ensure required config values exist
|
|
26
|
+
if (!verificationsCollection) {
|
|
27
|
+
logger?.log('ERROR', '[Verification] verificationsCollection is not defined in config');
|
|
28
|
+
return res.status(500).json({ error: "Configuration error: verificationsCollection not initialized." });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!username || typeof username !== 'string') {
|
|
32
|
+
return res.status(400).json({ error: "Invalid username." });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
// Generate a simple 6-char alphanumeric OTP
|
|
37
|
+
const otp = Math.random().toString(36).substring(2, 8).toUpperCase();
|
|
38
|
+
|
|
39
|
+
// Store pending request
|
|
40
|
+
// Key by username to prevent multiple active requests per user
|
|
41
|
+
await db.collection(verificationsCollection).doc(username.toLowerCase()).set({
|
|
42
|
+
username,
|
|
43
|
+
otp,
|
|
44
|
+
status: 'PENDING',
|
|
45
|
+
createdAt: FieldValue.serverTimestamp(),
|
|
46
|
+
expiresAt: new Date(Date.now() + 3600000) // 1 hour expiry
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return res.status(200).json({
|
|
50
|
+
success: true,
|
|
51
|
+
message: "Please place this code in your eToro Bio or Short Bio.",
|
|
52
|
+
otp
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return res.status(500).json({ error: error.message });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 2. Finalize Verification
|
|
62
|
+
* Checks eToro API for the OTP, claims the user, and triggers on-demand fetching.
|
|
63
|
+
*/
|
|
64
|
+
async function finalizeVerification(req, res, dependencies, config) {
|
|
65
|
+
const { db, logger } = dependencies;
|
|
66
|
+
const { username } = req.body;
|
|
67
|
+
|
|
68
|
+
// Safety check: ensure config exists and is an object
|
|
69
|
+
if (!config || typeof config !== 'object') {
|
|
70
|
+
logger.log('ERROR', '[Verification] Config is undefined, null, or not an object', { configType: typeof config, configValue: config });
|
|
71
|
+
return res.status(500).json({ error: "Configuration not initialized." });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Safe destructuring with defaults to prevent ReferenceErrors
|
|
75
|
+
const {
|
|
76
|
+
verificationsCollection = null,
|
|
77
|
+
signedInUsersCollection = null,
|
|
78
|
+
proxyConfig = null,
|
|
79
|
+
headerConfig = null,
|
|
80
|
+
pubsubTopicUserFetch = null,
|
|
81
|
+
pubsubTopicUserFetchOnDemand = null,
|
|
82
|
+
pubsubTopicSocialFetch = null
|
|
83
|
+
} = config || {};
|
|
84
|
+
|
|
85
|
+
// Safety check: ensure required config values exist
|
|
86
|
+
if (!signedInUsersCollection) {
|
|
87
|
+
logger.log('ERROR', '[Verification] signedInUsersCollection is not defined in config', {
|
|
88
|
+
configKeys: Object.keys(config),
|
|
89
|
+
signedInUsersCollection: config.signedInUsersCollection
|
|
90
|
+
});
|
|
91
|
+
return res.status(500).json({ error: "Configuration error: signedInUsersCollection not initialized." });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!verificationsCollection) {
|
|
95
|
+
logger.log('ERROR', '[Verification] verificationsCollection is not defined in config', {
|
|
96
|
+
configKeys: Object.keys(config),
|
|
97
|
+
verificationsCollection: config.verificationsCollection
|
|
98
|
+
});
|
|
99
|
+
return res.status(500).json({ error: "Configuration error: verificationsCollection not initialized." });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Use on-demand topic for user signup (API-triggered)
|
|
103
|
+
const taskEngineTopic = pubsubTopicUserFetchOnDemand || pubsubTopicUserFetch || 'etoro-user-fetch-topic-ondemand';
|
|
104
|
+
|
|
105
|
+
if (!username) return res.status(400).json({ error: "Missing username." });
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const docRef = db.collection(verificationsCollection).doc(username.toLowerCase());
|
|
109
|
+
const doc = await docRef.get();
|
|
110
|
+
|
|
111
|
+
if (!doc.exists) {
|
|
112
|
+
return res.status(404).json({ error: "No pending verification found. Please initiate first." });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const verificationData = doc.data();
|
|
116
|
+
if (verificationData.status === 'VERIFIED') {
|
|
117
|
+
return res.status(200).json({ success: true, message: "User already verified." });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const otp = verificationData.otp;
|
|
121
|
+
|
|
122
|
+
// --- A. Fetch eToro Profile ---
|
|
123
|
+
const proxyManager = new IntelligentProxyManager(db, logger, proxyConfig);
|
|
124
|
+
const headerManager = new IntelligentHeaderManager(db, logger, headerConfig);
|
|
125
|
+
|
|
126
|
+
logger.log('INFO', `[Verification] Fetching profile for ${username} via Proxy/Header managers...`);
|
|
127
|
+
|
|
128
|
+
// Select Header
|
|
129
|
+
const { header } = await headerManager.selectHeader();
|
|
130
|
+
const requestHeaders = {
|
|
131
|
+
'Accept': 'application/json',
|
|
132
|
+
'Referer': 'https://www.etoro.com/',
|
|
133
|
+
...header
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const targetUrl = `https://www.etoro.com/api/logininfo/v1.1/users/${username}`;
|
|
137
|
+
|
|
138
|
+
let profileData = null;
|
|
139
|
+
try {
|
|
140
|
+
const response = await proxyManager.fetch(targetUrl, {
|
|
141
|
+
method: 'GET',
|
|
142
|
+
headers: requestHeaders
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (response.ok) {
|
|
146
|
+
profileData = await response.json();
|
|
147
|
+
} else {
|
|
148
|
+
// [FIX] Fallback to direct fetch with proper error handling
|
|
149
|
+
logger.log('WARN', `[Verification] Proxy failed (${response.status}). Trying direct fetch...`);
|
|
150
|
+
try {
|
|
151
|
+
const directFetch = typeof fetch !== 'undefined' ? fetch : require('node-fetch');
|
|
152
|
+
const directRes = await directFetch(targetUrl, { headers: requestHeaders });
|
|
153
|
+
if (directRes.ok) {
|
|
154
|
+
profileData = await directRes.json();
|
|
155
|
+
} else {
|
|
156
|
+
throw new Error(`eToro API status: ${directRes.status}`);
|
|
157
|
+
}
|
|
158
|
+
} catch (directErr) {
|
|
159
|
+
logger.log('ERROR', `[Verification] Direct fetch also failed for ${username}`, directErr);
|
|
160
|
+
throw new Error(`Failed to fetch eToro profile. Proxy: ${response.status}, Direct: ${directErr.message}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch (fetchErr) {
|
|
164
|
+
logger.log('ERROR', `[Verification] Fetch failed for ${username}`, fetchErr);
|
|
165
|
+
return res.status(502).json({ error: "Failed to connect to eToro. Try again later." });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!profileData || !profileData.realCID) {
|
|
169
|
+
return res.status(404).json({ error: "eToro user not found or invalid response." });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// --- B. Validate OTP ---
|
|
173
|
+
const bio = profileData.userBio?.aboutMe || profileData.aboutMe || "";
|
|
174
|
+
const bioShort = profileData.userBio?.aboutMeShort || profileData.aboutMeShort || "";
|
|
175
|
+
|
|
176
|
+
if (!bio.includes(otp) && !bioShort.includes(otp)) {
|
|
177
|
+
return res.status(400).json({
|
|
178
|
+
success: false,
|
|
179
|
+
error: "OTP not found in Bio.",
|
|
180
|
+
debug: "Ensure the code is saved in your 'About Me' section."
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// --- C. Success: Store & Trigger ---
|
|
185
|
+
const realCID = profileData.realCID;
|
|
186
|
+
const isOptOut = profileData.optOut === true; // Private profile
|
|
187
|
+
|
|
188
|
+
// 1. Mark Verified
|
|
189
|
+
await docRef.update({
|
|
190
|
+
status: 'VERIFIED',
|
|
191
|
+
verifiedAt: FieldValue.serverTimestamp(),
|
|
192
|
+
cid: realCID,
|
|
193
|
+
isOptOut
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// 2. Create/Update User Doc
|
|
197
|
+
await db.collection(signedInUsersCollection).doc(String(realCID)).set({
|
|
198
|
+
username: profileData.username,
|
|
199
|
+
cid: realCID,
|
|
200
|
+
fullName: `${profileData.firstName || ''} ${profileData.lastName || ''}`.trim(),
|
|
201
|
+
avatar: profileData.avatars?.find(a => a.type === 'Original')?.url || null,
|
|
202
|
+
isOptOut,
|
|
203
|
+
verifiedAt: FieldValue.serverTimestamp(),
|
|
204
|
+
lastLogin: FieldValue.serverTimestamp()
|
|
205
|
+
}, { merge: true });
|
|
206
|
+
|
|
207
|
+
// 3. Trigger Downstream Systems via Pub/Sub
|
|
208
|
+
// Send unified request to task engine that handles both portfolio/history AND social data
|
|
209
|
+
const pubsubUtils = new PubSubUtils(dependencies);
|
|
210
|
+
|
|
211
|
+
// Generate a requestId for tracking and to ensure finalizeOnDemandRequest runs
|
|
212
|
+
// This is critical - without requestId, the root data indexer and computations won't be triggered
|
|
213
|
+
const requestId = `signup-${realCID}-${Date.now()}`;
|
|
214
|
+
|
|
215
|
+
// Create request tracking document (similar to user sync requests)
|
|
216
|
+
try {
|
|
217
|
+
const requestRef = db.collection('user_sync_requests')
|
|
218
|
+
.doc(String(realCID))
|
|
219
|
+
.collection('requests')
|
|
220
|
+
.doc(requestId);
|
|
221
|
+
|
|
222
|
+
await requestRef.set({
|
|
223
|
+
targetUserCid: realCID,
|
|
224
|
+
username: profileData.username,
|
|
225
|
+
status: 'pending',
|
|
226
|
+
source: 'user_signup',
|
|
227
|
+
requestedAt: FieldValue.serverTimestamp(),
|
|
228
|
+
createdAt: FieldValue.serverTimestamp(),
|
|
229
|
+
metadata: {
|
|
230
|
+
isNewUser: true,
|
|
231
|
+
verificationSource: 'otp_verification'
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
logger.log('INFO', `[Verification] Created request tracking document for ${username} (${realCID}): ${requestId}`);
|
|
236
|
+
} catch (reqError) {
|
|
237
|
+
logger.log('WARN', `[Verification] Failed to create request tracking document: ${reqError.message}`);
|
|
238
|
+
// Continue anyway - the requestId is still set
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Create a unified on-demand request that includes both portfolio and social
|
|
242
|
+
// The task engine will process both, update root data, then trigger computations
|
|
243
|
+
const unifiedTask = {
|
|
244
|
+
type: 'ON_DEMAND_USER_UPDATE', // Matches Task Engine handler
|
|
245
|
+
cid: realCID, // Top-level CID for handler
|
|
246
|
+
username: profileData.username, // Top-level username for handler
|
|
247
|
+
requestId: requestId, // CRITICAL: Required for finalizeOnDemandRequest to run
|
|
248
|
+
source: 'user_signup', // Mark as signup to ensure computations are triggered
|
|
249
|
+
data: {
|
|
250
|
+
cid: realCID,
|
|
251
|
+
username: profileData.username,
|
|
252
|
+
includeSocial: true, // Flag to include social data fetch
|
|
253
|
+
since: new Date(Date.now() - (7 * 24 * 60 * 60 * 1000)).toISOString() // Last 7 days for social
|
|
254
|
+
},
|
|
255
|
+
metadata: {
|
|
256
|
+
onDemand: true,
|
|
257
|
+
targetCid: realCID, // Optimization: only process this user
|
|
258
|
+
isNewUser: true, // Flag to add to daily update queue
|
|
259
|
+
requestedAt: new Date().toISOString(),
|
|
260
|
+
userType: 'SIGNED_IN_USER' // Explicitly set userType for computation selection
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Only trigger if user is public (has portfolio data)
|
|
265
|
+
if (!isOptOut) {
|
|
266
|
+
await pubsubUtils.publish(taskEngineTopic, unifiedTask);
|
|
267
|
+
logger.log('INFO', `[Verification] Triggered unified data fetch (portfolio + social) for ${username} (${realCID}) via on-demand topic with requestId: ${requestId}`);
|
|
268
|
+
} else {
|
|
269
|
+
// For private users, still fetch social data but no portfolio
|
|
270
|
+
const socialOnlyTask = {
|
|
271
|
+
type: 'ON_DEMAND_USER_UPDATE',
|
|
272
|
+
cid: realCID,
|
|
273
|
+
username: profileData.username,
|
|
274
|
+
requestId: requestId, // CRITICAL: Required for finalizeOnDemandRequest to run
|
|
275
|
+
source: 'user_signup',
|
|
276
|
+
data: {
|
|
277
|
+
cid: realCID,
|
|
278
|
+
username: profileData.username,
|
|
279
|
+
includeSocial: true,
|
|
280
|
+
portfolioOnly: false, // Skip portfolio for private users
|
|
281
|
+
since: new Date(Date.now() - (7 * 24 * 60 * 60 * 1000)).toISOString()
|
|
282
|
+
},
|
|
283
|
+
metadata: {
|
|
284
|
+
onDemand: true,
|
|
285
|
+
targetCid: realCID,
|
|
286
|
+
isNewUser: true,
|
|
287
|
+
requestedAt: new Date().toISOString(),
|
|
288
|
+
userType: 'SIGNED_IN_USER' // Explicitly set userType for computation selection
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
await pubsubUtils.publish(taskEngineTopic, socialOnlyTask);
|
|
292
|
+
logger.log('INFO', `[Verification] Triggered social-only fetch for private user ${username} (${realCID}) via on-demand topic with requestId: ${requestId}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return res.status(200).json({
|
|
296
|
+
success: true,
|
|
297
|
+
message: "Account verified successfully. Data ingestion started.",
|
|
298
|
+
cid: realCID
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
} catch (error) {
|
|
302
|
+
// Enhanced error logging to capture full error details
|
|
303
|
+
const errorDetails = {
|
|
304
|
+
message: error?.message || 'Unknown error',
|
|
305
|
+
stack: error?.stack || 'No stack trace',
|
|
306
|
+
name: error?.name || 'Error',
|
|
307
|
+
configExists: !!config,
|
|
308
|
+
configKeys: config ? Object.keys(config) : [],
|
|
309
|
+
signedInUsersCollection: config?.signedInUsersCollection || 'NOT SET',
|
|
310
|
+
verificationsCollection: config?.verificationsCollection || 'NOT SET'
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
logger.log('ERROR', `[Verification] System error for ${username}`, errorDetails);
|
|
314
|
+
logger.log('ERROR', `[Verification] Full error object:`, error);
|
|
315
|
+
|
|
316
|
+
return res.status(500).json({
|
|
317
|
+
error: error?.message || 'An unexpected error occurred during verification.',
|
|
318
|
+
details: process.env.NODE_ENV === 'development' ? errorDetails : undefined
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
module.exports = { initiateVerification, finalizeVerification };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Watchlist Analytics Helpers
|
|
3
|
+
* Handles watchlist analytics and trigger counts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { Timestamp } = require('@google-cloud/firestore');
|
|
7
|
+
const { readWithMigration } = require('../core/path_resolution_helpers');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* GET /user/me/watchlists/:id/trigger-counts
|
|
11
|
+
* Get alert trigger counts for PIs in a watchlist (last 7 days)
|
|
12
|
+
*/
|
|
13
|
+
async function getWatchlistTriggerCounts(req, res, dependencies, config) {
|
|
14
|
+
const { db, logger } = dependencies;
|
|
15
|
+
const { userCid } = req.query;
|
|
16
|
+
const { id } = req.params;
|
|
17
|
+
|
|
18
|
+
if (!userCid || !id) {
|
|
19
|
+
return res.status(400).json({ error: "Missing userCid or watchlist id" });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
// Read watchlist from new path with migration
|
|
24
|
+
const watchlistResult = await readWithMigration(
|
|
25
|
+
db,
|
|
26
|
+
'signedInUsers',
|
|
27
|
+
'watchlists',
|
|
28
|
+
{ cid: userCid },
|
|
29
|
+
{
|
|
30
|
+
isCollection: false,
|
|
31
|
+
dataType: 'watchlists',
|
|
32
|
+
config,
|
|
33
|
+
logger,
|
|
34
|
+
documentId: id,
|
|
35
|
+
collectionRegistry: dependencies.collectionRegistry
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
let items = [];
|
|
40
|
+
if (watchlistResult && watchlistResult.data) {
|
|
41
|
+
items = watchlistResult.data.items || [];
|
|
42
|
+
} else {
|
|
43
|
+
// Fallback to legacy path
|
|
44
|
+
const watchlistsCollection = config.watchlistsCollection || 'watchlists';
|
|
45
|
+
const watchlistRef = db.collection(watchlistsCollection)
|
|
46
|
+
.doc(String(userCid))
|
|
47
|
+
.collection('lists')
|
|
48
|
+
.doc(id);
|
|
49
|
+
|
|
50
|
+
const watchlistDoc = await watchlistRef.get();
|
|
51
|
+
|
|
52
|
+
if (!watchlistDoc.exists) {
|
|
53
|
+
return res.status(404).json({ error: "Watchlist not found" });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const watchlistData = watchlistDoc.data();
|
|
57
|
+
items = watchlistData.items || [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Calculate date 7 days ago
|
|
61
|
+
const sevenDaysAgo = new Date();
|
|
62
|
+
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
63
|
+
const cutoffTimestamp = Timestamp.fromDate(sevenDaysAgo);
|
|
64
|
+
|
|
65
|
+
const triggerCounts = {};
|
|
66
|
+
const alertTriggersCollection = config.alertTriggersCollection || 'alert_triggers';
|
|
67
|
+
|
|
68
|
+
// For each PI in the watchlist, count triggers in last 7 days
|
|
69
|
+
for (const item of items) {
|
|
70
|
+
const piCid = item.cid;
|
|
71
|
+
const triggersRef = db.collection(alertTriggersCollection)
|
|
72
|
+
.doc(String(userCid))
|
|
73
|
+
.collection('triggers')
|
|
74
|
+
.where('piCid', '==', piCid)
|
|
75
|
+
.where('triggeredAt', '>', cutoffTimestamp);
|
|
76
|
+
|
|
77
|
+
const triggersSnapshot = await triggersRef.get();
|
|
78
|
+
triggerCounts[piCid] = triggersSnapshot.size;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return res.status(200).json({
|
|
82
|
+
triggerCounts,
|
|
83
|
+
watchlistId: id,
|
|
84
|
+
period: '7days'
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
} catch (error) {
|
|
88
|
+
logger.log('ERROR', `[getWatchlistTriggerCounts] Error fetching trigger counts`, error);
|
|
89
|
+
return res.status(500).json({ error: error.message });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
getWatchlistTriggerCounts
|
|
95
|
+
};
|
|
96
|
+
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Legacy Watchlist Data Helpers
|
|
3
|
+
* Handles legacy single watchlist endpoints with migration support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { FieldValue } = require('@google-cloud/firestore');
|
|
7
|
+
const { readWithMigration, writeWithMigration } = require('../core/path_resolution_helpers');
|
|
8
|
+
const { getEffectiveCid } = require('../dev/dev_helpers');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* GET /user/me/watchlist (Legacy)
|
|
12
|
+
* Fetches the user's legacy watchlist
|
|
13
|
+
* Migrates to new path automatically
|
|
14
|
+
*/
|
|
15
|
+
async function getWatchlist(req, res, dependencies, config) {
|
|
16
|
+
const { db, logger } = dependencies;
|
|
17
|
+
const { userCid } = req.query;
|
|
18
|
+
|
|
19
|
+
if (!userCid) {
|
|
20
|
+
return res.status(400).json({ error: "Missing userCid" });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const effectiveCid = await getEffectiveCid(db, userCid, config, logger);
|
|
25
|
+
|
|
26
|
+
// Try new path with migration
|
|
27
|
+
const result = await readWithMigration(
|
|
28
|
+
db,
|
|
29
|
+
'signedInUsers',
|
|
30
|
+
'watchlists',
|
|
31
|
+
{ cid: effectiveCid },
|
|
32
|
+
{
|
|
33
|
+
isCollection: true,
|
|
34
|
+
dataType: 'watchlists',
|
|
35
|
+
config,
|
|
36
|
+
logger,
|
|
37
|
+
collectionRegistry: dependencies.collectionRegistry
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (result && result.snapshot) {
|
|
42
|
+
// Convert snapshot to legacy format for backward compatibility
|
|
43
|
+
const watchlistData = {};
|
|
44
|
+
result.snapshot.forEach(doc => {
|
|
45
|
+
watchlistData[doc.id] = doc.data();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return res.status(200).json({
|
|
49
|
+
watchlist: watchlistData,
|
|
50
|
+
effectiveCid: effectiveCid,
|
|
51
|
+
actualCid: Number(userCid),
|
|
52
|
+
migrated: result.source === 'legacy'
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Fallback to legacy collection
|
|
57
|
+
const watchlistsCollection = config.watchlistsCollection || 'watchlists';
|
|
58
|
+
const watchlistRef = db.collection(watchlistsCollection).doc(String(effectiveCid));
|
|
59
|
+
const watchlistDoc = await watchlistRef.get();
|
|
60
|
+
|
|
61
|
+
if (!watchlistDoc.exists) {
|
|
62
|
+
return res.status(200).json({ watchlist: {} });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const watchlistData = watchlistDoc.data();
|
|
66
|
+
return res.status(200).json({
|
|
67
|
+
watchlist: watchlistData,
|
|
68
|
+
effectiveCid: effectiveCid,
|
|
69
|
+
actualCid: Number(userCid)
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
} catch (error) {
|
|
73
|
+
logger.log('ERROR', `[getWatchlist] Error fetching watchlist for ${userCid}`, error);
|
|
74
|
+
return res.status(500).json({ error: error.message });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* POST /watchlist (Legacy)
|
|
80
|
+
* Adds/Updates a watchlist item
|
|
81
|
+
* Writes to both new and legacy paths during migration
|
|
82
|
+
*/
|
|
83
|
+
async function updateWatchlist(req, res, dependencies, config) {
|
|
84
|
+
const { db, logger } = dependencies;
|
|
85
|
+
const { userCid, type, target, alertConfig } = req.body;
|
|
86
|
+
|
|
87
|
+
if (!userCid || !type || !target) {
|
|
88
|
+
return res.status(400).json({ error: "Invalid payload" });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const effectiveCid = await getEffectiveCid(db, userCid, config, logger);
|
|
93
|
+
|
|
94
|
+
// Generate entry ID
|
|
95
|
+
const entryId = type === 'static'
|
|
96
|
+
? `static_${target}`
|
|
97
|
+
: `dyn_${Buffer.from(JSON.stringify(target)).toString('base64').slice(0,10)}`;
|
|
98
|
+
|
|
99
|
+
const watchlistEntry = {
|
|
100
|
+
type,
|
|
101
|
+
target,
|
|
102
|
+
alertConfig: alertConfig || {},
|
|
103
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Write to new path with dual write
|
|
107
|
+
await writeWithMigration(
|
|
108
|
+
db,
|
|
109
|
+
'signedInUsers',
|
|
110
|
+
'watchlists',
|
|
111
|
+
{ cid: effectiveCid },
|
|
112
|
+
watchlistEntry,
|
|
113
|
+
{
|
|
114
|
+
isCollection: true,
|
|
115
|
+
merge: true,
|
|
116
|
+
dataType: 'watchlists',
|
|
117
|
+
config,
|
|
118
|
+
documentId: entryId,
|
|
119
|
+
dualWrite: true,
|
|
120
|
+
collectionRegistry: dependencies.collectionRegistry
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
return res.status(200).json({
|
|
125
|
+
success: true,
|
|
126
|
+
id: entryId,
|
|
127
|
+
effectiveCid: effectiveCid,
|
|
128
|
+
actualCid: Number(userCid)
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
} catch (error) {
|
|
132
|
+
logger.log('ERROR', `[updateWatchlist] Error updating watchlist for ${userCid}`, error);
|
|
133
|
+
return res.status(500).json({ error: error.message });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = {
|
|
138
|
+
getWatchlist,
|
|
139
|
+
updateWatchlist
|
|
140
|
+
};
|
|
141
|
+
|