bulltrackers-module 1.0.126 → 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.
- package/functions/computation-system/helpers/computation_pass_runner.js +20 -773
- package/functions/computation-system/helpers/orchestration_helpers.js +88 -867
- package/functions/computation-system/utils/data_loader.js +58 -147
- package/functions/computation-system/utils/utils.js +55 -98
- package/functions/orchestrator/helpers/discovery_helpers.js +40 -188
- package/functions/orchestrator/helpers/update_helpers.js +21 -61
- package/functions/orchestrator/index.js +42 -121
- package/functions/task-engine/handler_creator.js +22 -143
- package/functions/task-engine/helpers/discover_helpers.js +20 -90
- package/functions/task-engine/helpers/update_helpers.js +90 -185
- package/functions/task-engine/helpers/verify_helpers.js +43 -159
- package/functions/task-engine/utils/firestore_batch_manager.js +97 -290
- package/functions/task-engine/utils/task_engine_utils.js +99 -0
- package/package.json +1 -1
- package/functions/task-engine/utils/api_calls.js +0 -0
- package/functions/task-engine/utils/firestore_ops.js +0 -0
|
@@ -12,231 +12,142 @@
|
|
|
12
12
|
* @param {string} dateString - The date in YYYY-MM-DD format.
|
|
13
13
|
* @returns {Promise<Firestore.DocumentReference[]>} An array of DocumentReferences.
|
|
14
14
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
/** --- Data Loader Sub-Pipes (Stateless, Dependency-Injection) --- */
|
|
16
|
+
|
|
17
|
+
/** Stage 1: Get portfolio part document references for a given date */
|
|
18
|
+
async function getPortfolioPartRefs(config, deps, dateString) {
|
|
19
|
+
const { db, logger, calculationUtils } = deps;
|
|
18
20
|
const { withRetry } = calculationUtils;
|
|
19
|
-
// <<< END FIX >>>
|
|
20
21
|
|
|
21
22
|
logger.log('INFO', `Getting portfolio part references for date: ${dateString}`);
|
|
22
23
|
const allPartRefs = [];
|
|
23
24
|
const collectionsToQuery = [config.normalUserPortfolioCollection, config.speculatorPortfolioCollection];
|
|
24
25
|
|
|
25
26
|
for (const collectionName of collectionsToQuery) {
|
|
26
|
-
const blockDocsQuery = db.collection(collectionName);
|
|
27
|
-
const blockDocRefs = await withRetry(
|
|
28
|
-
|
|
29
|
-
`listDocuments(${collectionName})`
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
if (blockDocRefs.length === 0) {
|
|
33
|
-
logger.log('WARN', `No block documents found in collection: ${collectionName}`);
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
27
|
+
const blockDocsQuery = db.collection(collectionName);
|
|
28
|
+
const blockDocRefs = await withRetry(() => blockDocsQuery.listDocuments(), `listDocuments(${collectionName})`);
|
|
29
|
+
if (!blockDocRefs.length) { logger.log('WARN', `No block documents in ${collectionName}`); continue; }
|
|
36
30
|
|
|
37
31
|
for (const blockDocRef of blockDocRefs) {
|
|
38
32
|
const partsCollectionRef = blockDocRef.collection(config.snapshotsSubcollection).doc(dateString).collection(config.partsSubcollection);
|
|
39
|
-
const partDocs = await withRetry(
|
|
40
|
-
() => partsCollectionRef.listDocuments(),
|
|
41
|
-
`listDocuments(${partsCollectionRef.path})`
|
|
42
|
-
);
|
|
33
|
+
const partDocs = await withRetry(() => partsCollectionRef.listDocuments(), `listDocuments(${partsCollectionRef.path})`);
|
|
43
34
|
allPartRefs.push(...partDocs);
|
|
44
35
|
}
|
|
45
36
|
}
|
|
46
37
|
|
|
47
|
-
logger.log('INFO', `Found ${allPartRefs.length} part
|
|
38
|
+
logger.log('INFO', `Found ${allPartRefs.length} portfolio part refs for ${dateString}`);
|
|
48
39
|
return allPartRefs;
|
|
49
40
|
}
|
|
50
41
|
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
* @param {object} dependencies - Contains db, logger, calculationUtils.
|
|
55
|
-
* @param {Firestore.DocumentReference[]} refs - An array of DocumentReferences to load.
|
|
56
|
-
* @returns {Promise<object>} A single map of { [userId]: portfolioData }.
|
|
57
|
-
*/
|
|
58
|
-
async function loadDataByRefs(config, dependencies, refs) {
|
|
59
|
-
// <<< FIX: Destructure all dependencies here, inside the function >>>
|
|
60
|
-
const { db, logger, calculationUtils } = dependencies;
|
|
42
|
+
/** Stage 2: Load data from an array of document references */
|
|
43
|
+
async function loadDataByRefs(config, deps, refs) {
|
|
44
|
+
const { db, logger, calculationUtils } = deps;
|
|
61
45
|
const { withRetry } = calculationUtils;
|
|
62
|
-
// <<< END FIX >>>
|
|
63
46
|
|
|
64
|
-
if (!refs || refs.length
|
|
47
|
+
if (!refs || !refs.length) return {};
|
|
65
48
|
const mergedPortfolios = {};
|
|
66
49
|
const batchSize = config.partRefBatchSize || 50;
|
|
67
50
|
|
|
68
51
|
for (let i = 0; i < refs.length; i += batchSize) {
|
|
69
52
|
const batchRefs = refs.slice(i, i + batchSize);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
() => db.getAll(...batchRefs),
|
|
73
|
-
`getAll(batch ${Math.floor(i / batchSize)})`
|
|
74
|
-
);
|
|
53
|
+
const snapshots = await withRetry(() => db.getAll(...batchRefs), `getAll(batch ${Math.floor(i / batchSize)})`);
|
|
54
|
+
|
|
75
55
|
for (const doc of snapshots) {
|
|
76
|
-
if (doc.exists)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
} else {
|
|
81
|
-
logger.log('WARN', `Document ${doc.id} exists but data is not an object. Data:`, data);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
56
|
+
if (!doc.exists) continue;
|
|
57
|
+
const data = doc.data();
|
|
58
|
+
if (data && typeof data === 'object') Object.assign(mergedPortfolios, data);
|
|
59
|
+
else logger.log('WARN', `Doc ${doc.id} exists but data is not an object`, data);
|
|
84
60
|
}
|
|
85
61
|
}
|
|
62
|
+
|
|
86
63
|
return mergedPortfolios;
|
|
87
64
|
}
|
|
88
65
|
|
|
89
|
-
/**
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
* @param {Firestore.DocumentReference[]} partRefs - Array of part document references.
|
|
94
|
-
* @returns {Promise<object>} A single map of { [userId]: portfolioData }.
|
|
95
|
-
*/
|
|
96
|
-
async function loadFullDayMap(config, dependencies, partRefs) {
|
|
97
|
-
// <<< FIX: Destructure only what's needed for this specific function >>>
|
|
98
|
-
const { logger } = dependencies;
|
|
99
|
-
// <<< END FIX >>>
|
|
66
|
+
/** Stage 3: Load a full day map by delegating to loadDataByRefs */
|
|
67
|
+
async function loadFullDayMap(config, deps, partRefs) {
|
|
68
|
+
const { logger } = deps;
|
|
69
|
+
if (!partRefs.length) return {};
|
|
100
70
|
|
|
101
|
-
if (partRefs.length === 0) return {};
|
|
102
71
|
logger.log('TRACE', `Loading full day map from ${partRefs.length} references...`);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const fullMap = await loadDataByRefs(config, dependencies, partRefs);
|
|
106
|
-
logger.log('TRACE', `Full day map loaded with ${Object.keys(fullMap).length} users.`);
|
|
72
|
+
const fullMap = await loadDataByRefs(config, deps, partRefs);
|
|
73
|
+
logger.log('TRACE', `Full day map loaded with ${Object.keys(fullMap).length} users`);
|
|
107
74
|
return fullMap;
|
|
108
75
|
}
|
|
109
76
|
|
|
110
|
-
/**
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
* @param {object} config - The computation system configuration object.
|
|
114
|
-
* @param {object} dependencies - Contains db, logger, calculationUtils.
|
|
115
|
-
* @param {string} dateString - The date in YYYY-MM-DD format.
|
|
116
|
-
* @returns {Promise<object|null>} The insights data object or null if not found/error.
|
|
117
|
-
*/
|
|
118
|
-
async function loadDailyInsights(config, dependencies, dateString) {
|
|
119
|
-
// <<< FIX: Destructure all dependencies here, inside the function >>>
|
|
120
|
-
const { db, logger, calculationUtils } = dependencies;
|
|
77
|
+
/** Stage 4: Load daily instrument insights */
|
|
78
|
+
async function loadDailyInsights(config, deps, dateString) {
|
|
79
|
+
const { db, logger, calculationUtils } = deps;
|
|
121
80
|
const { withRetry } = calculationUtils;
|
|
122
|
-
// <<< END FIX >>>
|
|
123
81
|
|
|
124
|
-
const insightsCollectionName = config.insightsCollectionName || 'daily_instrument_insights';
|
|
125
|
-
logger.log('INFO', `Loading daily insights for
|
|
82
|
+
const insightsCollectionName = config.insightsCollectionName || 'daily_instrument_insights';
|
|
83
|
+
logger.log('INFO', `Loading daily insights for ${dateString} from ${insightsCollectionName}`);
|
|
84
|
+
|
|
126
85
|
try {
|
|
127
86
|
const docRef = db.collection(insightsCollectionName).doc(dateString);
|
|
128
87
|
const docSnap = await withRetry(() => docRef.get(), `getInsights(${dateString})`);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
logger.log('WARN', `Daily insights document not found for ${dateString}`);
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
logger.log('TRACE', `Successfully loaded insights for ${dateString}.`);
|
|
88
|
+
if (!docSnap.exists) { logger.log('WARN', `Insights not found for ${dateString}`); return null; }
|
|
89
|
+
logger.log('TRACE', `Successfully loaded insights for ${dateString}`);
|
|
135
90
|
return docSnap.data();
|
|
136
91
|
} catch (error) {
|
|
137
92
|
logger.log('ERROR', `Failed to load daily insights for ${dateString}`, { errorMessage: error.message });
|
|
138
|
-
return null;
|
|
93
|
+
return null;
|
|
139
94
|
}
|
|
140
95
|
}
|
|
141
96
|
|
|
142
|
-
/**
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
* Fetches all analyzed social post documents for a specific date.
|
|
146
|
-
* @param {object} config - The computation system configuration object.
|
|
147
|
-
* @param {object} dependencies - Contains db, logger, calculationUtils.
|
|
148
|
-
* @param {string} dateString - The date in YYYY-MM-DD format.
|
|
149
|
-
* @returns {Promise<object|null>} An object map of { [postId]: postData } or null.
|
|
150
|
-
*/
|
|
151
|
-
async function loadDailySocialPostInsights(config, dependencies, dateString) {
|
|
152
|
-
// <<< FIX: Destructure all dependencies here, inside the function >>>
|
|
153
|
-
const { db, logger, calculationUtils } = dependencies;
|
|
97
|
+
/** Stage 5: Load daily social post insights */
|
|
98
|
+
async function loadDailySocialPostInsights(config, deps, dateString) {
|
|
99
|
+
const { db, logger, calculationUtils } = deps;
|
|
154
100
|
const { withRetry } = calculationUtils;
|
|
155
|
-
// <<< END FIX >>>
|
|
156
101
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
102
|
+
const collectionName = config.socialInsightsCollectionName || 'daily_social_insights';
|
|
103
|
+
logger.log('INFO', `Loading social post insights for ${dateString} from ${collectionName}`);
|
|
104
|
+
|
|
161
105
|
try {
|
|
162
|
-
const postsCollectionRef = db.collection(
|
|
106
|
+
const postsCollectionRef = db.collection(collectionName).doc(dateString).collection('posts');
|
|
163
107
|
const querySnapshot = await withRetry(() => postsCollectionRef.get(), `getSocialPosts(${dateString})`);
|
|
108
|
+
if (querySnapshot.empty) { logger.log('WARN', `No social post insights for ${dateString}`); return null; }
|
|
164
109
|
|
|
165
|
-
if (querySnapshot.empty) {
|
|
166
|
-
logger.log('WARN', `No social post insights found for ${dateString}.`);
|
|
167
|
-
return null;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
110
|
const postsMap = {};
|
|
171
|
-
querySnapshot.forEach(doc => {
|
|
172
|
-
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
logger.log('TRACE', `Successfully loaded ${Object.keys(postsMap).length} social post insights for ${dateString}.`);
|
|
111
|
+
querySnapshot.forEach(doc => { postsMap[doc.id] = doc.data(); });
|
|
112
|
+
logger.log('TRACE', `Loaded ${Object.keys(postsMap).length} social post insights`);
|
|
176
113
|
return postsMap;
|
|
177
|
-
|
|
178
114
|
} catch (error) {
|
|
179
115
|
logger.log('ERROR', `Failed to load social post insights for ${dateString}`, { errorMessage: error.message });
|
|
180
116
|
return null;
|
|
181
117
|
}
|
|
182
118
|
}
|
|
183
|
-
// --- END NEW ---
|
|
184
|
-
|
|
185
119
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
* Sub-pipe: pipe.computationSystem.dataLoader.getHistoryPartRefs
|
|
190
|
-
* @param {object} config - The computation system configuration object.
|
|
191
|
-
* @param {object} dependencies - Contains db, logger, calculationUtils.
|
|
192
|
-
* @param {string} dateString - The date in YYYY-MM-DD format.
|
|
193
|
-
* @returns {Promise<Firestore.DocumentReference[]>} An array of DocumentReferences.
|
|
194
|
-
*/
|
|
195
|
-
async function getHistoryPartRefs(config, dependencies, dateString) {
|
|
196
|
-
const { db, logger, calculationUtils } = dependencies;
|
|
120
|
+
/** Stage 6: Get history part references for a given date */
|
|
121
|
+
async function getHistoryPartRefs(config, deps, dateString) {
|
|
122
|
+
const { db, logger, calculationUtils } = deps;
|
|
197
123
|
const { withRetry } = calculationUtils;
|
|
198
124
|
|
|
199
|
-
logger.log('INFO', `Getting history part references for
|
|
125
|
+
logger.log('INFO', `Getting history part references for ${dateString}`);
|
|
200
126
|
const allPartRefs = [];
|
|
201
|
-
// --- MODIFIED: Use new history collection config keys ---
|
|
202
127
|
const collectionsToQuery = [config.normalUserHistoryCollection, config.speculatorHistoryCollection];
|
|
203
128
|
|
|
204
129
|
for (const collectionName of collectionsToQuery) {
|
|
205
|
-
if (!collectionName) {
|
|
206
|
-
logger.log('WARN', `History collection name is undefined. Skipping.`);
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
130
|
+
if (!collectionName) { logger.log('WARN', `History collection undefined. Skipping.`); continue; }
|
|
209
131
|
const blockDocsQuery = db.collection(collectionName);
|
|
210
|
-
const blockDocRefs = await withRetry(
|
|
211
|
-
|
|
212
|
-
`listDocuments(${collectionName})`
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
if (blockDocRefs.length === 0) {
|
|
216
|
-
logger.log('WARN', `No block documents found in collection: ${collectionName}`);
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
132
|
+
const blockDocRefs = await withRetry(() => blockDocsQuery.listDocuments(), `listDocuments(${collectionName})`);
|
|
133
|
+
if (!blockDocRefs.length) { logger.log('WARN', `No block documents in ${collectionName}`); continue; }
|
|
219
134
|
|
|
220
135
|
for (const blockDocRef of blockDocRefs) {
|
|
221
136
|
const partsCollectionRef = blockDocRef.collection(config.snapshotsSubcollection).doc(dateString).collection(config.partsSubcollection);
|
|
222
|
-
const partDocs = await withRetry(
|
|
223
|
-
() => partsCollectionRef.listDocuments(),
|
|
224
|
-
`listDocuments(${partsCollectionRef.path})`
|
|
225
|
-
);
|
|
137
|
+
const partDocs = await withRetry(() => partsCollectionRef.listDocuments(), `listDocuments(${partsCollectionRef.path})`);
|
|
226
138
|
allPartRefs.push(...partDocs);
|
|
227
139
|
}
|
|
228
140
|
}
|
|
229
141
|
|
|
230
|
-
logger.log('INFO', `Found ${allPartRefs.length} history part
|
|
142
|
+
logger.log('INFO', `Found ${allPartRefs.length} history part refs for ${dateString}`);
|
|
231
143
|
return allPartRefs;
|
|
232
144
|
}
|
|
233
145
|
|
|
234
|
-
|
|
235
146
|
module.exports = {
|
|
236
147
|
getPortfolioPartRefs,
|
|
237
148
|
loadDataByRefs,
|
|
238
149
|
loadFullDayMap,
|
|
239
150
|
loadDailyInsights,
|
|
240
|
-
loadDailySocialPostInsights,
|
|
241
|
-
getHistoryPartRefs,
|
|
242
|
-
};
|
|
151
|
+
loadDailySocialPostInsights,
|
|
152
|
+
getHistoryPartRefs,
|
|
153
|
+
};
|
|
@@ -3,56 +3,37 @@
|
|
|
3
3
|
* REFACTORED: Now stateless and receive dependencies where needed.
|
|
4
4
|
* DYNAMIC: Categorization logic is removed, replaced by manifest.
|
|
5
5
|
*/
|
|
6
|
+
/** --- Computation System Sub-Pipes & Utils (Stateless) --- */
|
|
6
7
|
|
|
7
8
|
const { FieldValue, FieldPath } = require('@google-cloud/firestore');
|
|
8
9
|
|
|
9
|
-
/**
|
|
10
|
-
* Normalizes a calculation name to kebab-case.
|
|
11
|
-
* @param {string} name
|
|
12
|
-
* @returns {string}
|
|
13
|
-
*/
|
|
10
|
+
/** Stage 1: Normalize a calculation name to kebab-case */
|
|
14
11
|
function normalizeName(name) {
|
|
15
12
|
return name.replace(/_/g, '-');
|
|
16
13
|
}
|
|
17
14
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* @param {object} dependencies - Contains db, logger, calculationUtils.
|
|
22
|
-
* @param {Array<object>} writes - Array of { ref: DocumentReference, data: object }.
|
|
23
|
-
* @param {string} operationName - Name for logging.
|
|
24
|
-
*/
|
|
25
|
-
async function commitBatchInChunks(config, dependencies, writes, operationName) {
|
|
26
|
-
// --- MODIFIED: Get withRetry from dependencies ---
|
|
27
|
-
const { db, logger, calculationUtils } = dependencies;
|
|
15
|
+
/** Stage 2: Commit a batch of writes in chunks */
|
|
16
|
+
async function commitBatchInChunks(config, deps, writes, operationName) {
|
|
17
|
+
const { db, logger, calculationUtils } = deps;
|
|
28
18
|
const { withRetry } = calculationUtils;
|
|
29
|
-
|
|
30
|
-
|
|
19
|
+
|
|
31
20
|
const batchSizeLimit = config.batchSizeLimit || 450;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
logger.log('WARN', `[${operationName}] No writes to commit.`);
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
21
|
+
if (!writes.length) { logger.log('WARN', `[${operationName}] No writes to commit.`); return; }
|
|
22
|
+
|
|
37
23
|
for (let i = 0; i < writes.length; i += batchSizeLimit) {
|
|
38
|
-
const batch = db.batch(); // Use db
|
|
39
24
|
const chunk = writes.slice(i, i + batchSizeLimit);
|
|
25
|
+
const batch = db.batch();
|
|
40
26
|
chunk.forEach(write => batch.set(write.ref, write.data, { merge: true }));
|
|
41
27
|
|
|
42
28
|
const chunkNum = Math.floor(i / batchSizeLimit) + 1;
|
|
43
29
|
const totalChunks = Math.ceil(writes.length / batchSizeLimit);
|
|
44
|
-
await withRetry(
|
|
45
|
-
|
|
46
|
-
`${operationName} (Chunk ${chunkNum}/${totalChunks})`
|
|
47
|
-
);
|
|
30
|
+
await withRetry(() => batch.commit(), `${operationName} (Chunk ${chunkNum}/${totalChunks})`);
|
|
31
|
+
|
|
48
32
|
logger.log('INFO', `[${operationName}] Committed chunk ${chunkNum}/${totalChunks} (${chunk.length} ops).`);
|
|
49
33
|
}
|
|
50
34
|
}
|
|
51
35
|
|
|
52
|
-
/**
|
|
53
|
-
* Sub-pipe: pipe.computationSystem.computationUtils.getExpectedDateStrings
|
|
54
|
-
* (Stateless)
|
|
55
|
-
*/
|
|
36
|
+
/** Stage 3: Generate an array of expected date strings between two dates */
|
|
56
37
|
function getExpectedDateStrings(startDate, endDate) {
|
|
57
38
|
const dateStrings = [];
|
|
58
39
|
if (startDate <= endDate) {
|
|
@@ -65,86 +46,62 @@ function getExpectedDateStrings(startDate, endDate) {
|
|
|
65
46
|
return dateStrings;
|
|
66
47
|
}
|
|
67
48
|
|
|
68
|
-
/**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
async function getFirstDateFromCollection(config, dependencies, collectionName) {
|
|
72
|
-
// --- MODIFIED: Get withRetry from dependencies ---
|
|
73
|
-
const { db, logger, calculationUtils } = dependencies;
|
|
49
|
+
/** Stage 4: Get the earliest date in a collection */
|
|
50
|
+
async function getFirstDateFromCollection(config, deps, collectionName) {
|
|
51
|
+
const { db, logger, calculationUtils } = deps;
|
|
74
52
|
const { withRetry } = calculationUtils;
|
|
75
|
-
|
|
53
|
+
|
|
76
54
|
let earliestDate = null;
|
|
77
55
|
try {
|
|
78
|
-
const blockDocRefs = await withRetry(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
for (const blockDocRef of blockDocRefs) {
|
|
89
|
-
const snapshotQuery = blockDocRef.collection(config.snapshotsSubcollection)
|
|
90
|
-
.where(FieldPath.documentId(), '>=', '2000-01-01')
|
|
91
|
-
.orderBy(FieldPath.documentId(), 'asc')
|
|
92
|
-
.limit(1);
|
|
93
|
-
|
|
94
|
-
const snapshotSnap = await withRetry(
|
|
95
|
-
() => snapshotQuery.get(),
|
|
96
|
-
`GetEarliestSnapshot(${blockDocRef.path})`
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
if (!snapshotSnap.empty && /^\d{4}-\d{2}-\d{2}$/.test(snapshotSnap.docs[0].id)) {
|
|
100
|
-
const foundDate = new Date(snapshotSnap.docs[0].id + 'T00:00:00Z');
|
|
101
|
-
if (!earliestDate || foundDate < earliestDate) {
|
|
102
|
-
earliestDate = foundDate;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
56
|
+
const blockDocRefs = await withRetry(() => db.collection(collectionName).listDocuments(), `GetBlocks(${collectionName})`);
|
|
57
|
+
if (!blockDocRefs.length) { logger.log('WARN', `No block documents in collection: ${collectionName}`); return null; }
|
|
58
|
+
|
|
59
|
+
for (const blockDocRef of blockDocRefs) {
|
|
60
|
+
const snapshotQuery = blockDocRef.collection(config.snapshotsSubcollection)
|
|
61
|
+
.where(FieldPath.documentId(), '>=', '2000-01-01')
|
|
62
|
+
.orderBy(FieldPath.documentId(), 'asc')
|
|
63
|
+
.limit(1);
|
|
106
64
|
|
|
65
|
+
const snapshotSnap = await withRetry(() => snapshotQuery.get(), `GetEarliestSnapshot(${blockDocRef.path})`);
|
|
66
|
+
if (!snapshotSnap.empty && /^\d{4}-\d{2}-\d{2}$/.test(snapshotSnap.docs[0].id)) {
|
|
67
|
+
const foundDate = new Date(snapshotSnap.docs[0].id + 'T00:00:00Z');
|
|
68
|
+
if (!earliestDate || foundDate < earliestDate) earliestDate = foundDate;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
107
71
|
} catch (e) {
|
|
108
72
|
logger.log('ERROR', `GetFirstDate failed for ${collectionName}`, { errorMessage: e.message });
|
|
109
73
|
}
|
|
74
|
+
|
|
110
75
|
return earliestDate;
|
|
111
76
|
}
|
|
112
77
|
|
|
113
|
-
/**
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (earliestDate) {
|
|
135
|
-
logger.log('INFO', `Found earliest source data date: ${earliestDate.toISOString().slice(0, 10)}`);
|
|
136
|
-
return earliestDate;
|
|
137
|
-
} else {
|
|
138
|
-
const fallbackDate = new Date(config.earliestComputationDate + 'T00:00:00Z' || '2023-01-01T00:00:00Z');
|
|
139
|
-
logger.log('WARN', `No source data found. Defaulting first date to: ${fallbackDate.toISOString().slice(0, 10)}`);
|
|
140
|
-
return fallbackDate;
|
|
141
|
-
}
|
|
78
|
+
/** Stage 5: Determine the earliest date from source data across both user types */
|
|
79
|
+
async function getFirstDateFromSourceData(config, deps) {
|
|
80
|
+
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
|
+
}
|
|
142
98
|
}
|
|
143
99
|
|
|
144
100
|
module.exports = {
|
|
145
|
-
FieldValue,
|
|
101
|
+
FieldValue,
|
|
102
|
+
FieldPath,
|
|
146
103
|
normalizeName,
|
|
147
104
|
commitBatchInChunks,
|
|
148
|
-
getExpectedDateStrings,
|
|
105
|
+
getExpectedDateStrings,
|
|
149
106
|
getFirstDateFromSourceData,
|
|
150
|
-
};
|
|
107
|
+
};
|