bulltrackers-module 1.0.472 → 1.0.474

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.
@@ -16,7 +16,8 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
16
16
  const piUsername = await getPIUsername(db, piCid);
17
17
 
18
18
  // 2. Find all users subscribed to this PI and alert type
19
- const subscriptions = await findSubscriptionsForPI(db, piCid, alertType.id);
19
+ // Use computationName (e.g., 'RiskScoreIncrease') to map to alertConfig keys
20
+ const subscriptions = await findSubscriptionsForPI(db, logger, piCid, alertType.computationName);
20
21
 
21
22
  if (subscriptions.length === 0) {
22
23
  logger.log('INFO', `[processAlertForPI] No subscriptions found for PI ${piCid}, alert type ${alertType.id}`);
@@ -28,37 +29,41 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
28
29
  // 3. Generate alert message
29
30
  const alertMessage = generateAlertMessage(alertType, piUsername, computationMetadata);
30
31
 
31
- // 4. Create alerts for each subscribed user
32
+ // 4. Create notifications for each subscribed user (using user_notifications collection)
32
33
  const batch = db.batch();
33
- const alertRefs = [];
34
+ const notificationRefs = [];
34
35
  const counterUpdates = {};
35
36
 
36
37
  for (const subscription of subscriptions) {
37
- const alertId = `alert_${Date.now()}_${subscription.userCid}_${piCid}`;
38
- const alertRef = db.collection('user_alerts')
38
+ const notificationId = `alert_${Date.now()}_${subscription.userCid}_${piCid}_${Math.random().toString(36).substring(2, 9)}`;
39
+ const notificationRef = db.collection('user_notifications')
39
40
  .doc(String(subscription.userCid))
40
- .collection('alerts')
41
- .doc(alertId);
41
+ .collection('notifications')
42
+ .doc(notificationId);
42
43
 
43
- const alertData = {
44
- id: alertId,
45
- userCid: subscription.userCid,
46
- piCid: piCid,
47
- piUsername: piUsername,
48
- alertType: alertType.id,
49
- alertTypeName: alertType.name,
50
- computationName: alertType.computationName,
51
- computationDate: computationDate,
44
+ const notificationData = {
45
+ id: notificationId,
46
+ type: 'alert',
47
+ title: alertType.name,
52
48
  message: alertMessage,
53
- severity: alertType.severity,
54
49
  read: false,
55
- readAt: null,
56
50
  createdAt: FieldValue.serverTimestamp(),
57
- metadata: computationMetadata || {}
51
+ metadata: {
52
+ piCid: Number(piCid),
53
+ piUsername: piUsername,
54
+ alertType: alertType.id,
55
+ alertTypeName: alertType.name,
56
+ computationName: alertType.computationName,
57
+ computationDate: computationDate,
58
+ severity: alertType.severity,
59
+ watchlistId: subscription.watchlistId,
60
+ watchlistName: subscription.watchlistName,
61
+ ...(computationMetadata || {})
62
+ }
58
63
  };
59
64
 
60
- batch.set(alertRef, alertData);
61
- alertRefs.push(alertRef);
65
+ batch.set(notificationRef, notificationData);
66
+ notificationRefs.push(notificationRef);
62
67
 
63
68
  // Track counter updates
64
69
  const dateKey = computationDate || new Date().toISOString().split('T')[0];
@@ -76,9 +81,9 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
76
81
  (counterUpdates[subscription.userCid].byType[alertType.id] || 0) + 1;
77
82
  }
78
83
 
79
- // 5. Update alert counters
84
+ // 5. Update notification counters
80
85
  for (const [userCid, counter] of Object.entries(counterUpdates)) {
81
- const counterRef = db.collection('user_alerts')
86
+ const counterRef = db.collection('user_notifications')
82
87
  .doc(String(userCid))
83
88
  .collection('counters')
84
89
  .doc(counter.date);
@@ -92,25 +97,10 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
92
97
  }, { merge: true });
93
98
  }
94
99
 
