bulltrackers-module 1.0.795 → 1.0.797

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.
Files changed (27) hide show
  1. package/functions/alert-system-v3/adapters/MembershipRepo.js +27 -0
  2. package/functions/alert-system-v3/adapters/NotificationBus.js +34 -0
  3. package/functions/alert-system-v3/adapters/UserConfigRepo.js +104 -0
  4. package/functions/alert-system-v3/config/defaults.js +14 -0
  5. package/functions/alert-system-v3/core/AlertContext.js +34 -0
  6. package/functions/alert-system-v3/core/AlertLogic.js +39 -0
  7. package/functions/alert-system-v3/core/ComputationResults.js +99 -0
  8. package/functions/alert-system-v3/handlers/history.js +40 -0
  9. package/functions/alert-system-v3/handlers/ingest.js +35 -0
  10. package/functions/alert-system-v3/handlers/notifier.js +53 -0
  11. package/functions/alert-system-v3/handlers/router.js +118 -0
  12. package/functions/alert-system-v3/index.js +13 -0
  13. package/functions/alert-system-v3/tests/e2e/AlertSystemE2E.test.js +190 -0
  14. package/functions/alert-system-v3/tests/e2e/seed_emulator.js +51 -0
  15. package/functions/alert-system-v3/tests/unit/AlertLogic.test.js +59 -0
  16. package/functions/computation-system-v3/computations/index.js +31 -33
  17. package/functions/core/utils/fcm_utils.js +4 -2
  18. package/index.js +10 -4
  19. package/package.json +3 -4
  20. package/functions/alert-system/helpers/alert_helpers.js +0 -726
  21. package/functions/alert-system/helpers/alert_manifest_loader.js +0 -157
  22. package/functions/alert-system/helpers/dynamic_evaluator.js +0 -279
  23. package/functions/alert-system/index.js +0 -441
  24. package/functions/alert-system/tests/stage1-alert-manifest.test.js +0 -94
  25. package/functions/alert-system/tests/stage2-alert-metadata.test.js +0 -93
  26. package/functions/alert-system/tests/stage3-alert-handler.test.js +0 -79
  27. package/functions/computation-system-v3/computations/RecipeAudit.report.md +0 -38
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ class MembershipRepo {
4
+ constructor(dependencies, config) {
5
+ this.db = dependencies.db;
6
+ this.logger = dependencies.logger;
7
+ this.config = config;
8
+ }
9
+
10
+ async getUserCidsForPi(date, piCid) {
11
+ if (!date || piCid == null) return [];
12
+ const col = this.config.collections.watchlistMembership;
13
+ try {
14
+ const doc = await this.db.collection(col).doc(date).get();
15
+ if (!doc.exists) return [];
16
+ const data = doc.data();
17
+ const entry = data[String(piCid)] || data[Number(piCid)];
18
+ if (!entry || !Array.isArray(entry.users)) return [];
19
+ return entry.users.map(cid => String(cid));
20
+ } catch (error) {
21
+ this.logger?.log('WARN', `[MembershipRepo] Failed to read membership for ${piCid} on ${date}: ${error.message}`);
22
+ return [];
23
+ }
24
+ }
25
+ }
26
+
27
+ module.exports = { MembershipRepo };
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+
3
+ const { PubSubUtils } = require('../../core/utils/pubsub_utils');
4
+
5
+ class NotificationBus {
6
+ constructor(dependencies, config) {
7
+ this.pubsubUtils = dependencies.pubsubUtils || new PubSubUtils(dependencies);
8
+ this.config = config;
9
+ }
10
+
11
+ async publishRawEvent(payload) {
12
+ return this.pubsubUtils.publish(this.config.topics.raw, payload);
13
+ }
14
+
15
+ async publishNotifications(notifications) {
16
+ return this.pubsubUtils.batchPublishTasks({
17
+ topicName: this.config.topics.notify,
18
+ tasks: notifications,
19
+ taskType: 'alert-notifications',
20
+ maxPubsubBatchSize: 200
21
+ });
22
+ }
23
+
24
+ async publishHistoryEvents(events) {
25
+ return this.pubsubUtils.batchPublishTasks({
26
+ topicName: this.config.topics.history,
27
+ tasks: events,
28
+ taskType: 'alert-history',
29
+ maxPubsubBatchSize: 200
30
+ });
31
+ }
32
+ }
33
+
34
+ module.exports = { NotificationBus };
@@ -0,0 +1,104 @@
1
+ 'use strict';
2
+
3
+ const { isDeveloper, manageNotificationPreferences, fetchPopularInvestorMasterList } = require('../../api-v2/helpers/data-fetchers/firestore');
4
+
5
+ class UserConfigRepo {
6
+ constructor(dependencies, config) {
7
+ this.db = dependencies.db;
8
+ this.logger = dependencies.logger;
9
+ this.config = config;
10
+ }
11
+
12
+ async getSubscriptionsForPi({ piCid, userCids, alertType }) {
13
+ const subscriptions = [];
14
+ if (!piCid || !Array.isArray(userCids) || userCids.length === 0) return subscriptions;
15
+
16
+ const piUsername = await this._resolvePiUsername(piCid);
17
+
18
+ for (const userCidStr of userCids) {
19
+ const userCid = Number(userCidStr);
20
+ const watchlistsSnap = await this.db
21
+ .collection(this.config.collections.signedInUsers)
22
+ .doc(String(userCid))
23
+ .collection('watchlists')
24
+ .get();
25
+
26
+ if (watchlistsSnap.empty) continue;
27
+
28
+ for (const doc of watchlistsSnap.docs) {
29
+ const watchlist = { id: doc.id, ...doc.data() };
30
+ if (watchlist.type !== 'static' || !Array.isArray(watchlist.items)) continue;
31
+
32
+ for (const item of watchlist.items) {
33
+ if (Number(item.cid) !== Number(piCid)) continue;
34
+
35
+ if (alertType && alertType.isTest) {
36
+ const prefs = await this._getPrefs(userCid);
37
+ if (!prefs || prefs.testAlerts !== true) continue;
38
+ }
39
+
40
+ subscriptions.push({
41
+ userCid,
42
+ piCid: Number(piCid),
43
+ piUsername: item.username || piUsername,
44
+ watchlistId: watchlist.id,
45
+ watchlistName: watchlist.name || 'Unnamed Watchlist',
46
+ alertConfig: item.alertConfig || {},
47
+ useDynamic: item.useDynamic || {},
48
+ dynamicConfig: item.dynamicConfig || {}
49
+ });
50
+ break;
51
+ }
52
+ }
53
+ }
54
+
55
+ await this._appendDevOverrides(subscriptions, piCid, piUsername, alertType);
56
+ return subscriptions;
57
+ }
58
+
59
+ async _getPrefs(userCid) {
60
+ try {
61
+ return await manageNotificationPreferences(this.db, userCid, 'get');
62
+ } catch (e) {
63
+ this.logger?.log('WARN', `[UserConfigRepo] Prefs read failed for ${userCid}: ${e.message}`);
64
+ return null;
65
+ }
66
+ }
67
+
68
+ async _resolvePiUsername(piCid) {
69
+ try {
70
+ const data = await fetchPopularInvestorMasterList(this.db, String(piCid));
71
+ if (data && data.username) return data.username;
72
+ } catch (e) {}
73
+ return `PI-${piCid}`;
74
+ }
75
+
76
+ async _appendDevOverrides(subscriptions, piCid, piUsername, alertType) {
77
+ try {
78
+ const devOverridesSnap = await this.db.collection(this.config.collections.devOverrides).get();
79
+ for (const doc of devOverridesSnap.docs) {
80
+ const userCid = Number(doc.id);
81
+ const isDev = await isDeveloper(this.db, String(userCid));
82
+ if (!isDev) continue;
83
+
84
+ const data = doc.data();
85
+ if (data.enabled !== true || data.pretendSubscribedToAllAlerts !== true) continue;
86
+
87
+ subscriptions.push({
88
+ userCid,
89
+ piCid: Number(piCid),
90
+ piUsername,
91
+ watchlistId: 'dev-override-all-alerts',
92
+ watchlistName: 'Dev Override - All Alerts',
93
+ alertConfig: { [alertType.configKey]: true },
94
+ useDynamic: {},
95
+ dynamicConfig: {}
96
+ });
97
+ }
98
+ } catch (e) {
99
+ this.logger?.log('WARN', `[UserConfigRepo] Dev override check failed: ${e.message}`);
100
+ }
101
+ }
102
+ }
103
+
104
+ module.exports = { UserConfigRepo };
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ topics: {
5
+ raw: process.env.ALERT_RAW_TOPIC || 'alert-raw-topic',
6
+ notify: process.env.ALERT_NOTIFY_TOPIC || 'alert-notify-topic',
7
+ history: process.env.ALERT_HISTORY_TOPIC || 'alert-history-topic'
8
+ },
9
+ collections: {
10
+ watchlistMembership: 'WatchlistMembershipData',
11
+ signedInUsers: 'SignedInUsers',
12
+ devOverrides: 'dev_overrides'
13
+ }
14
+ };
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+
3
+ const { PubSubUtils } = require('../../core/utils/pubsub_utils');
4
+ const { MembershipRepo } = require('../adapters/MembershipRepo');
5
+ const { UserConfigRepo } = require('../adapters/UserConfigRepo');
6
+ const { NotificationBus } = require('../adapters/NotificationBus');
7
+ const defaults = require('../config/defaults');
8
+
9
+ function createAlertContext(dependencies, overrideConfig = {}) {
10
+ const config = {
11
+ ...defaults,
12
+ ...overrideConfig,
13
+ topics: { ...defaults.topics, ...(overrideConfig.topics || {}) },
14
+ collections: { ...defaults.collections, ...(overrideConfig.collections || {}) }
15
+ };
16
+
17
+ const pubsubUtils = new PubSubUtils(dependencies);
18
+ const membershipRepo = new MembershipRepo(dependencies, config);
19
+ const userConfigRepo = new UserConfigRepo(dependencies, config);
20
+ const notificationBus = new NotificationBus({ ...dependencies, pubsubUtils }, config);
21
+
22
+ return {
23
+ ...dependencies,
24
+ config,
25
+ pubsubUtils,
26
+ membershipRepo,
27
+ userConfigRepo,
28
+ notificationBus
29
+ };
30
+ }
31
+
32
+ module.exports = {
33
+ createAlertContext
34
+ };
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ const { evaluateDynamicConditions } = require('../../alert-system/helpers/dynamic_evaluator');
4
+
5
+ function evaluateAlertForSubscription({ alertType, piResult, subscription, isDeveloper, logger }) {
6
+ if (!alertType || !subscription) {
7
+ return { shouldNotify: false, reason: 'missing-input', mode: 'invalid' };
8
+ }
9
+
10
+ const configKey = alertType.configKey;
11
+ const isEnabled = !!(subscription.alertConfig && configKey && subscription.alertConfig[configKey] === true);
12
+ if (!isEnabled) {
13
+ return { shouldNotify: false, reason: 'disabled', mode: 'static' };
14
+ }
15
+
16
+ const useDynamic = !!(subscription.useDynamic && configKey && subscription.useDynamic[configKey] === true);
17
+ const dynamicConfig = subscription.dynamicConfig && configKey ? subscription.dynamicConfig[configKey] : null;
18
+
19
+ const evaluation = evaluateDynamicConditions(
20
+ alertType,
21
+ piResult || {},
22
+ dynamicConfig || {},
23
+ useDynamic,
24
+ !!isDeveloper,
25
+ logger
26
+ );
27
+
28
+ return {
29
+ shouldNotify: evaluation.passes === true,
30
+ reason: evaluation.reason,
31
+ mode: evaluation.mode || (useDynamic ? 'dynamic' : 'static'),
32
+ effectiveConfig: evaluation.effectiveConfig || null,
33
+ developerBypass: evaluation.developerBypass === true
34
+ };
35
+ }
36
+
37
+ module.exports = {
38
+ evaluateAlertForSubscription
39
+ };
@@ -0,0 +1,99 @@
1
+ 'use strict';
2
+
3
+ const zlib = require('zlib');
4
+ const { Storage } = require('@google-cloud/storage');
5
+
6
+ const storage = new Storage();
7
+
8
+ function tryDecompress(data) {
9
+ if (data && data._compressed === true && data.payload) {
10
+ try {
11
+ let buffer;
12
+ if (Buffer.isBuffer(data.payload)) {
13
+ buffer = data.payload;
14
+ } else if (typeof data.payload === 'string') {
15
+ buffer = Buffer.from(data.payload, 'base64');
16
+ } else {
17
+ buffer = Buffer.from(data.payload);
18
+ }
19
+ const decompressed = zlib.gunzipSync(buffer);
20
+ const jsonString = decompressed.toString('utf8');
21
+ const parsed = JSON.parse(jsonString);
22
+ return typeof parsed === 'string' ? JSON.parse(parsed) : parsed;
23
+ } catch (e) {
24
+ return {};
25
+ }
26
+ }
27
+ return data;
28
+ }
29
+
30
+ function readComputationResults(docData) {
31
+ const decompressed = tryDecompress(docData);
32
+ if (decompressed && typeof decompressed === 'object') {
33
+ if (Array.isArray(decompressed.cids)) {
34
+ const perUserData = {};
35
+ for (const key of Object.keys(decompressed)) {
36
+ if (key !== 'cids' && key !== 'metadata' && /^\d+$/.test(key)) {
37
+ perUserData[Number(key)] = decompressed[key];
38
+ }
39
+ }
40
+ return {
41
+ cids: decompressed.cids,
42
+ perUserData,
43
+ metadata: decompressed.metadata || {}
44
+ };
45
+ }
46
+ const cids = Object.keys(decompressed)
47
+ .filter(k => /^\d+$/.test(k))
48
+ .map(k => Number(k));
49
+ return {
50
+ cids,
51
+ perUserData: cids.reduce((acc, cid) => {
52
+ const val = decompressed[String(cid)];
53
+ if (val) acc[cid] = val;
54
+ return acc;
55
+ }, {}),
56
+ metadata: decompressed.metadata || {}
57
+ };
58
+ }
59
+ return { cids: [], perUserData: {}, metadata: {} };
60
+ }
61
+
62
+ async function readComputationResultsWithShards(db, docData, docRef, logger) {
63
+ try {
64
+ if (docData.gcsUri || (docData._gcs && docData.gcsBucket && docData.gcsPath)) {
65
+ const bucketName = docData.gcsBucket || docData.gcsUri.split('/')[2];
66
+ const fileName = docData.gcsPath || docData.gcsUri.split('/').slice(3).join('/');
67
+ const [fileContent] = await storage.bucket(bucketName).file(fileName).download();
68
+ let parsed;
69
+ try {
70
+ parsed = JSON.parse(zlib.gunzipSync(fileContent).toString('utf8'));
71
+ } catch (e) {
72
+ parsed = JSON.parse(fileContent.toString('utf8'));
73
+ }
74
+ return readComputationResults(parsed);
75
+ }
76
+
77
+ if (docData._sharded === true && docData._shardCount) {
78
+ const shardsCol = docRef.collection('_shards');
79
+ const shardsSnapshot = await shardsCol.get();
80
+ if (!shardsSnapshot.empty) {
81
+ const merged = {};
82
+ for (const shardDoc of shardsSnapshot.docs) {
83
+ const shardData = tryDecompress(shardDoc.data());
84
+ Object.assign(merged, shardData);
85
+ }
86
+ return readComputationResults(merged);
87
+ }
88
+ }
89
+
90
+ return readComputationResults(docData);
91
+ } catch (error) {
92
+ logger?.log('ERROR', `[AlertSystemV3] Failed reading results: ${error.message}`);
93
+ return { cids: [], perUserData: {}, metadata: {} };
94
+ }
95
+ }
96
+
97
+ module.exports = {
98
+ readComputationResultsWithShards
99
+ };
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ const { createAlertContext } = require('../core/AlertContext');
4
+ // CHANGE 1: Import the whole module object
5
+ const bqUtils = require('../../core/utils/bigquery_utils');
6
+
7
+ async function handleAlertHistory(message, context, config, dependencies) {
8
+ const ctx = createAlertContext(dependencies, config);
9
+ const { logger } = ctx;
10
+
11
+ try {
12
+ const payload = JSON.parse(Buffer.from(message.data, 'base64').toString());
13
+ const datasetId = process.env.BIGQUERY_DATASET_ID || 'bulltrackers_data';
14
+
15
+ // CHANGE 2: Call functions off the object
16
+ await bqUtils.ensurePIAlertHistoryTable(logger);
17
+
18
+ const row = {
19
+ date: payload.date,
20
+ pi_id: Number(payload.piId),
21
+ alert_type: payload.alertType,
22
+ triggered: true,
23
+ trigger_count: payload.triggerCount || 0,
24
+ triggered_for: payload.triggeredFor || [],
25
+ metadata: payload.metadata && typeof payload.metadata === 'object' ? payload.metadata : {},
26
+ last_triggered: payload.lastTriggered || new Date().toISOString(),
27
+ last_updated: payload.lastUpdated || new Date().toISOString()
28
+ };
29
+
30
+ // CHANGE 3: Call functions off the object
31
+ await bqUtils.insertRowsWithMerge(datasetId, 'pi_alert_history', [row], ['date', 'pi_id', 'alert_type'], logger);
32
+
33
+ logger?.log('INFO', `[AlertV3/History] Synced ${payload.alertType} for PI ${payload.piId}`);
34
+ } catch (error) {
35
+ logger?.log('ERROR', `[AlertV3/History] Failed: ${error.message}`);
36
+ throw error;
37
+ }
38
+ }
39
+
40
+ module.exports = { handleAlertHistory };
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ const { createAlertContext } = require('../core/AlertContext');
4
+ const { loadAlertTypesFromManifest, isAlertComputation } = require('../../alert-system/helpers/alert_manifest_loader');
5
+
6
+ let cachedAlertTypes = null;
7
+
8
+ async function handleComputationResultWrite(change, context, config, dependencies) {
9
+ const ctx = createAlertContext(dependencies, config);
10
+ const { logger } = ctx;
11
+ const { date, computationName, entityId } = context.params || {};
12
+
13
+ try {
14
+ if (!change.after.exists) return;
15
+ if (!cachedAlertTypes) cachedAlertTypes = await loadAlertTypesFromManifest(logger);
16
+
17
+ if (!isAlertComputation(cachedAlertTypes, computationName)) return;
18
+
19
+ const payload = {
20
+ date,
21
+ computationName,
22
+ entityId: entityId || null,
23
+ documentPath: change.after.ref.path,
24
+ timestamp: Date.now(),
25
+ traceId: context.eventId
26
+ };
27
+
28
+ await ctx.notificationBus.publishRawEvent(payload);
29
+ logger?.log('INFO', `[AlertV3/Ingest] Queued ${computationName} for ${date}`);
30
+ } catch (error) {
31
+ logger?.log('ERROR', `[AlertV3/Ingest] Failed: ${error.message}`);
32
+ }
33
+ }
34
+
35
+ module.exports = { handleComputationResultWrite };
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ const { createAlertContext } = require('../core/AlertContext');
4
+ const { FieldValue } = require('@google-cloud/firestore');
5
+ // CHANGE 1: Import the whole object to allow Stubbing
6
+ const fcmUtils = require('../../core/utils/fcm_utils');
7
+
8
+ async function handleAlertNotification(message, context, config, dependencies) {
9
+ const ctx = createAlertContext(dependencies, config);
10
+ const { db, logger } = ctx;
11
+
12
+ try {
13
+ const payload = JSON.parse(Buffer.from(message.data, 'base64').toString());
14
+
15
+ const alertId = `alert_${payload.computationDate}_${payload.alertTypeId}_${payload.piCid}_${payload.userCid}_${payload.watchlistId}`;
16
+
17
+ const alertData = {
18
+ alertId,
19
+ piCid: Number(payload.piCid),
20
+ piUsername: payload.piUsername || `PI-${payload.piCid}`,
21
+ alertType: payload.alertTypeId,
22
+ alertTypeName: payload.alertTypeName,
23
+ message: payload.message,
24
+ severity: payload.severity || 'medium',
25
+ watchlistId: payload.watchlistId,
26
+ watchlistName: payload.watchlistName,
27
+ read: false,
28
+ createdAt: FieldValue.serverTimestamp(),
29
+ computationDate: payload.computationDate,
30
+ computationName: payload.computationName,
31
+ ...(payload.metadata || {})
32
+ };
33
+
34
+ // 1. Write to Inbox (Firestore)
35
+ await db.collection(ctx.config.collections.signedInUsers)
36
+ .doc(String(payload.userCid))
37
+ .collection('alerts')
38
+ .doc(alertId)
39
+ .set(alertData);
40
+
41
+ try {
42
+ // CHANGE 2: Call the function off the object
43
+ await fcmUtils.sendAlertPushNotification(db, payload.userCid, alertData, logger);
44
+ } catch (e) {
45
+ logger?.log('WARN', `[AlertV3/Notifier] FCM failed for ${payload.userCid}: ${e.message}`);
46
+ }
47
+ } catch (error) {
48
+ logger?.log('ERROR', `[AlertV3/Notifier] Failed: ${error.message}`);
49
+ throw error;
50
+ }
51
+ }
52
+
53
+ module.exports = { handleAlertNotification };
@@ -0,0 +1,118 @@
1
+ 'use strict';
2
+
3
+ const { createAlertContext } = require('../core/AlertContext');
4
+ // Use object references to allow Sinon stubbing
5
+ const computationResults = require('../core/ComputationResults');
6
+ const alertLogic = require('../core/AlertLogic');
7
+ // Import legacy helpers as objects
8
+ const manifestLoader = require('../../alert-system/helpers/alert_manifest_loader');
9
+
10
+ let cachedAlertTypes = null;
11
+
12
+ async function handleRawAlert(message, context, config, dependencies) {
13
+ const ctx = createAlertContext(dependencies, config);
14
+ const { db, logger } = ctx;
15
+
16
+ try {
17
+ const payload = JSON.parse(Buffer.from(message.data, 'base64').toString());
18
+ const { date, computationName, documentPath } = payload;
19
+
20
+ // Call through the object reference
21
+ if (!cachedAlertTypes) {
22
+ cachedAlertTypes = await manifestLoader.loadAlertTypesFromManifest(logger);
23
+ }
24
+
25
+ const alertType = manifestLoader.getAlertTypeByComputation(cachedAlertTypes, computationName);
26
+
27
+ if (!alertType) {
28
+ logger?.log('WARN', `[AlertV3/Router] Unknown computation ${computationName}`);
29
+ return;
30
+ }
31
+
32
+ const docRef = db.doc(documentPath);
33
+ const snapshot = await docRef.get();
34
+ if (!snapshot.exists) return;
35
+
36
+ // Reference the computationResults object
37
+ const results = await computationResults.readComputationResultsWithShards(db, snapshot.data(), docRef, logger);
38
+ const cids = Array.isArray(results.cids) ? results.cids : [];
39
+
40
+ const notifications = [];
41
+ const historyEvents = [];
42
+
43
+ for (const piCid of cids) {
44
+ const piResult = (results.perUserData && results.perUserData[piCid]) || results.metadata || {};
45
+
46
+ const userCids = await ctx.membershipRepo.getUserCidsForPi(date, piCid);
47
+ if (!userCids.length) continue;
48
+
49
+ const subscriptions = await ctx.userConfigRepo.getSubscriptionsForPi({ piCid, userCids, alertType });
50
+ const triggeredFor = [];
51
+
52
+ for (const subscription of subscriptions) {
53
+ const isDeveloper = false;
54
+ // Call through the alertLogic object reference
55
+ const evaluation = alertLogic.evaluateAlertForSubscription({
56
+ alertType,
57
+ piResult,
58
+ subscription,
59
+ isDeveloper,
60
+ logger
61
+ });
62
+
63
+ if (!evaluation.shouldNotify) continue;
64
+
65
+ const piUsername = subscription.piUsername || piResult.username || `PI-${piCid}`;
66
+ const messageText = manifestLoader.generateAlertMessage(alertType, piUsername, piResult);
67
+
68
+ notifications.push({
69
+ userCid: subscription.userCid,
70
+ piCid: Number(piCid),
71
+ piUsername,
72
+ alertTypeId: alertType.id,
73
+ alertTypeName: alertType.name,
74
+ computationName: alertType.computationName,
75
+ computationDate: date,
76
+ severity: alertType.severity || 'medium',
77
+ watchlistId: subscription.watchlistId,
78
+ watchlistName: subscription.watchlistName,
79
+ message: messageText,
80
+ metadata: piResult,
81
+ evaluation
82
+ });
83
+
84
+ triggeredFor.push(String(subscription.userCid));
85
+ }
86
+
87
+ if (triggeredFor.length) {
88
+ historyEvents.push({
89
+ date,
90
+ piId: Number(piCid),
91
+ alertType: alertType.computationName || alertType.id,
92
+ computationName: alertType.computationName,
93
+ triggered: true,
94
+ triggerCount: triggeredFor.length,
95
+ triggeredFor,
96
+ metadata: piResult,
97
+ lastTriggered: new Date().toISOString(),
98
+ lastUpdated: new Date().toISOString()
99
+ });
100
+ }
101
+ }
102
+
103
+ if (notifications.length) {
104
+ await ctx.notificationBus.publishNotifications(notifications);
105
+ }
106
+
107
+ if (historyEvents.length) {
108
+ await ctx.notificationBus.publishHistoryEvents(historyEvents);
109
+ }
110
+
111
+ logger?.log('INFO', `[AlertV3/Router] Notifications: ${notifications.length}, History: ${historyEvents.length}`);
112
+ } catch (error) {
113
+ logger?.log('ERROR', `[AlertV3/Router] Failed: ${error.message}`);
114
+ throw error;
115
+ }
116
+ }
117
+
118
+ module.exports = { handleRawAlert };
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ const { handleComputationResultWrite } = require('./handlers/ingest');
4
+ const { handleRawAlert } = require('./handlers/router');
5
+ const { handleAlertNotification } = require('./handlers/notifier');
6
+ const { handleAlertHistory } = require('./handlers/history');
7
+
8
+ module.exports = {
9
+ handleComputationResultWrite,
10
+ handleRawAlert,
11
+ handleAlertNotification,
12
+ handleAlertHistory
13
+ };