bulltrackers-module 1.0.127 → 1.0.128

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.
@@ -4,797 +4,44 @@
4
4
  *
5
5
  * This orchestrator is designed to be run by a separate Cloud Function for each "pass".
6
6
  * It reads its pass number from the config and executes only those calculations.
7
- *
8
- * - Pass 1: Runs "standard" calcs that stream user data.
9
- * - Pass 2+: Runs "meta" calcs, which it first supplies with dependencies
10
- * by fetching the results of *previous* passes from Firestore.
11
- */
12
-
13
- const { FieldPath } = require('@google-cloud/firestore');
14
- const {
15
- getPortfolioPartRefs,
16
- loadFullDayMap, // <-- Ensure loadFullDayMap is imported
17
- loadDataByRefs,
18
- loadDailyInsights,
19
- loadDailySocialPostInsights,
20
- getHistoryPartRefs // <-- IMPORT NEW FUNCTION
21
- } = require('../utils/data_loader.js');
22
-
23
- const {
24
- normalizeName,
25
- getExpectedDateStrings,
26
- getFirstDateFromSourceData,
27
- commitBatchInChunks
28
- } = require('../utils/utils.js');
29
-
30
-
31
- /**
32
- * Groups the manifest by pass number.
33
- * @param {Array<object>} manifest - The computation manifest.
34
- * @returns {object} { '1': [...], '2': [...] }
35
- */
36
- function groupByPass(manifest) {
37
- return manifest.reduce((acc, calc) => {
38
- (acc[calc.pass] = acc[calc.pass] || []).push(calc);
39
- return acc;
40
- }, {});
41
- }
42
-
43
- /**
44
- * Checks if a calculation's root data dependencies are met.
45
- * @param {object} calcManifest - The manifest entry for the calculation.
46
- * @param {object} rootDataStatus - The status object from checkRootDataAvailability.
47
- * @returns {boolean} True if dependencies are met, false otherwise.
48
- */
49
- function checkRootDependencies(calcManifest, rootDataStatus) {
50
- if (!calcManifest.rootDataDependencies || calcManifest.rootDataDependencies.length === 0) {
51
- return true;
52
- }
53
- for (const dep of calcManifest.rootDataDependencies) {
54
- if (dep === 'portfolio' && !rootDataStatus.hasPortfolio) return false;
55
- if (dep === 'insights' && !rootDataStatus.hasInsights) return false;
56
- if (dep === 'social' && !rootDataStatus.hasSocial) return false;
57
- if (dep === 'history' && !rootDataStatus.hasHistory) return false; // <-- ADDED THIS LINE
58
- }
59
- return true; // All dependencies were met
60
- }
61
-
62
-
63
- /**
64
- * Checks if the root data (portfolios, insights, social, history) exists for a given day.
65
- * @param {string} dateStr - The date string to check (YYYY-MM-DD).
66
- * @param {object} config - The computation system configuration object.
67
- * @param {object} dependencies - Contains db, logger, calculationUtils.
68
- * @returns {Promise<object>} { portfolioRefs, insightsData, socialData, historyRefs, hasPortfolio, hasInsights, hasSocial, hasHistory }
7
+ * This file contains the high-level "manual" of steps. The "how-to" logic
8
+ * is extracted into 'computation_system_utils.js'.
69
9
  */
70
- async function checkRootDataAvailability(dateStr, config, dependencies) {
71
- const { logger } = dependencies;
72
- logger.log('INFO', `[PassRunner] Checking root data availability for ${dateStr}...`);
73
-
74
- try {
75
- // --- MODIFIED: Add getHistoryPartRefs to the parallel load ---
76
- const [portfolioRefs, insightsData, socialData, historyRefs] = await Promise.all([
77
- getPortfolioPartRefs(config, dependencies, dateStr),
78
- loadDailyInsights(config, dependencies, dateStr),
79
- loadDailySocialPostInsights(config, dependencies, dateStr),
80
- getHistoryPartRefs(config, dependencies, dateStr) // <-- ADD THIS
81
- ]);
10
+ const { groupByPass, checkRootDataAvailability, fetchDependenciesForPass, filterCalculations, runStandardComputationPass, runMetaComputationPass } = require('./orchestration_helpers.js');
11
+ const { getExpectedDateStrings, getFirstDateFromSourceData } = require('../utils/utils.js');
82
12
 
83
- const hasPortfolio = (portfolioRefs && portfolioRefs.length > 0);
84
- const hasInsights = !!insightsData;
85
- const hasSocial = !!socialData;
86
- const hasHistory = (historyRefs && historyRefs.length > 0); // <-- ADD THIS
87
-
88
- return {
89
- portfolioRefs: portfolioRefs || [],
90
- insightsData: insightsData || null,
91
- socialData: socialData || null,
92
- historyRefs: historyRefs || [], // <-- ADD THIS
93
- hasPortfolio: hasPortfolio,
94
- hasInsights: hasInsights,
95
- hasSocial: hasSocial,
96
- hasHistory: hasHistory // <-- ADD THIS
97
- };
98
-
99
- } catch (err) {
100
- logger.log('ERROR', `[PassRunner] Error checking data availability for ${dateStr}`, { errorMessage: err.message });
101
- return {
102
- portfolioRefs: [], insightsData: null, socialData: null, historyRefs: [], // <-- ADD historyRefs
103
- hasPortfolio: false, hasInsights: false, hasSocial: false, hasHistory: false // <-- ADD hasHistory
104
- };
105
- }
106
- }
107
-
108
- /**
109
- * (NEW) Fetches all computed dependencies for a given pass and date from Firestore.
110
- * @param {string} dateStr - The date string (YYYY-MM-DD).
111
- * @param {Array<object>} calcsInPass - The manifest entries for this pass.
112
- * @param {Array<object>} fullManifest - The *entire* computation manifest.
113
- * @param {object} config - The computation system configuration.
114
- * @param {object} dependencies - Contains db, logger.
115
- * @returns {Promise<object>} A map of { 'calc-name': result, ... }
116
- */
117
- async function fetchDependenciesForPass(dateStr, calcsInPass, fullManifest, config, dependencies) {
118
- const { db, logger } = dependencies;
119
- const { resultsCollection, resultsSubcollection, computationsSubcollection } = config;
120
-
121
- // --- THIS IS THE FIX ---
122
- // Build the manifestMap from the *fullManifest* so we can find
123
- // dependencies from *all* passes, not just the current one.
124
- const manifestMap = new Map();
125
- for (const calc of fullManifest) {
126
- manifestMap.set(normalizeName(calc.name), calc);
127
- }
128
- // --- END FIX ---
129
-
130
- const requiredDeps = new Set();
131
-
132
- // 1. Get all unique dependencies required by calcs in *this* pass
133
- for (const calc of calcsInPass) {
134
- if (calc.type === 'meta' && calc.dependencies) {
135
- calc.dependencies.forEach(depName => requiredDeps.add(normalizeName(depName)));
136
- }
137
- }
138
-
139
- if (requiredDeps.size === 0) {
140
- logger.log('INFO', `[PassRunner] No Firestore dependencies to fetch for this pass on ${dateStr}.`);
141
- return {};
142
- }
143
-
144
- logger.log('INFO', `[PassRunner] Fetching ${requiredDeps.size} dependencies from Firestore for ${dateStr}...`);
145
-
146
- const docRefs = [];
147
- const depNames = [];
148
-
149
- // 2. Build the list of Firestore document references
150
- for (const calcName of requiredDeps) {
151
- const calcManifest = manifestMap.get(calcName); // Now correctly finds deps from other passes
152
- if (!calcManifest) {
153
- logger.log('ERROR', `[PassRunner] Cannot find manifest entry for dependency "${calcName}". This is a manifest error. Skipping dependency.`);
154
- continue;
155
- }
156
-
157
- const category = calcManifest.category || 'unknown';
158
- const docRef = db.collection(resultsCollection).doc(dateStr)
159
- .collection(resultsSubcollection).doc(category)
160
- .collection(computationsSubcollection).doc(calcName);
161
-
162
- docRefs.push(docRef);
163
- depNames.push(calcName);
164
- }
165
-
166
- // 3. Fetch all dependencies in one batch
167
- const fetchedDependencies = {};
168
- if (docRefs.length > 0) {
169
- const snapshots = await db.getAll(...docRefs);
170
- snapshots.forEach((doc, i) => {
171
- const calcName = depNames[i];
172
- if (doc.exists) {
173
- fetchedDependencies[calcName] = doc.data();
174
- } else {
175
- fetchedDependencies[calcName] = null; // Mark as null if not found
176
- }
177
- });
178
- }
179
-
180
- logger.log('INFO', `[PassRunner] Successfully fetched ${Object.keys(fetchedDependencies).length} dependencies for ${dateStr}.`);
181
- return fetchedDependencies;
182
- }
183
-
184
-
185
- /**
186
- * Main pipe: pipe.computationSystem.runComputationPass
187
- * @param {object} config - The computation system configuration object.
188
- * @param {object} dependencies - Contains db, logger, calculationUtils.
189
- * @param {Array<object>} computationManifest - The injected computation manifest.
190
- * @returns {Promise<Object>} Summary of all passes.
191
- */
192
13
  async function runComputationPass(config, dependencies, computationManifest) {
193
14
  const { logger } = dependencies;
194
-
195
- // --- (NEW) Get the pass number this function is responsible for ---
196
- const passToRun = String(config.COMPUTATION_PASS_TO_RUN);
197
- if (!passToRun) {
198
- logger.log('ERROR', '[PassRunner] FATAL: COMPUTATION_PASS_TO_RUN is not defined in config. Aborting.');
199
- return;
200
- }
201
- logger.log('INFO', `🚀 [PassRunner] Starting run for PASS ${passToRun}...`);
202
- // --- END NEW ---
15
+ const passToRun = String(config.COMPUTATION_PASS_TO_RUN); if (!passToRun) return logger.log('ERROR', '[PassRunner] No pass defined. Aborting.');
16
+ logger.log('INFO', `🚀 Starting PASS ${passToRun}...`);
203
17
 
204
- const summary = {};
205
- const yesterday = new Date();
206
- yesterday.setUTCDate(yesterday.getUTCDate() - 1);
18
+ const yesterday = new Date(); yesterday.setUTCDate(yesterday.getUTCDate()-1);
207
19
  const endDateUTC = new Date(Date.UTC(yesterday.getUTCFullYear(), yesterday.getUTCMonth(), yesterday.getUTCDate()));
208
-
209
20
  const firstDate = await getFirstDateFromSourceData(config, dependencies);
210
- const startDateUTC = firstDate
211
- ? new Date(Date.UTC(firstDate.getUTCFullYear(), firstDate.getUTCMonth(), firstDate.getUTCDate()))
212
- : new Date(config.earliestComputationDate + 'T00:00:00Z');
213
-
21
+ const startDateUTC = firstDate ? new Date(Date.UTC(firstDate.getUTCFullYear(), firstDate.getUTCMonth(), firstDate.getUTCDate())) : new Date(config.earliestComputationDate+'T00:00:00Z');
214
22
  const allExpectedDates = getExpectedDateStrings(startDateUTC, endDateUTC);
215
23
 
216
24
  const passes = groupByPass(computationManifest);
217
- const calcsInThisPass = passes[passToRun] || [];
218
-
219
- if (calcsInThisPass.length === 0) {
220
- logger.log('WARN', `[PassRunner] No calculations found in manifest for Pass ${passToRun}. Exiting.`);
221
- return;
222
- }
25
+ const calcsInThisPass = passes[passToRun] || []; if (!calcsInThisPass.length) return logger.log('WARN', `[PassRunner] No calcs for Pass ${passToRun}. Exiting.`);
223
26
 
224
- const standardCalcs = calcsInThisPass.filter(c => c.type === 'standard');
225
- const metaCalcs = calcsInThisPass.filter(c => c.type === 'meta');
226
- logger.log('INFO', `[PassRunner] Found ${standardCalcs.length} standard and ${metaCalcs.length} meta calcs for Pass ${passToRun}.`);
27
+ const standardCalcs = calcsInThisPass.filter(c => c.type==='standard');
28
+ const metaCalcs = calcsInThisPass.filter(c => c.type==='meta');
227
29
 
228
-
229
- // --- Process ONE DAY at a time, in order ---
230
30
  for (const dateStr of allExpectedDates) {
231
- const dateToProcess = new Date(dateStr + 'T00:00:00Z');
232
-
233
- // 1. Check for root data (portfolios, insights, social, AND HISTORY)
234
- const rootData = await checkRootDataAvailability(dateStr, config, dependencies);
235
- const rootDataStatus = {
236
- hasPortfolio: rootData.hasPortfolio,
237
- hasInsights: rootData.hasInsights,
238
- hasSocial: rootData.hasSocial,
239
- hasHistory: rootData.hasHistory // <-- ADD THIS
240
- };
241
-
242
- if (!rootData.hasPortfolio && !rootData.hasInsights && !rootData.hasSocial && !rootData.hasHistory) {
243
- logger.log('WARN', `[PassRunner] Skipping Pass ${passToRun} for ${dateStr} due to missing all root data.`);
244
- continue; // Skip to the next day
245
- }
246
-
247
- logger.log('INFO', `[PassRunner] Processing Pass ${passToRun} for ${dateStr}...`);
248
-
249
- // 2. (NEW) Fetch all dependencies for this pass and date from Firestore
250
- // This is skipped for Pass 1, as `requiredDeps.size` will be 0
251
- const fetchedDependencies = await fetchDependenciesForPass(
252
- dateStr,
253
- calcsInThisPass,
254
- computationManifest, // <-- Pass the *full* manifest here
255
- config,
256
- dependencies
257
- );
258
-
259
- const skippedCalculations = new Set();
260
-
261
- // 3. Filter calculations based on root data
262
- const standardCalcsToRun = [];
263
- for (const calcManifest of standardCalcs) {
264
- if (checkRootDependencies(calcManifest, rootDataStatus)) {
265
- standardCalcsToRun.push(calcManifest);
266
- } else {
267
- logger.log('INFO', `[Pass ${passToRun}] Skipping standard calc "${calcManifest.name}" for ${dateStr} due to missing root data.`);
268
- skippedCalculations.add(calcManifest.name);
269
- }
270
- }
271
-
272
- const metaCalcsToRun = [];
273
- for (const calcManifest of metaCalcs) {
274
- const calcName = calcManifest.name;
275
-
276
- // Check 1: Are root data dependencies met?
277
- if (!checkRootDependencies(calcManifest, rootDataStatus)) {
278
- logger.log('INFO', `[Pass ${passToRun} (Meta)] Skipping meta calc "${calcName}" for ${dateStr} due to missing root data.`);
279
- skippedCalculations.add(calcName);
280
- continue;
281
- }
282
-
283
- // Check 2: Are *computed* dependencies (from previous passes) met?
284
- let depCheck = true;
285
- let missingDepName = '';
286
- for (const depName of (calcManifest.dependencies || [])) {
287
- const normalizedDepName = normalizeName(depName);
288
- if (!fetchedDependencies[normalizedDepName]) { // Check if null or undefined
289
- depCheck = false;
290
- missingDepName = normalizedDepName;
291
- break;
292
- }
293
- }
294
-
295
- if (depCheck) {
296
- metaCalcsToRun.push(calcManifest);
297
- } else {
298
- logger.log('WARN', `[Pass ${passToRun} (Meta)] Skipping meta calc "${calcName}" for ${dateStr} due to missing computed dependency "${missingDepName}" from Firestore.`);
299
- skippedCalculations.add(calcName);
300
- }
301
- }
302
-
303
- // --- 4. Run the filtered calculations ---
31
+ const dateToProcess = new Date(dateStr+'T00:00:00Z');
304
32
  try {
305
- // Run standard calcs for this pass (e.g., Pass 1)
306
- if (standardCalcsToRun.length > 0) {
307
- await runUnifiedComputation(
308
- dateToProcess,
309
- standardCalcsToRun,
310
- `Pass ${passToRun} (Standard)`,
311
- config,
312
- dependencies,
313
- rootData
314
- );
315
- }
316
-
317
- // Run meta calcs for this pass (e.g., Pass 2, 3, 4)
318
- if (metaCalcsToRun.length > 0) {
319
- await runMetaComputation(
320
- dateToProcess,
321
- metaCalcsToRun,
322
- `Pass ${passToRun} (Meta)`,
323
- config,
324
- dependencies,
325
- fetchedDependencies, // <-- This is the NEW object from Firestore
326
- rootData
327
- );
328
- }
329
- logger.log('SUCCESS', `[PassRunner] Completed Pass ${passToRun} for ${dateStr}.`);
33
+ const rootData = await checkRootDataAvailability(dateStr, config, dependencies); if (!rootData) continue;
34
+ const fetchedDeps = await fetchDependenciesForPass(dateStr, calcsInThisPass, computationManifest, config, dependencies);
35
+ const { standardCalcsToRun, metaCalcsToRun } = filterCalculations(standardCalcs, metaCalcs, rootData.status, fetchedDeps, passToRun, dateStr, logger);
36
+ if (standardCalcsToRun.length) await runStandardComputationPass(dateToProcess, standardCalcsToRun, `Pass ${passToRun} (Standard)`, config, dependencies, rootData);
37
+ if (metaCalcsToRun.length) await runMetaComputationPass(dateToProcess, metaCalcsToRun, `Pass ${passToRun} (Meta)`, config, dependencies, fetchedDeps, rootData);
38
+ logger.log('SUCCESS', `[PassRunner] Completed Pass ${passToRun} for ${dateStr}.`);
330
39
  } catch (err) {
331
40
  logger.log('ERROR', `[PassRunner] FAILED Pass ${passToRun} for ${dateStr}`, { errorMessage: err.message, stack: err.stack });
332
41
  }
333
- } // End dates loop
334
-
335
- logger.log('INFO', `[PassRunner] Pass ${passToRun} orchestration finished.`);
336
- return summary;
337
- }
338
-
339
- /**
340
- * Internal sub-pipe: Initializes calculator instances.
341
- */
342
- function initializeCalculators(calculationsToRun, logger) {
343
- const state = {};
344
- for (const calcManifest of calculationsToRun) {
345
- const calcName = normalizeName(calcManifest.name);
346
- const CalculationClass = calcManifest.class;
347
-
348
- if (typeof CalculationClass === 'function') {
349
- try {
350
- const instance = new CalculationClass();
351
- instance.manifest = calcManifest; // Attach manifest data
352
- state[calcName] = instance;
353
- } catch (e) {
354
- logger.warn(`[PassRunner] Init failed for ${calcName}`, { errorMessage: e.message });
355
- state[calcName] = null;
356
- }
357
- } else {
358
- logger.warn(`[PassRunner] Calculation class not found in manifest for: ${calcName}`);
359
- state[calcName] = null;
360
- }
361
- }
362
- return state;
363
- }
364
-
365
- /**
366
- * Internal sub-pipe: Streams data and calls process() on "standard" calculators.
367
- */
368
- async function streamAndProcess(
369
- dateStr, todayRefs, state, passName, config, dependencies,
370
- yesterdayPortfolios = {},
371
- todayInsights = null,
372
- yesterdayInsights = null,
373
- todaySocialPostInsights = null,
374
- yesterdaySocialPostInsights = null,
375
- todayHistoryData = null, // <-- ADD THIS
376
- yesterdayHistoryData = null // <-- ADD THIS
377
- ) {
378
- const { logger, calculationUtils } = dependencies;
379
- logger.log('INFO', `[${passName}] Streaming ${todayRefs.length} 'today' part docs for ${dateStr}...`);
380
-
381
- const yesterdayDate = new Date(dateStr + 'T00:00:00Z');
382
- yesterdayDate.setUTCDate(yesterdayDate.getUTCDate() - 1);
383
- const yesterdayStr = yesterdayDate.toISOString().slice(0, 10);
384
-
385
- const { instrumentToTicker, instrumentToSector } = await calculationUtils.loadInstrumentMappings();
386
-
387
- const context = {
388
- instrumentMappings: instrumentToTicker,
389
- sectorMapping: instrumentToSector,
390
- todayDateStr: dateStr,
391
- yesterdayDateStr: yesterdayStr,
392
- dependencies: dependencies,
393
- config: config
394
- };
395
-
396
- const batchSize = config.partRefBatchSize || 10;
397
- let isFirstUser = true;
398
-
399
- for (let i = 0; i < todayRefs.length; i += batchSize) {
400
- const batchRefs = todayRefs.slice(i, i + batchSize);
401
- const todayPortfoliosChunk = await loadDataByRefs(config, dependencies, batchRefs);
402
-
403
- for (const uid in todayPortfoliosChunk) {
404
- const p = todayPortfoliosChunk[uid];
405
- if (!p) continue;
406
- const userType = p.PublicPositions ? 'speculator' : 'normal';
407
- context.userType = userType;
408
-
409
- for (const calcName in state) {
410
- const calc = state[calcName];
411
- if (!calc || typeof calc.process !== 'function') continue;
412
-
413
- const manifestCalc = calc.manifest;
414
- const isSocialOrInsights = manifestCalc.category === 'socialPosts' || manifestCalc.category === 'insights';
415
- const isHistoricalCalc = manifestCalc.isHistorical === true;
416
- const isSpeculatorCalc = manifestCalc.category === 'speculators';
417
- let processArgs;
418
-
419
- // --- MODIFIED: Add history data to the context args ---
420
- const allContextArgs = [
421
- context,
422
- todayInsights,
423
- yesterdayInsights,
424
- todaySocialPostInsights,
425
- yesterdaySocialPostInsights,
426
- todayHistoryData, // <-- ADD THIS
427
- yesterdayHistoryData // <-- ADD THIS
428
- ];
429
-
430
- if (isSocialOrInsights) {
431
- if (isFirstUser) {
432
- processArgs = [null, null, null, ...allContextArgs];
433
- } else {
434
- continue; // Only run once
435
- }
436
- } else if (isHistoricalCalc) {
437
- const pYesterday = yesterdayPortfolios[uid];
438
- if (!pYesterday) continue;
439
- processArgs = [p, pYesterday, uid, ...allContextArgs];
440
- } else {
441
- processArgs = [p, null, uid, ...allContextArgs];
442
- }
443
-
444
- if (!isSocialOrInsights) {
445
- if ((userType === 'normal' && isSpeculatorCalc) ||
446
- (userType === 'speculator' && !isSpeculatorCalc && calcName !== 'users-processed')) {
447
- continue; // Skip: wrong user type
448
- }
449
- }
450
-
451
- try {
452
- await Promise.resolve(calc.process(...processArgs));
453
- } catch (e) {
454
- logger.log('WARN', `Process error in ${calcName} for user ${uid}`, { err: e.message });
455
- }
456
- }
457
- isFirstUser = false;
458
- }
459
42
  }
460
-
461
- // Handle case where there are no users but we still need to run insights/social calcs
462
- if (todayRefs.length === 0 && isFirstUser) {
463
- logger.log('INFO', `[${passName}] No user portfolios found for ${dateStr}. Running insights/social calcs once.`);
464
-
465
- // --- MODIFIED: Add null history data ---
466
- const allContextArgs = [
467
- context,
468
- todayInsights,
469
- yesterdayInsights,
470
- todaySocialPostInsights,
471
- yesterdaySocialPostInsights,
472
- todayHistoryData, // <-- ADD THIS
473
- yesterdayHistoryData // <-- ADD THIS
474
- ];
475
-
476
- for (const calcName in state) {
477
- const calc = state[calcName];
478
- if (!calc || typeof calc.process !== 'function') continue;
479
- const manifestCalc = calc.manifest;
480
- const isSocialOrInsights = manifestCalc.category === 'socialPosts' || manifestCalc.category === 'insights';
481
- if (isSocialOrInsights) {
482
- try {
483
- await Promise.resolve(calc.process(null, null, null, ...allContextArgs));
484
- } catch (e) {
485
- logger.log('WARN', `Process error in ${calcName} for no-user run`, { err: e.message });
486
- }
487
- }
488
- }
489
- }
490
- }
491
43
 
492
-
493
- /**
494
- * Internal sub-pipe: Runs "standard" computations (Pass 1) for a single date.
495
- */
496
- async function runUnifiedComputation(dateToProcess, calculationsToRun, passName, config, dependencies, rootData) {
497
- const { db, logger } = dependencies;
498
- const dateStr = dateToProcess.toISOString().slice(0, 10);
499
- logger.log('INFO', `[${passName}] Starting run for ${dateStr} with ${calculationsToRun.length} calcs.`);
500
-
501
- try {
502
- // --- NEW: Get history root data from the orchestrator ---
503
- const {
504
- portfolioRefs: todayRefs,
505
- insightsData: todayInsightsData,
506
- socialData: todaySocialPostInsightsData,
507
- historyRefs: todayHistoryRefs // <-- ADD THIS
508
- } = rootData;
509
-
510
- let yesterdayPortfolios = {};
511
- let yesterdayInsightsData = null;
512
- let yesterdaySocialPostInsightsData = null;
513
- let todayHistoryData = null; // <-- ADD THIS
514
- let yesterdayHistoryData = null; // <-- ADD THIS
515
-
516
- // --- MODIFIED: Add checks for history data ---
517
- const requiresYesterdayPortfolio = calculationsToRun.some(c => c.isHistorical === true);
518
- const requiresYesterdayInsights = calculationsToRun.some(c => c.class.prototype.process.toString().includes('yesterdayInsights'));
519
- const requiresYesterdaySocialPosts = calculationsToRun.some(c => c.class.prototype.process.toString().includes('yesterdaySocialPostInsights'));
520
- const requiresHistory = calculationsToRun.some(c => c.rootDataDependencies.includes('history'));
521
- const requiresYesterdayHistory = calculationsToRun.some(c => c.isHistorical === true && c.class.prototype.process.toString().includes('yesterdayHistoryData'));
522
- // --- END MODIFICATION ---
523
-
524
-
525
- // --- (This block is now much larger, handling all "yesterday" and "history" data) ---
526
- if (requiresYesterdayPortfolio || requiresYesterdayInsights || requiresYesterdaySocialPosts || requiresHistory || requiresYesterdayHistory) {
527
-
528
- if(requiresYesterdayInsights) {
529
- let daysAgo = 1; const maxLookback = 30;
530
- while (!yesterdayInsightsData && daysAgo <= maxLookback) {
531
- const prev = new Date(dateToProcess); prev.setUTCDate(prev.getUTCDate() - daysAgo);
532
- const prevStr = prev.toISOString().slice(0, 10);
533
- yesterdayInsightsData = await loadDailyInsights(config, dependencies, prevStr);
534
- if (yesterdayInsightsData) logger.log('INFO', `[${passName}] Found 'yesterday' instrument insights data from ${daysAgo} day(s) ago (${prevStr}).`);
535
- else daysAgo++;
536
- }
537
- if (!yesterdayInsightsData) logger.log('WARN', `[${passName}] Could not find any 'yesterday' instrument insights data within a ${maxLookback} day lookback.`);
538
- }
539
- if(requiresYesterdaySocialPosts) {
540
- let daysAgo = 1; const maxLookback = 30;
541
- while (!yesterdaySocialPostInsightsData && daysAgo <= maxLookback) {
542
- const prev = new Date(dateToProcess); prev.setUTCDate(prev.getUTCDate() - daysAgo);
543
- const prevStr = prev.toISOString().slice(0, 10);
544
- yesterdaySocialPostInsightsData = await loadDailySocialPostInsights(config, dependencies, prevStr);
545
- if (yesterdaySocialPostInsightsData) logger.log('INFO', `[${passName}] Found 'yesterday' social post insights data from ${daysAgo} day(s) ago (${prevStr}).`);
546
- else daysAgo++;
547
- }
548
- if (!yesterdaySocialPostInsightsData) logger.log('WARN', `[${passName}] Could not find any 'yesterday' social post insights data within a ${maxLookback} day lookback.`);
549
- }
550
- if (requiresYesterdayPortfolio) {
551
- const prev = new Date(dateToProcess); prev.setUTCDate(prev.getUTCDate() - 1);
552
- const prevStr = prev.toISOString().slice(0, 10);
553
- const yesterdayRefs = await getPortfolioPartRefs(config, dependencies, prevStr);
554
- if (yesterdayRefs.length > 0) {
555
- yesterdayPortfolios = await loadFullDayMap(config, dependencies, yesterdayRefs);
556
- logger.log('INFO', `[${passName}] Loaded yesterday's (${prevStr}) portfolio map for historical calcs.`);
557
- } else {
558
- logger.log('WARN', `[${passName}] Yesterday's (${prevStr}) portfolio data not found. Historical calcs requiring it will be skipped.`);
559
- }
560
- }
561
-
562
- // --- NEW: Add logic to load Today's History Data (if needed) ---
563
- if (requiresHistory && todayHistoryRefs.length > 0) {
564
- logger.log('INFO', `[${passName}] Loading today's (${dateStr}) history map...`);
565
- todayHistoryData = await loadFullDayMap(config, dependencies, todayHistoryRefs);
566
- logger.log('INFO', `[${passName}] Loaded today's history map with ${Object.keys(todayHistoryData).length} users.`);
567
- }
568
-
569
- // --- NEW: Add logic to load Yesterday's History Data (if needed) ---
570
- if (requiresYesterdayHistory) {
571
- const prev = new Date(dateToProcess);
572
- prev.setUTCDate(prev.getUTCDate() - 1);
573
- const prevStr = prev.toISOString().slice(0, 10);
574
- const yesterdayHistoryRefs = await getHistoryPartRefs(config, dependencies, prevStr);
575
- if (yesterdayHistoryRefs.length > 0) {
576
- yesterdayHistoryData = await loadFullDayMap(config, dependencies, yesterdayHistoryRefs);
577
- logger.log('INFO', `[${passName}] Loaded yesterday's (${prevStr}) history map for historical calcs.`);
578
- } else {
579
- logger.log('WARN', `[${passName}] Yesterday's (${prevStr}) history data not found. Historical calcs requiring it will be skipped.`);
580
- }
581
- }
582
- }
583
-
584
- const state = initializeCalculators(calculationsToRun, logger);
585
-
586
- // --- MODIFIED: Pass new history data ---
587
- await streamAndProcess(
588
- dateStr,
589
- todayRefs,
590
- state,
591
- passName,
592
- config,
593
- dependencies,
594
- yesterdayPortfolios,
595
- todayInsightsData,
596
- yesterdayInsightsData,
597
- todaySocialPostInsightsData,
598
- yesterdaySocialPostInsightsData,
599
- todayHistoryData, // <-- ADD THIS
600
- yesterdayHistoryData // <-- ADD THIS
601
- );
602
-
603
- let successCount = 0;
604
- const resultsCollectionRef = db.collection(config.resultsCollection).doc(dateStr).collection(config.resultsSubcollection);
605
-
606
- for (const calcName in state) {
607
- const calc = state[calcName];
608
- if (!calc || typeof calc.getResult !== 'function') {
609
- if (!calc) logger.log('WARN', `[${passName}] Skipping ${calcName} for ${dateStr} because it failed to initialize.`);
610
- continue;
611
- }
612
-
613
- const category = calc.manifest.category || 'unknown';
614
- let result = null;
615
- try {
616
- result = await Promise.resolve(calc.getResult());
617
-
618
- // --- Database write logic (Identical to original file) ---
619
- const pendingWrites = [];
620
- const summaryData = {};
621
- if (result && Object.keys(result).length > 0) {
622
- let isSharded = false;
623
- const shardedCollections = {
624
- 'sharded_user_profile': config.shardedUserProfileCollection,
625
- 'sharded_user_profitability': config.shardedProfitabilityCollection
626
- };
627
- for (const resultKey in shardedCollections) {
628
- if (result[resultKey]) {
629
- isSharded = true;
630
- const shardCollectionName = shardedCollections[resultKey];
631
- const shardedData = result[resultKey];
632
- for (const shardId in shardedData) {
633
- const shardDocData = shardedData[shardId];
634
- if (shardDocData && Object.keys(shardDocData).length > 0) {
635
- const shardRef = db.collection(shardCollectionName).doc(shardId);
636
- pendingWrites.push({ ref: shardRef, data: shardedData[shardId] });
637
- }
638
- }
639
- const { [resultKey]: _, ...otherResults } = result;
640
- if (Object.keys(otherResults).length > 0) {
641
- const computationDocRef = resultsCollectionRef.doc(category)
642
- .collection(config.computationsSubcollection)
643
- .doc(calcName);
644
- pendingWrites.push({ ref: computationDocRef, data: otherResults });
645
- }
646
- }
647
- }
648
- if (!isSharded) {
649
- const computationDocRef = resultsCollectionRef.doc(category)
650
- .collection(config.computationsSubcollection)
651
- .doc(calcName);
652
- pendingWrites.push({ ref: computationDocRef, data: result });
653
- }
654
- if (!summaryData[category]) summaryData[category] = {};
655
- summaryData[category][calcName] = true;
656
- if (Object.keys(summaryData).length > 0) {
657
- const topLevelDocRef = db.collection(config.resultsCollection).doc(dateStr);
658
- pendingWrites.push({ ref: topLevelDocRef, data: summaryData });
659
- }
660
- if (pendingWrites.length > 0) {
661
- await commitBatchInChunks(config, dependencies, pendingWrites, `Commit ${passName} ${dateStr} [${calcName}]`);
662
- successCount++;
663
- }
664
- } else {
665
- if (result === null) logger.log('INFO', `[${passName}] Calculation ${calcName} returned null for ${dateStr}. This is expected if no data was processed.`);
666
- else logger.log('WARN', `[${passName}] Calculation ${calcName} produced empty results {} for ${dateStr}. Skipping write.`);
667
- }
668
- } catch (e) {
669
- logger.log('ERROR', `[${passName}] getResult/Commit failed for ${calcName} on ${dateStr}`, { err: e.message, stack: e.stack });
670
- }
671
- }
672
- const completionStatus = successCount === calculationsToRun.length ? 'SUCCESS' : 'WARN';
673
- logger.log(completionStatus, `[${passName}] Completed ${dateStr}. Success: ${successCount}/${calculationsToRun.length}.`);
674
-
675
- } catch (err) {
676
- logger.log('ERROR', `[${passName}] Fatal error for ${dateStr}`, { errorMessage: err.message, stack: err.stack });
677
- throw err; // Re-throw to stop the orchestrator for this day
678
- }
679
- }
680
-
681
-
682
- /**
683
- * Internal sub-pipe: Runs "meta" computations (Pass 2, 3, 4) for a single date.
684
- */
685
- async function runMetaComputation(
686
- dateToProcess,
687
- calculationsToRun, // This is the *filtered* list
688
- passName,
689
- config,
690
- dependencies,
691
- fetchedDependencies, // <-- This is the NEW object from Firestore
692
- rootData
693
- ) {
694
- const { db, logger } = dependencies;
695
- const dateStr = dateToProcess.toISOString().slice(0, 10);
696
- logger.log('INFO', `[${passName}] Starting run for ${dateStr} with ${calculationsToRun.length} calcs.`);
697
-
698
- try {
699
- const resultsCollectionRef = db.collection(config.resultsCollection).doc(dateStr).collection(config.resultsSubcollection);
700
- const compsSub = config.computationsSubcollection || 'computations';
701
- let successCount = 0;
702
-
703
- // Add rootData to dependencies for meta-calcs that need to stream users
704
- const dependenciesForMetaCalc = {
705
- ...dependencies,
706
- rootData: rootData
707
- };
708
-
709
- for (const manifestCalc of calculationsToRun) {
710
- const calcName = normalizeName(manifestCalc.name);
711
- const category = manifestCalc.category || 'unknown';
712
- const CalcClass = manifestCalc.class;
713
-
714
- if (typeof CalcClass !== 'function') {
715
- logger.log('ERROR', `[${passName}] Invalid class in manifest for ${calcName}. Skipping.`);
716
- continue;
717
- }
718
-
719
- const instance = new CalcClass();
720
- let result = null;
721
- try {
722
- // --- Call process with the fetched dependencies ---
723
- result = await Promise.resolve(instance.process(
724
- dateStr,
725
- dependenciesForMetaCalc,
726
- config,
727
- fetchedDependencies // <-- Pass the pre-fetched results
728
- ));
729
-
730
- // --- Database write logic (Identical to original file) ---
731
- const pendingWrites = [];
732
- const summaryData = {};
733
- if (result && Object.keys(result).length > 0) {
734
- let isSharded = false;
735
- const shardedCollections = {
736
- 'sharded_user_profile': config.shardedUserProfileCollection,
737
- 'sharded_user_profitability': config.shardedProfitabilityCollection
738
- };
739
- for (const resultKey in shardedCollections) {
740
- if (result[resultKey]) {
741
- isSharded = true;
742
- const shardCollectionName = shardedCollections[resultKey];
743
- if (!shardCollectionName) {
744
- logger.log('ERROR', `[${passName}] Missing config key for sharded collection: ${resultKey}`);
745
- continue;
746
- }
747
- const shardedData = result[resultKey];
748
- for (const shardId in shardedData) {
749
- const shardDocData = shardedData[shardId];
750
- if (shardDocData && (Object.keys(shardDocData).length > 0)) {
751
- const shardRef = db.collection(shardCollectionName).doc(shardId);
752
- pendingWrites.push({ ref: shardRef, data: shardDocData, merge: true });
753
- }
754
- }
755
- const { [resultKey]: _, ...otherResults } = result;
756
- result = otherResults;
757
- }
758
- }
759
- if (result && Object.keys(result).length > 0) {
760
- const computationDocRef = resultsCollectionRef.doc(category)
761
- .collection(compsSub)
762
- .doc(calcName);
763
- pendingWrites.push({ ref: computationDocRef, data: result });
764
- }
765
- if (!summaryData[category]) summaryData[category] = {};
766
- summaryData[category][calcName] = true;
767
- if (Object.keys(summaryData).length > 0) {
768
- const topLevelDocRef = db.collection(config.resultsCollection).doc(dateStr);
769
- pendingWrites.push({ ref: topLevelDocRef, data: summaryData });
770
- }
771
- if (pendingWrites.length > 0) {
772
- await commitBatchInChunks(
773
- config,
774
- dependencies,
775
- pendingWrites,
776
- `Commit ${passName} ${dateStr} [${calcName}]`
777
- );
778
- successCount++;
779
- }
780
- } else {
781
- logger.log('WARN', `[${passName}] Meta-calculation ${calcName} produced no results for ${dateStr}. Skipping write.`);
782
- }
783
- } catch (e) {
784
- logger.log('ERROR', `[${passName}] Meta-calc process/commit failed for ${calcName} on ${dateStr}`, { err: e.message, stack: e.stack });
785
- }
786
- }
787
-
788
- const completionStatus = successCount === calculationsToRun.length ? 'SUCCESS' : 'WARN';
789
- logger.log(completionStatus, `[${passName}] Completed ${dateStr}. Success: ${successCount}/${calculationsToRun.length}.`);
790
-
791
- } catch (err) {
792
- logger.log('ERROR', `[${passName}] Fatal error for ${dateStr}`, { errorMessage: err.message, stack: err.stack });
793
- throw err;
794
- }
44
+ logger.log('INFO', `[PassRunner] Pass ${passToRun} orchestration finished.`);
795
45
  }
796
46
 
797
-
798
- module.exports = {
799
- runComputationPass,
800
- };
47
+ module.exports = { runComputationPass };