95
- // 6. Update subscription lastAlertAt
96
- for (const subscription of subscriptions) {
97
- const subscriptionRef = db.collection('watchlist_subscriptions')
98
- .doc(String(subscription.userCid))
99
- .collection('alerts')
100
- .doc(String(piCid));
101
-
102
- batch.update(subscriptionRef, {
103
- lastAlertAt: FieldValue.serverTimestamp()
104
- });
105
- }
106
-
107
- // 7. Commit batch
100
+ // 6. Commit batch
108
101
  await batch.commit();
109
102
 
110
- logger.log('SUCCESS', `[processAlertForPI] Created ${alertRefs.length} alerts for PI ${piCid}, alert type ${alertType.id}`);
111
-
112
- // 8. Optionally notify the PI if they're a signed-in user
113
- await notifyPIIfSignedIn(db, logger, piCid, alertType, subscriptions.length);
103
+ logger.log('SUCCESS', `[processAlertForPI] Created ${notificationRefs.length} notifications for PI ${piCid}, alert type ${alertType.id}`);
114
104
 
115
105
  } catch (error) {
116
106
  logger.log('ERROR', `[processAlertForPI] Error processing alert for PI ${piCid}`, error);
@@ -119,35 +109,67 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
119
109
  }
120
110
 
121
111
  /**
122
- * Find all subscriptions for a PI and alert type
112
+ * Find all users who should receive alerts for a PI and alert type
113
+ * Reads from actual watchlist structure: watchlists/{userCid}/lists/{watchlistId}
123
114
  */
124
- async function findSubscriptionsForPI(db, piCid, alertTypeId) {
115
+ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId) {
125
116
  const subscriptions = [];
126
117
 
127
- // Query all users who have subscriptions
128
- const subscriptionsCollection = db.collection('watchlist_subscriptions');
129
- const snapshot = await subscriptionsCollection
130
- .where('piCid', '==', Number(piCid))
131
- .get();
118
+ // Map computation names to watchlist alertConfig keys
119
+ // The alertTypeId is actually the computation name (e.g., 'RiskScoreIncrease')
120
+ const computationToConfigKey = {
121
+ 'RiskScoreIncrease': 'increasedRisk',
122
+ 'SignificantVolatility': 'volatilityChanges',
123
+ 'NewSectorExposure': 'newSector',
124
+ 'PositionInvestedIncrease': 'increasedPositionSize',
125
+ 'NewSocialPost': 'newSocialPost',
126
+ 'NewPositions': 'newPositions' // If this computation exists
127
+ };
132
128
 
133
- for (const doc of snapshot.docs) {
134
- const data = doc.data();
135
- const userCid = data.userCid;
129
+ const configKey = computationToConfigKey[alertTypeId];
130
+ if (!configKey) {
131
+ logger.log('WARN', `[findSubscriptionsForPI] No mapping found for alert type: ${alertTypeId}`);
132
+ return subscriptions;
133
+ }
134
+
135
+ // Get all watchlists
136
+ const watchlistsCollection = db.collection('watchlists');
137
+ const watchlistsSnapshot = await watchlistsCollection.get();
138
+
139
+ for (const userDoc of watchlistsSnapshot.docs) {
140
+ const userCid = Number(userDoc.id);
141
+ const userListsSnapshot = await userDoc.ref.collection('lists').get();
136
142
 
137
- // Check if this user is subscribed to this alert type
138
- if (data.alertTypes && data.alertTypes[alertTypeId] === true) {
139
- // Check thresholds if applicable
140
- if (shouldTriggerAlert(data, alertTypeId)) {
141
- subscriptions.push({
142
- userCid: userCid,
143
- piCid: piCid,
144
- alertTypes: data.alertTypes,
145
- thresholds: data.thresholds || {}
146
- });
143
+ for (const listDoc of userListsSnapshot.docs) {
144
+ const listData = listDoc.data();
145
+
146
+ // Check static watchlists
147
+ if (listData.type === 'static' && listData.items && Array.isArray(listData.items)) {
148
+ for (const item of listData.items) {
149
+ if (Number(item.cid) === Number(piCid)) {
150
+ // Check if this alert type is enabled for this PI in this watchlist
151
+ if (item.alertConfig && item.alertConfig[configKey] === true) {
152
+ subscriptions.push({
153
+ userCid: userCid,
154
+ piCid: piCid,
155
+ piUsername: item.username || `PI-${piCid}`,
156
+ watchlistId: listDoc.id,
157
+ watchlistName: listData.name || 'Unnamed Watchlist',
158
+ alertConfig: item.alertConfig
159
+ });
160
+ break; // Found in this watchlist, no need to check other items
161
+ }
162
+ }
163
+ }
147
164
  }
165
+
166
+ // Check dynamic watchlists - if the PI matches the computation result, check alertConfig
167
+ // Note: Dynamic watchlists are handled separately as they're based on computation results
168
+ // For now, we only process static watchlists here
148
169
  }
149
170
  }
150
171
 
172
+ logger.log('INFO', `[findSubscriptionsForPI] Found ${subscriptions.length} subscriptions for PI ${piCid}, alert type ${alertTypeId}`);
151
173
  return subscriptions;
152
174
  }
153
175
 
@@ -336,10 +358,41 @@ function readComputationResults(docData) {
336
358
  }
337
359
  }
338
360
 
361
+ /**
362
+ * Read computation results, handling sharded data
363
+ */
364
+ async function readComputationResultsWithShards(db, docData, docRef) {
365
+ try {
366
+ // Check if data is sharded
367
+ if (docData._sharded === true && docData._shardCount) {
368
+ // Data is stored in shards - read all shards and merge
369
+ const shardsCol = docRef.collection('_shards');
370
+ const shardsSnapshot = await shardsCol.get();
371
+
372
+ if (!shardsSnapshot.empty) {
373
+ let mergedData = {};
374
+ for (const shardDoc of shardsSnapshot.docs) {
375
+ const shardData = shardDoc.data();
376
+ const decompressed = tryDecompress(shardData);
377
+ Object.assign(mergedData, decompressed);
378
+ }
379
+ return readComputationResults(mergedData);
380
+ }
381
+ }
382
+
383
+ // Data is in the main document (compressed or not)
384
+ return readComputationResults(docData);
385
+ } catch (error) {
386
+ console.error('[readComputationResultsWithShards] Error reading sharded results', error);
387
+ return { cids: [], metadata: {}, perUserData: {} };
388
+ }
389
+ }
390
+
339
391
  module.exports = {
340
392
  processAlertForPI,
341
393
  findSubscriptionsForPI,
342
394
  getPIUsername,
343
- readComputationResults
395
+ readComputationResults,
396
+ readComputationResultsWithShards
344
397
  };
345
398
 
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  const { getAlertTypeByComputation, isAlertComputation } = require('./helpers/alert_type_registry');
7
- const { processAlertForPI, readComputationResults } = require('./helpers/alert_helpers');
7
+ const { processAlertForPI, readComputationResults, readComputationResultsWithShards } = require('./helpers/alert_helpers');
8
8
 
9
9
  /**
10
10
  * Pub/Sub trigger handler for alert generation
@@ -126,13 +126,33 @@ async function handleAlertTrigger(message, context, config, dependencies) {
126
126
  }
127
127
 
128
128
  /**
129
- * Firestore trigger function for alert generation (if using Firestore triggers)
129
+ * Firestore trigger function for alert generation
130
130
  * Triggered when computation results are written to:
131
- * unified_insights/{date}/results/popular-investor/computations/{computationName}
131
+ * unified_insights/{date}/results/{category}/computations/{computationName}
132
+ *
133
+ * Handles sharded data by checking _shards subcollection if _sharded === true
132
134
  */
133
135
  async function handleComputationResultWrite(change, context, config, dependencies) {
134
136
  const { db, logger } = dependencies;
135
- const { date, computationName } = context.params;
137
+
138
+ // Extract path parameters from the document path
139
+ // Path format: unified_insights/{date}/results/{category}/computations/{computationName}
140
+ const resource = context.resource || change.after.ref.path;
141
+ const pathParts = resource.split('/');
142
+
143
+ // Find indices of key path segments
144
+ const dateIndex = pathParts.indexOf('unified_insights') + 1;
145
+ const categoryIndex = pathParts.indexOf('results') + 1;
146
+ const computationIndex = pathParts.indexOf('computations') + 1;
147
+
148
+ if (dateIndex === 0 || categoryIndex === 0 || computationIndex === 0) {
149
+ logger.log('WARN', `[AlertTrigger] Invalid document path format: ${resource}`);
150
+ return;
151
+ }
152
+
153
+ const date = pathParts[dateIndex];
154
+ const category = pathParts[categoryIndex];
155
+ const computationName = pathParts[computationIndex];
136
156
 
137
157
  // Only process if document was created or updated (not deleted)
138
158
  if (!change.after.exists) {
@@ -148,10 +168,16 @@ async function handleComputationResultWrite(change, context, config, dependencie
148
168
  return;
149
169
  }
150
170
 
171
+ // Only process popular-investor category computations
172
+ if (category !== 'popular-investor') {
173
+ logger.log('DEBUG', `[AlertTrigger] Not popular-investor category: ${category}`);
174
+ return;
175
+ }
176
+
151
177
  // If it's PopularInvestorProfileMetrics, check for all-clear notifications only
152
178
  if (isProfileMetrics) {
153
179
  const docData = change.after.data();
154
- const results = readComputationResults(docData);
180
+ const results = await readComputationResultsWithShards(db, docData, change.after.ref);
155
181
  if (results.cids && results.cids.length > 0) {
156
182
  await checkAndSendAllClearNotifications(db, logger, results.cids, date, config);
157
183
  }
@@ -166,9 +192,9 @@ async function handleComputationResultWrite(change, context, config, dependencie
166
192
 
167
193
  logger.log('INFO', `[AlertTrigger] Processing alert computation: ${computationName} for date ${date}`);
168
194
 
169
- // 2. Read and decompress computation results
195
+ // 2. Read and decompress computation results (handling shards)
170
196
  const docData = change.after.data();
171
- const results = readComputationResults(docData);
197
+ const results = await readComputationResultsWithShards(db, docData, change.after.ref);
172
198
 
173
199
  if (!results.cids || results.cids.length === 0) {
174
200
  logger.log('INFO', `[AlertTrigger] No PIs found in computation results for ${computationName}`);
@@ -178,6 +204,7 @@ async function handleComputationResultWrite(change, context, config, dependencie
178
204
  logger.log('INFO', `[AlertTrigger] Found ${results.cids.length} PIs in ${computationName} results`);
179
205
 
180
206
  // 3. Process alerts for each PI
207
+ // Use computationName as the alertTypeId (it maps to alertConfig keys)
181
208
  let processedCount = 0;
182
209
  let errorCount = 0;
183
210
 
@@ -186,7 +213,7 @@ async function handleComputationResultWrite(change, context, config, dependencie
186
213
  // Extract PI-specific metadata if available, otherwise use global metadata
187
214
  const piMetadata = results.perUserData && results.perUserData[piCid]
188
215
  ? results.perUserData[piCid]
189
- : results.metadata || {};
216
+ : results.metadata || results.globalMetadata || {};
190
217
 
191
218
  await processAlertForPI(
192
219
  db,
@@ -206,12 +233,6 @@ async function handleComputationResultWrite(change, context, config, dependencie
206
233
 
207
234
  logger.log('SUCCESS', `[AlertTrigger] Completed processing ${computationName}: ${processedCount} successful, ${errorCount} errors`);
208
235
 
209
- // After processing alerts, check for "all clear" notifications for static watchlists
210
- if (processedCount > 0 || results.cids.length > 0) {
211
- // Check if any PIs were processed but didn't trigger alerts
212
- await checkAndSendAllClearNotifications(db, logger, results.cids, date, config);
213
- }
214
-
215
236
  } catch (error) {
216
237
  logger.log('ERROR', `[AlertTrigger] Fatal error processing ${computationName}`, error);
217
238
  // Don't throw - we don't want to retry the entire computation write
@@ -238,7 +259,7 @@ async function checkAndSendAllClearNotifications(db, logger, processedPICids, co
238
259
  const usersToNotify = new Map(); // userCid -> Map of piCid -> {username, ...}
239
260
 
240
261
  for (const userDoc of watchlistsSnapshot.docs) {
241
- const userCid = userDoc.id;
262
+ const userCid = Number(userDoc.id);
242
263
  const userListsSnapshot = await userDoc.ref.collection('lists').get();
243
264
 
244
265
  for (const listDoc of userListsSnapshot.docs) {
@@ -253,8 +274,8 @@ async function checkAndSendAllClearNotifications(db, logger, processedPICids, co
253
274
  for (const item of listData.items) {
254
275
  const piCid = Number(item.cid);
255
276
  if (processedPICids.includes(piCid)) {
256
- // Check if this PI triggered any alerts today
257
- const hasAlerts = await checkIfPIHasAlertsToday(db, userCid, piCid, today);
277
+ // Check if this PI triggered any alerts today for this user
278
+ const hasAlerts = await checkIfPIHasAlertsToday(db, logger, userCid, piCid, today);
258
279
 
259
280
  if (!hasAlerts) {
260
281
  // No alerts triggered - send "all clear" notification
@@ -292,28 +313,28 @@ async function checkAndSendAllClearNotifications(db, logger, processedPICids, co
292
313
 
293
314
  /**
294
315
  * Check if a PI has any alerts for a user today
316
+ * Checks user_notifications collection for alert-type notifications
295
317
  */
296
- async function checkIfPIHasAlertsToday(db, userCid, piCid, date) {
318
+ async function checkIfPIHasAlertsToday(db, logger, userCid, piCid, date) {
297
319
  try {
298
- const alertsRef = db.collection('user_alerts')
320
+ const notificationsRef = db.collection('user_notifications')
299
321
  .doc(String(userCid))
300
- .collection('alerts');
301
-
302
- // Check if there are any alerts for this PI today
303
- const todayStart = new Date(date);
304
- todayStart.setHours(0, 0, 0, 0);
305
- const todayEnd = new Date(date);
306
- todayEnd.setHours(23, 59, 59, 999);
322
+ .collection('notifications');
307
323
 
308
- const snapshot = await alertsRef
309
- .where('piCid', '==', Number(piCid))
310
- .where('computationDate', '==', date)
324
+ // Check if there are any alert notifications for this PI today
325
+ const snapshot = await notificationsRef
326
+ .where('type', '==', 'alert')
327
+ .where('metadata.piCid', '==', Number(piCid))
328
+ .where('metadata.computationDate', '==', date)
311
329
  .limit(1)
312
330
  .get();
313
331
 
314
332
  return !snapshot.empty;
315
333
  } catch (error) {
316
- // If we can't check, assume there are alerts (safer)
334
+ if (logger) {
335
+ logger.log('WARN', `[checkIfPIHasAlertsToday] Error checking alerts for user ${userCid}, PI ${piCid}: ${error.message}`);
336
+ }
337
+ // If we can't check, assume there are alerts (safer - won't send false all-clear)
317
338
  return true;
318
339
  }
319
340
  }
@@ -202,15 +202,10 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
202
202
  await updateComputationStatus(dStr, successUpdates, config, deps);
203
203
  }
204
204
 
205
- // Flush Alert Triggers to Pub/Sub
205
+ // Alert triggers are now handled via Firestore triggers
206
+ // No need to publish Pub/Sub messages - the Firestore write itself triggers the alert system
206
207
  if (alertTriggers.length > 0) {
207
- const topicName = config.alertTopicName || 'alert-trigger';
208
- try {
209
- await pubSubUtils.publishMessageBatch(topicName, alertTriggers);
210
- logger.log('INFO', `[Alert System] Triggered ${alertTriggers.length} alerts to ${topicName}`);
211
- } catch (alertErr) {
212
- logger.log('ERROR', `[Alert System] Failed to publish alerts: ${alertErr.message}`);
213
- }
208
+ logger.log('INFO', `[Alert System] ${alertTriggers.length} alert computations written to Firestore - triggers will fire automatically`);
214
209
  }
215
210
 
216
211
  return { successUpdates, failureReport, shardIndexes: nextShardIndexes };
@@ -1,5 +1,6 @@
1
- # Data Feeder Pipeline (V2.3 - Try/Retry Syntax Fixed)
2
- # Orchestrates data fetching with UTC-alignment, Test Mode, and Reliability.
1
+ # Data Feeder Pipeline (V3.1 - Syntax Fixed)
2
+ # Starts at 22:00 UTC via Cloud Scheduler.
3
+ # Fixes: Split assign/call steps and corrected assign syntax.
3
4
 
4
5
  main:
5
6
  params: [input]
@@ -8,17 +9,8 @@ main:
8
9
  assign:
9
10
  - project: '${sys.get_env("GOOGLE_CLOUD_PROJECT_ID")}'
10
11
  - location: "europe-west1"
11
- - market_date: '${text.split(time.format(sys.now()), "T")[0]}'
12
- # Define a central retry policy to reuse across all HTTP calls
13
- - default_retry:
14
- predicate: ${http.default_retry_predicate}
15
- max_retries: 5
16
- backoff:
17
- initial_delay: 2
18
- max_delay: 60
19
- multiplier: 2
20
-
21
- # --- TEST MODE / SELECTIVE EXECUTION ---
12
+
13
+ # --- TEST MODE ---
22
14
  - check_test_mode:
23
15
  switch:
24
16
  - condition: '${input != null and "target_step" in input}'
@@ -26,130 +18,164 @@ main:
26
18
  - route_test:
27
19
  switch:
28
20
  - condition: '${input.target_step == "market"}'
29
- next: run_market_close_tasks
30
- - condition: '${input.target_step == "rankings"}'
31
- next: run_rankings_fetch
21
+ next: phase_2200_price
22
+ - condition: '${input.target_step == "midnight"}'
23
+ next: phase_0000_rankings
32
24
  - condition: '${input.target_step == "social"}'
33
- next: run_social_midnight
34
- - condition: '${input.target_step == "global"}'
35
- next: run_global_indexer
36
-
37
- # --- PHASE 1: MARKET CLOSE (22:00 UTC) ---
38
- - run_market_close_tasks:
39
- parallel:
40
- branches:
41
- - price_fetch:
42
- steps:
43
- - call_price_fetcher:
44
- try:
45
- call: http.post
46
- args:
47
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/price-fetcher"}'
48
- auth: { type: OIDC }
49
- timeout: 300
50
- retry: ${default_retry} # Fixed: Moved retry to a Try Step
51
- - insights_fetch:
52
- steps:
53
- - call_insights_fetcher:
54
- try:
55
- call: http.post
56
- args:
57
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/insights-fetcher"}'
58
- auth: { type: OIDC }
59
- timeout: 300
60
- retry: ${default_retry} # Fixed: Moved retry to a Try Step
61
-
62
- - index_market_data:
25
+ next: social_loop_start
26
+
27
+ # ==========================================
28
+ # PHASE 1: MARKET CLOSE (Starts 22:00 UTC)
29
+ # ==========================================
30
+
31
+ - phase_2200_price:
63
32
  try:
64
33
  call: http.post
65
34
  args:
66
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
67
- body:
68
- targetDate: '${market_date}'
35
+ url: '${"https://" + location + "-" + project + ".cloudfunctions.net/price-fetcher"}'
69
36
  auth: { type: OIDC }
70
37
  timeout: 300
71
- retry: ${default_retry}
38
+ except:
39
+ as: e
40
+ steps:
41
+ - log_price_error:
42
+ call: sys.log
43
+ args: { severity: "WARNING", text: "Price fetch timed out/failed. Proceeding." }
44
+
45
+ - wait_10_after_price:
46
+ call: sys.sleep
47
+ args: { seconds: 600 } # 10 Minutes
72
48
 
73
- # --- PHASE 2: ALIGN TO MIDNIGHT ---
74
- - wait_for_midnight:
49
+ # FIX 1: Split assign and call
50
+ - prepare_index_price:
75
51
  assign:
76
- - now_sec: '${int(sys.now())}'
77
- - day_sec: 86400
78
- - sleep_midnight: '${day_sec - (now_sec % day_sec)}'
79
- - do_midnight_sleep:
80
- call: sys.sleep
52
+ - today: '${text.split(time.format(sys.now()), "T")[0]}'
53
+ - index_today_after_price:
54
+ call: http.post
81
55
  args:
82
- seconds: '${sleep_midnight}'
56
+ url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
57
+ body: { targetDate: '${today}' }
58
+ auth: { type: OIDC }
83
59
 
84
- # --- PHASE 3: RANKINGS & INITIAL SOCIAL (00:00 UTC) ---
85
- - run_rankings_fetch:
60
+ - wait_10_before_insights:
61
+ call: sys.sleep
62
+ args: { seconds: 600 }
63
+
64
+ - phase_2200_insights:
86
65
  try:
87
66
  call: http.post
88
67
  args:
89
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/fetch-popular-investors"}'
68
+ url: '${"https://" + location + "-" + project + ".cloudfunctions.net/insights-fetcher"}'
90
69
  auth: { type: OIDC }
91
70
  timeout: 300
92
- retry: ${default_retry}
93
71
  except:
94
72
  as: e
95
73
  steps:
96
- - log_rankings_error:
74
+ - log_insights_error:
97
75
  call: sys.log
98
- args:
99
- severity: "ERROR"
100
- text: '${"Rankings Fetch Failed: " + json.encode(e)}'
76
+ args: { severity: "WARNING", text: "Insights fetch timed out/failed. Proceeding." }
101
77
 
102
- - run_social_midnight:
103
- try:
104
- call: http.post
105
- args:
106
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/social-orchestrator"}'
107
- auth: { type: OIDC }
108
- timeout: 300
109
- retry: ${default_retry}
78
+ - wait_10_after_insights:
79
+ call: sys.sleep
80
+ args: { seconds: 600 }
110
81
 
111
- - prepare_midnight_index:
82
+ # FIX 2: Split assign and call
83
+ - prepare_index_insights:
112
84
  assign:
113
- - current_date: '${text.split(time.format(sys.now()), "T")[0]}'
114
- - index_midnight_data:
85
+ - today: '${text.split(time.format(sys.now()), "T")[0]}'
86
+ - index_today_after_insights:
87
+ call: http.post
88
+ args:
89
+ url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
90
+ body: { targetDate: '${today}' }
91
+ auth: { type: OIDC }
92
+
93
+ # ==========================================
94
+ # PHASE 2: WAIT FOR MIDNIGHT
95
+ # ==========================================
96
+
97
+ - align_to_midnight:
98
+ assign:
99
+ - now_sec: '${int(sys.now())}'
100
+ - day_sec: 86400
101
+ - sleep_midnight: '${day_sec - (now_sec % day_sec)}'
102
+ - wait_for_midnight:
103
+ call: sys.sleep
104
+ args: { seconds: '${sleep_midnight}' }
105
+
106
+ # ==========================================
107
+ # PHASE 3: MIDNIGHT TASKS (00:00 UTC)
108
+ # ==========================================
109
+
110
+ - phase_0000_rankings:
115
111
  try:
116
112
  call: http.post
117
113
  args:
118
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
119
- body:
120
- targetDate: '${current_date}'
114
+ url: '${"https://" + location + "-" + project + ".cloudfunctions.net/fetch-popular-investors"}'
121
115
  auth: { type: OIDC }
122
116
  timeout: 300
123
- retry: ${default_retry}
117
+ except:
118
+ as: e
119
+ steps:
120
+ - log_ranking_error:
121
+ call: sys.log
122
+ args: { severity: "WARNING", text: "Rankings failed. Proceeding to Social (risky)." }
124
123
 
125
- - run_global_indexer:
124
+ - wait_10_after_rankings:
125
+ call: sys.sleep
126
+ args: { seconds: 600 }
127
+
128
+ # FIX 3: Split assign and call
129
+ - prepare_index_rankings:
130
+ assign:
131
+ - today: '${text.split(time.format(sys.now()), "T")[0]}'
132
+ - index_today_after_rankings:
133
+ call: http.post
134
+ args:
135
+ url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
136
+ body: { targetDate: '${today}' }
137
+ auth: { type: OIDC }
138
+
139
+ - phase_0000_social:
126
140
  try:
127
141
  call: http.post
128
142
  args:
129
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
143
+ url: '${"https://" + location + "-" + project + ".cloudfunctions.net/social-orchestrator"}'
130
144
  auth: { type: OIDC }
131
145
  timeout: 300
132
- retry: ${default_retry}
146
+ except:
147
+ as: e
148
+ steps:
149
+ - log_social_error:
150
+ call: sys.log
151
+ args: { severity: "WARNING", text: "Social failed. Proceeding." }
152
+
153
+ - wait_10_after_social:
154
+ call: sys.sleep
155
+ args: { seconds: 600 }
156
+
157
+ - global_index_midnight:
158
+ call: http.post
159
+ args:
160
+ url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
161
+ # No targetDate = Global Run
162
+ auth: { type: OIDC }
163
+
164
+ # ==========================================
165
+ # PHASE 4: SOCIAL LOOP (Every 3 Hours)
166
+ # ==========================================
133
167
 
134
- # --- PHASE 4: RECURRING SOCIAL FETCH (UTC Aligned 3hr) ---
135
168
  - init_social_loop:
136
169
  assign:
137
170
  - i: 0
138
171
 
139
- - social_loop:
172
+ - social_loop_start:
140
173
  switch:
141
- - condition: ${i < 7}
174
+ - condition: ${i < 7} # Covers the remainder of the 24h cycle
142
175
  steps:
143
- - calculate_next_window:
144
- assign:
145
- - now_sec_loop: '${int(sys.now())}'
146
- - window_size: 10800
147
- - sleep_loop: '${window_size - (now_sec_loop % window_size)}'
148
-
149
- - wait_for_3hr_window:
176
+ - wait_3_hours:
150
177
  call: sys.sleep
151
- args:
152
- seconds: '${sleep_loop}'
178
+ args: { seconds: 10800 }
153
179
 
154
180
  - run_social_recurring:
155
181
  try:
@@ -158,27 +184,34 @@ main:
158
184
  url: '${"https://" + location + "-" + project + ".cloudfunctions.net/social-orchestrator"}'
159
185
  auth: { type: OIDC }
160
186
  timeout: 300
161
- retry: ${default_retry}
187
+ except:
188
+ as: e
189
+ steps:
190
+ - log_loop_social_warn:
191
+ call: sys.log
192
+ args: { severity: "WARNING", text: "Loop Social timed out. Proceeding." }
162
193
 
163
- - prepare_recurring_index:
194
+ - wait_10_in_loop:
195
+ call: sys.sleep
196
+ args: { seconds: 600 }
197
+
198
+ # FIX 4: Split assign and call
199
+ - prepare_index_loop:
164
200
  assign:
165
- - cur_date_rec: '${text.split(time.format(sys.now()), "T")[0]}'
166
- - index_recurring:
167
- try:
168
- call: http.post
169
- args:
170
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
171
- body:
172
- targetDate: '${cur_date_rec}'
173
- auth: { type: OIDC }
174
- timeout: 300
175
- retry: ${default_retry}
201
+ - today: '${text.split(time.format(sys.now()), "T")[0]}'
202
+ - index_today_in_loop:
203
+ call: http.post
204
+ args:
205
+ url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
206
+ body: { targetDate: '${today}' }
207
+ auth: { type: OIDC }
176
208
 
177
- - increment:
178
- assign:
179
- - i: ${i + 1}
209
+ # FIX 5: Correct assign syntax (must be a list)
210
+ - increment_loop:
211
+ assign:
212
+ - i: '${i + 1}'
180
213
  - next_iteration:
181
- next: social_loop
214
+ next: social_loop_start
182
215
 
183
216
  - finish:
184
- return: "Daily Data Pipeline Completed"
217
+ return: "Complete 24h Cycle Finished"
package/index.js CHANGED
@@ -57,7 +57,7 @@ const { runRootDataIndexer } = require('./functions
57
57
  const { runPopularInvestorFetch } = require('./functions/fetch-popular-investors/index');
58
58
 
59
59
  // Alert System
60
- const { handleAlertTrigger, handleComputationResultWrite } = require('./functions/alert-system/index');
60
+ const { handleAlertTrigger, handleComputationResultWrite, checkAndSendAllClearNotifications } = require('./functions/alert-system/index');
61
61
 
62
62
  // Proxy
63
63
  const { handlePost } = require('./functions/appscript-api/index');
@@ -126,8 +126,9 @@ const maintenance = {
126
126
  const proxy = { handlePost };
127
127
 
128
128
  const alertSystem = {
129
- handleAlertTrigger,
130
- handleComputationResultWrite
129
+ handleAlertTrigger, // Pub/Sub handler (kept for backward compatibility, but not used)
130
+ handleComputationResultWrite, // Firestore trigger handler - main entry point
131
+ checkAndSendAllClearNotifications
131
132
  };
132
133
 
133
134
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.472",
3
+ "version": "1.0.474",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [