bulltrackers-module 1.0.131 → 1.0.132

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.
@@ -6,9 +6,11 @@
6
6
  * It reads its pass number from the config and executes only those calculations.
7
7
  * This file contains the high-level "manual" of steps. The "how-to" logic
8
8
  * is extracted into 'computation_system_utils.js'.
9
+ * --- MODIFIED: To use getEarliestDataDates and pass the date map to the orchestrator helpers. ---
9
10
  */
10
11
  const { groupByPass, checkRootDataAvailability, fetchDependenciesForPass, filterCalculations, runStandardComputationPass, runMetaComputationPass } = require('./orchestration_helpers.js');
11
- const { getExpectedDateStrings, getFirstDateFromSourceData } = require('../utils/utils.js');
12
+ // --- MODIFIED: Import getEarliestDataDates ---
13
+ const { getExpectedDateStrings, getEarliestDataDates } = require('../utils/utils.js');
12
14
 
13
15
  async function runComputationPass(config, dependencies, computationManifest) {
14
16
  const { logger } = dependencies;
@@ -17,7 +19,12 @@ async function runComputationPass(config, dependencies, computationManifest) {
17
19
 
18
20
  const yesterday = new Date(); yesterday.setUTCDate(yesterday.getUTCDate()-1);
19
21
  const endDateUTC = new Date(Date.UTC(yesterday.getUTCFullYear(), yesterday.getUTCMonth(), yesterday.getUTCDate()));
20
- const firstDate = await getFirstDateFromSourceData(config, dependencies);
22
+
23
+ // --- MODIFIED: Call new date function ---
24
+ const earliestDates = await getEarliestDataDates(config, dependencies);
25
+ const firstDate = earliestDates.absoluteEarliest; // Use the absolute earliest for the loop
26
+ // --- END MODIFICATION ---
27
+
21
28
  const startDateUTC = firstDate ? new Date(Date.UTC(firstDate.getUTCFullYear(), firstDate.getUTCMonth(), firstDate.getUTCDate())) : new Date(config.earliestComputationDate+'T00:00:00Z');
22
29
  const allExpectedDates = getExpectedDateStrings(startDateUTC, endDateUTC);
23
30
 
@@ -30,7 +37,10 @@ async function runComputationPass(config, dependencies, computationManifest) {
30
37
  for (const dateStr of allExpectedDates) {
31
38
  const dateToProcess = new Date(dateStr+'T00:00:00Z');
32
39
  try {
33
- const rootData = await checkRootDataAvailability(dateStr, config, dependencies); if (!rootData) continue;
40
+ // --- MODIFIED: Pass earliestDates map to checkRootDataAvailability ---
41
+ const rootData = await checkRootDataAvailability(dateStr, config, dependencies, earliestDates); if (!rootData) continue;
42
+ // --- END MODIFICATION ---
43
+
34
44
  const fetchedDeps = await fetchDependenciesForPass(dateStr, calcsInThisPass, computationManifest, config, dependencies);
35
45
  const { standardCalcsToRun, metaCalcsToRun } = filterCalculations(standardCalcs, metaCalcs, rootData.status, fetchedDeps, passToRun, dateStr, logger);
36
46
  if (standardCalcsToRun.length) await runStandardComputationPass(dateToProcess, standardCalcsToRun, `Pass ${passToRun} (Standard)`, config, dependencies, rootData);
@@ -44,4 +54,4 @@ async function runComputationPass(config, dependencies, computationManifest) {
44
54
  logger.log('INFO', `[PassRunner] Pass ${passToRun} orchestration finished.`);
45
55
  }
46
56
 
47
- module.exports = { runComputationPass };
57
+ module.exports = { runComputationPass };
@@ -5,30 +5,96 @@ const { normalizeName, commitBatchInChunks } = require('../utils/utils.js');
5
5
  /** Stage 1: Group manifest by pass number */
6
6
  function groupByPass(manifest) { return manifest.reduce((acc, calc) => { (acc[calc.pass] = acc[calc.pass] || []).push(calc); return acc; }, {}); }
7
7
 
8
- /** Stage 2: Check root data dependencies for a calc */
9
- function checkRootDependencies(calcManifest, rootDataStatus) { if (!calcManifest.rootDataDependencies || !calcManifest.rootDataDependencies.length) return true;
10
- for (const dep of calcManifest.rootDataDependencies) if ((dep==='portfolio'&&!rootDataStatus.hasPortfolio)||(dep==='insights'&&!rootDataStatus.hasInsights)||(dep==='social'&&!rootDataStatus.hasSocial)||(dep==='history'&&!rootDataStatus.hasHistory)) return false;
11
- return true;
8
+ /** * --- MODIFIED: Returns detailed missing dependencies for logging ---
9
+ * Stage 2: Check root data dependencies for a calc
10
+ */
11
+ function checkRootDependencies(calcManifest, rootDataStatus) {
12
+ const missing = [];
13
+ if (!calcManifest.rootDataDependencies || !calcManifest.rootDataDependencies.length) {
14
+ return { canRun: true, missing };
15
+ }
16
+ for (const dep of calcManifest.rootDataDependencies) {
17
+ if (dep === 'portfolio' && !rootDataStatus.hasPortfolio) missing.push('portfolio');
18
+ else if (dep === 'insights' && !rootDataStatus.hasInsights) missing.push('insights');
19
+ else if (dep === 'social' && !rootDataStatus.hasSocial) missing.push('social');
20
+ else if (dep === 'history' && !rootDataStatus.hasHistory) missing.push('history');
21
+ }
22
+ return { canRun: missing.length === 0, missing };
12
23
  }
13
24
 
14
- /** Stage 3: Check root data availability for a date */
15
- // --- FIX: Passes the full 'dependencies' object down ---
16
- async function checkRootDataAvailability(dateStr, config, dependencies) {
17
- const { logger } = dependencies; // Destructure logger for local use
25
+ /** * --- MODIFIED: Uses earliestDates map to avoid unnecessary queries ---
26
+ * Stage 3: Check root data availability for a date
27
+ */
28
+ async function checkRootDataAvailability(dateStr, config, dependencies, earliestDates) {
29
+ const { logger } = dependencies;
18
30
  logger.log('INFO', `[PassRunner] Checking root data for ${dateStr}...`);
31
+
32
+ const dateToProcess = new Date(dateStr + 'T00:00:00Z');
33
+ let portfolioRefs = [], insightsData = null, socialData = null, historyRefs = [];
34
+ let hasPortfolio = false, hasInsights = false, hasSocial = false, hasHistory = false;
35
+
19
36
  try {
20
- const [portfolioRefs, insightsData, socialData, historyRefs] = await Promise.all([
21
- getPortfolioPartRefs(config, dependencies, dateStr), // Pass full 'dependencies'
22
- loadDailyInsights(config, dependencies, dateStr), // Pass full 'dependencies'
23
- loadDailySocialPostInsights(config, dependencies, dateStr), // Pass full 'dependencies'
24
- getHistoryPartRefs(config, dependencies, dateStr) // Pass full 'dependencies'
25
- ]);
26
- const hasPortfolio = !!(portfolioRefs?.length), hasInsights = !!insightsData, hasSocial = !!socialData, hasHistory = !!(historyRefs?.length);
27
- if (!(hasPortfolio||hasInsights||hasSocial||hasHistory)) { logger.log('WARN', `[PassRunner] No root data for ${dateStr}.`); return null; }
28
- return { portfolioRefs: portfolioRefs||[], insightsData: insightsData||null, socialData: socialData||null, historyRefs: historyRefs||[], status: { hasPortfolio, hasInsights, hasSocial, hasHistory } };
29
- } catch (err) { logger.log('ERROR', `[PassRunner] Error checking data for ${dateStr}`, { errorMessage: err.message }); return null; }
37
+ const tasks = [];
38
+
39
+ if (dateToProcess >= earliestDates.portfolio) {
40
+ tasks.push(
41
+ getPortfolioPartRefs(config, dependencies, dateStr).then(res => {
42
+ portfolioRefs = res;
43
+ hasPortfolio = !!(res?.length);
44
+ })
45
+ );
46
+ }
47
+
48
+ if (dateToProcess >= earliestDates.insights) {
49
+ tasks.push(
50
+ loadDailyInsights(config, dependencies, dateStr).then(res => {
51
+ insightsData = res;
52
+ hasInsights = !!res;
53
+ })
54
+ );
55
+ }
56
+
57
+ if (dateToProcess >= earliestDates.social) {
58
+ tasks.push(
59
+ loadDailySocialPostInsights(config, dependencies, dateStr).then(res => {
60
+ socialData = res;
61
+ hasSocial = !!res;
62
+ })
63
+ );
64
+ }
65
+
66
+ if (dateToProcess >= earliestDates.history) {
67
+ tasks.push(
68
+ getHistoryPartRefs(config, dependencies, dateStr).then(res => {
69
+ historyRefs = res;
70
+ hasHistory = !!(res?.length);
71
+ })
72
+ );
73
+ }
74
+
75
+ await Promise.all(tasks);
76
+
77
+ if (!(hasPortfolio || hasInsights || hasSocial || hasHistory)) {
78
+ logger.log('WARN', `[PassRunner] No root data for ${dateStr}.`);
79
+ return null;
80
+ }
81
+
82
+ return {
83
+ portfolioRefs,
84
+ insightsData,
85
+ socialData,
86
+ historyRefs,
87
+ status: { hasPortfolio, hasInsights, hasSocial, hasHistory }
88
+ };
89
+
90
+ } catch (err) {
91
+ logger.log('ERROR', `[PassRunner] Error checking data for ${dateStr}`, { errorMessage: err.message });
92
+ return null;
93
+ }
30
94
  }
31
95
 
96
+
97
+
32
98
  /** Stage 4: Fetch computed dependencies from Firestore */
33
99
  async function fetchDependenciesForPass(dateStr, calcsInPass, fullManifest, config, { db, logger }) {
34
100
  const manifestMap = new Map(fullManifest.map(c => [normalizeName(c.name), c]));
@@ -47,14 +113,47 @@ async function fetchDependenciesForPass(dateStr, calcsInPass, fullManifest, conf
47
113
  return fetched;
48
114
  }
49
115
 
50
- /** Stage 5: Filter calculations based on available root data and dependencies */
116
+ /** * --- MODIFIED: Added detailed logging ---
117
+ * Stage 5: Filter calculations based on available root data and dependencies
118
+ */
51
119
  function filterCalculations(standardCalcs, metaCalcs, rootDataStatus, fetchedDeps, passToRun, dateStr, logger) {
52
120
  const skipped = new Set();
53
- const standardCalcsToRun = standardCalcs.filter(c => checkRootDependencies(c, rootDataStatus) || (logger.log('INFO', `[Pass ${passToRun}] Skipping ${c.name} missing root data`), skipped.add(c.name), false));
54
- const metaCalcsToRun = metaCalcs.filter(c => checkRootDependencies(c, rootDataStatus) && (c.dependencies||[]).every(d=>fetchedDeps[normalizeName(d)]) || (logger.log('WARN', `[Pass ${passToRun} Meta] Skipping ${c.name} missing dep`), skipped.add(c.name), false));
121
+
122
+ // Filter Standard Calcs
123
+ const standardCalcsToRun = standardCalcs.filter(c => {
124
+ const { canRun, missing } = checkRootDependencies(c, rootDataStatus);
125
+ if (canRun) return true;
126
+
127
+ logger.log('INFO', `[Pass ${passToRun}] Skipping ${c.name} for ${dateStr}. Missing root data: [${missing.join(', ')}]`);
128
+ skipped.add(c.name);
129
+ return false;
130
+ });
131
+
132
+ // Filter Meta Calcs
133
+ const metaCalcsToRun = metaCalcs.filter(c => {
134
+ // 1. Check root data
135
+ const { canRun, missing: missingRoot } = checkRootDependencies(c, rootDataStatus);
136
+ if (!canRun) {
137
+ logger.log('INFO', `[Pass ${passToRun} Meta] Skipping ${c.name} for ${dateStr}. Missing root data: [${missingRoot.join(', ')}]`);
138
+ skipped.add(c.name);
139
+ return false;
140
+ }
141
+
142
+ // 2. Check computed dependencies
143
+ const missingDeps = (c.dependencies || []).map(normalizeName).filter(d => !fetchedDeps[d]);
144
+ if (missingDeps.length > 0) {
145
+ logger.log('WARN', `[Pass ${passToRun} Meta] Skipping ${c.name} for ${dateStr}. Missing computed deps: [${missingDeps.join(', ')}]`);
146
+ skipped.add(c.name);
147
+ return false;
148
+ }
149
+
150
+ return true; // All checks passed
151
+ });
152
+
55
153
  return { standardCalcsToRun, metaCalcsToRun };
56
154
  }
57
155
 
156
+
58
157
  /** Stage 6: Initialize calculator instances */
59
158
  function initializeCalculators(calcs, logger) { const state = {}; for (const c of calcs) { const name=normalizeName(c.name), Cl=c.class; if(typeof Cl==='function') try { const inst=new Cl(); inst.manifest=c; state[name]=inst; } catch(e){logger.warn(`Init failed ${name}`,{errorMessage:e.message}); state[name]=null;} else {logger.warn(`Class missing ${name}`); state[name]=null;} } return state; }
60
159
 
@@ -2,8 +2,10 @@
2
2
  * @fileoverview Computation system sub-pipes and utils.
3
3
  * REFACTORED: Now stateless and receive dependencies where needed.
4
4
  * DYNAMIC: Categorization logic is removed, replaced by manifest.
5
+ * --- MODIFIED: getFirstDateFromSourceData is now getEarliestDataDates
6
+ * and queries all data sources to build an availability map. ---
5
7
  */
6
- /** --- Computation System Sub-Pipes & Utils (Stateless) --- */
8
+ /** --- Computation System Sub-Pipes & Utils (Stateless, Dependency-Injection) --- */
7
9
 
8
10
  const { FieldValue, FieldPath } = require('@google-cloud/firestore');
9
11
 
@@ -46,13 +48,46 @@ function getExpectedDateStrings(startDate, endDate) {
46
48
  return dateStrings;
47
49
  }
48
50
 
49
- /** Stage 4: Get the earliest date in a collection */
51
+ /**
52
+ * --- NEW HELPER ---
53
+ * Stage 4: Get the earliest date in a *flat* collection where doc IDs are dates.
54
+ */
55
+ async function getFirstDateFromSimpleCollection(config, deps, collectionName) {
56
+ const { db, logger, calculationUtils } = deps;
57
+ const { withRetry } = calculationUtils;
58
+
59
+ try {
60
+ if (!collectionName) {
61
+ logger.log('WARN', `[Core Utils] Collection name not provided for simple date query.`);
62
+ return null;
63
+ }
64
+ const query = db.collection(collectionName)
65
+ .where(FieldPath.documentId(), '>=', '2000-01-01')
66
+ .orderBy(FieldPath.documentId(), 'asc')
67
+ .limit(1);
68
+
69
+ const snapshot = await withRetry(() => query.get(), `GetEarliestDoc(${collectionName})`);
70
+
71
+ if (!snapshot.empty && /^\d{4}-\d{2}-\d{2}$/.test(snapshot.docs[0].id)) {
72
+ return new Date(snapshot.docs[0].id + 'T00:00:00Z');
73
+ }
74
+ } catch (e) {
75
+ logger.log('ERROR', `GetFirstDate failed for ${collectionName}`, { errorMessage: e.message });
76
+ }
77
+ return null;
78
+ }
79
+
80
+ /** Stage 4: Get the earliest date in a sharded collection */
50
81
  async function getFirstDateFromCollection(config, deps, collectionName) {
51
82
  const { db, logger, calculationUtils } = deps;
52
83
  const { withRetry } = calculationUtils;
53
84
 
54
85
  let earliestDate = null;
55
86
  try {
87
+ if (!collectionName) {
88
+ logger.log('WARN', `[Core Utils] Collection name not provided for sharded date query.`);
89
+ return null;
90
+ }
56
91
  const blockDocRefs = await withRetry(() => db.collection(collectionName).listDocuments(), `GetBlocks(${collectionName})`);
57
92
  if (!blockDocRefs.length) { logger.log('WARN', `No block documents in collection: ${collectionName}`); return null; }
58
93
 
@@ -75,26 +110,69 @@ async function getFirstDateFromCollection(config, deps, collectionName) {
75
110
  return earliestDate;
76
111
  }
77
112
 
78
- /** Stage 5: Determine the earliest date from source data across both user types */
79
- async function getFirstDateFromSourceData(config, deps) {
113
+ /** * --- MODIFIED FUNCTION ---
114
+ * Stage 5: Determine the earliest date from *all* source data.
115
+ */
116
+ async function getEarliestDataDates(config, deps) {
80
117
  const { logger } = deps;
81
- logger.log('INFO', 'Querying for earliest date from source portfolio data...');
82
-
83
- const investorDate = await getFirstDateFromCollection(config, deps, config.normalUserPortfolioCollection);
84
- const speculatorDate = await getFirstDateFromCollection(config, deps, config.speculatorPortfolioCollection);
85
-
86
- let earliestDate;
87
- if (investorDate && speculatorDate) earliestDate = investorDate < speculatorDate ? investorDate : speculatorDate;
88
- else earliestDate = investorDate || speculatorDate;
89
-
90
- if (earliestDate) {
91
- logger.log('INFO', `Found earliest source data date: ${earliestDate.toISOString().slice(0, 10)}`);
92
- return earliestDate;
93
- } else {
94
- const fallbackDate = new Date(config.earliestComputationDate + 'T00:00:00Z' || '2023-01-01T00:00:00Z');
95
- logger.log('WARN', `No source data found. Defaulting first date to: ${fallbackDate.toISOString().slice(0, 10)}`);
96
- return fallbackDate;
97
- }
118
+ logger.log('INFO', 'Querying for earliest date from ALL source data collections...');
119
+
120
+ // These return null on error or if empty
121
+ const [
122
+ investorDate,
123
+ speculatorDate,
124
+ investorHistoryDate,
125
+ speculatorHistoryDate,
126
+ insightsDate,
127
+ socialDate
128
+ ] = await Promise.all([
129
+ getFirstDateFromCollection(config, deps, config.normalUserPortfolioCollection),
130
+ getFirstDateFromCollection(config, deps, config.speculatorPortfolioCollection),
131
+ getFirstDateFromCollection(config, deps, config.normalUserHistoryCollection),
132
+ getFirstDateFromCollection(config, deps, config.speculatorHistoryCollection),
133
+ getFirstDateFromSimpleCollection(config, deps, config.insightsCollectionName),
134
+ getFirstDateFromSimpleCollection(config, deps, config.socialInsightsCollectionName)
135
+ ]);
136
+
137
+ // Helper to find the minimum (earliest) of a set of dates
138
+ const getMinDate = (...dates) => {
139
+ const validDates = dates.filter(Boolean); // Filter out nulls
140
+ if (validDates.length === 0) return null;
141
+ return new Date(Math.min(...validDates));
142
+ };
143
+
144
+ const earliestPortfolioDate = getMinDate(investorDate, speculatorDate);
145
+ const earliestHistoryDate = getMinDate(investorHistoryDate, speculatorHistoryDate);
146
+ const earliestInsightsDate = getMinDate(insightsDate); // Already a single date
147
+ const earliestSocialDate = getMinDate(socialDate); // Already a single date
148
+
149
+ const absoluteEarliest = getMinDate(
150
+ earliestPortfolioDate,
151
+ earliestHistoryDate,
152
+ earliestInsightsDate,
153
+ earliestSocialDate
154
+ );
155
+
156
+ // Fallback date
157
+ const fallbackDate = new Date(config.earliestComputationDate + 'T00:00:00Z' || '2023-01-01T00:00:00Z');
158
+
159
+ const result = {
160
+ portfolio: earliestPortfolioDate || new Date('2999-12-31'), // Use a 'far future' date if null
161
+ history: earliestHistoryDate || new Date('2999-12-31'),
162
+ insights: earliestInsightsDate || new Date('2999-12-31'),
163
+ social: earliestSocialDate || new Date('2999-12-31'),
164
+ absoluteEarliest: absoluteEarliest || fallbackDate // Use fallback for the main loop
165
+ };
166
+
167
+ logger.log('INFO', 'Earliest data availability map built:', {
168
+ portfolio: result.portfolio.toISOString().slice(0, 10),
169
+ history: result.history.toISOString().slice(0, 10),
170
+ insights: result.insights.toISOString().slice(0, 10),
171
+ social: result.social.toISOString().slice(0, 10),
172
+ absoluteEarliest: result.absoluteEarliest.toISOString().slice(0, 10)
173
+ });
174
+
175
+ return result;
98
176
  }
99
177
 
100
178
  module.exports = {
@@ -103,5 +181,6 @@ module.exports = {
103
181
  normalizeName,
104
182
  commitBatchInChunks,
105
183
  getExpectedDateStrings,
106
- getFirstDateFromSourceData,
107
- };
184
+ // getFirstDateFromSourceData, // This is replaced
185
+ getEarliestDataDates, // <-- EXPORT NEW FUNCTION
186
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.131",
3
+ "version": "1.0.132",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [