@dhyasama/totem-models 11.119.0 → 11.121.0
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/lib/Financials.js +3 -4
- package/package.json +1 -1
- package/scripts/migrate-financials-schema.js +125 -36
- package/migrations/social_handles.js +0 -119
package/lib/Financials.js
CHANGED
|
@@ -97,12 +97,12 @@ module.exports = function(mongoose, config) {
|
|
|
97
97
|
_id: false,
|
|
98
98
|
name: { type: String, trim: true },
|
|
99
99
|
document: { type: Schema.ObjectId, ref: 'Document' },
|
|
100
|
-
|
|
100
|
+
scenario: { type: String, enum: ['actual', 'budget', 'forecast'] },
|
|
101
101
|
history: [{
|
|
102
102
|
_id: false,
|
|
103
103
|
timestamp: { type: Date, required: true, default: Date.now },
|
|
104
104
|
user: { type: String, trim: true, required: true },
|
|
105
|
-
action: { type: String, enum: ['updated', 'deleted'
|
|
105
|
+
action: { type: String, enum: ['updated', 'deleted'], required: true },
|
|
106
106
|
}]
|
|
107
107
|
}],
|
|
108
108
|
|
|
@@ -136,12 +136,11 @@ module.exports = function(mongoose, config) {
|
|
|
136
136
|
_id: false,
|
|
137
137
|
prompt: { type: String, trim: true },
|
|
138
138
|
response: { type: String, trim: true },
|
|
139
|
-
verified: { type: Boolean, default: false },
|
|
140
139
|
history: [{
|
|
141
140
|
_id: false,
|
|
142
141
|
timestamp: { type: Date, required: true, default: Date.now },
|
|
143
142
|
user: { type: String, trim: true, required: true },
|
|
144
|
-
action: { type: String, enum: ['updated', 'deleted'
|
|
143
|
+
action: { type: String, enum: ['updated', 'deleted'], required: true },
|
|
145
144
|
}]
|
|
146
145
|
}],
|
|
147
146
|
|
package/package.json
CHANGED
|
@@ -5,11 +5,15 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Changes being migrated:
|
|
7
7
|
* 1. Rename field: review → approval (in recurring and snapshots)
|
|
8
|
-
* 2. Update status enum
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* 2. Update status enum value: 'In Review' → 'Pending'
|
|
9
|
+
* 3. Populate history[] for documents[], questions[], and metrics[]
|
|
10
|
+
* History is populated with submission and approval data from the snapshot
|
|
11
|
+
* 4. Add verified: true for metrics[] only
|
|
12
|
+
* 5. Convert metrics from old structure { name, actual, budget, forecast, breakdown }
|
|
13
|
+
* to new structure { name, scenario, value, breakdown, source, verified, history }
|
|
14
|
+
* 6. Link metrics to their source financial statement documents (Income Statement,
|
|
15
|
+
* Balance Sheet, Cash Flow Statement) when available
|
|
16
|
+
* 7. Remove projections array from root level
|
|
13
17
|
*
|
|
14
18
|
* Usage:
|
|
15
19
|
* node scripts/migrate-financials-schema.js <customerId> [--config <configPath>] [--dry-run]
|
|
@@ -146,94 +150,179 @@ async function migrateFinancials() {
|
|
|
146
150
|
}
|
|
147
151
|
|
|
148
152
|
// 2b. Update status enum values
|
|
149
|
-
if (snapshot.status === '
|
|
150
|
-
updates[`snapshots.${index}.status`] = '
|
|
151
|
-
changes.push(` - Snapshot ${index}: Changed status '
|
|
152
|
-
} else if (snapshot.status === 'In Review') {
|
|
153
|
-
updates[`snapshots.${index}.status`] = 'Unapproved';
|
|
154
|
-
changes.push(` - Snapshot ${index}: Changed status 'In Review' → 'Unapproved'`);
|
|
153
|
+
if (snapshot.status === 'In Review') {
|
|
154
|
+
updates[`snapshots.${index}.status`] = 'Pending';
|
|
155
|
+
changes.push(` - Snapshot ${index}: Changed status 'In Review' → 'Pending'`);
|
|
155
156
|
}
|
|
156
157
|
|
|
157
|
-
//
|
|
158
|
+
// Build history array from snapshot submission/approval data
|
|
159
|
+
const buildHistory = () => {
|
|
160
|
+
const history = [];
|
|
161
|
+
|
|
162
|
+
// Add submission history entry
|
|
163
|
+
if (snapshot.submittedOn && snapshot.submittedBy) {
|
|
164
|
+
history.push({
|
|
165
|
+
timestamp: snapshot.submittedOn,
|
|
166
|
+
user: snapshot.submittedBy,
|
|
167
|
+
action: 'updated'
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Add approval history entry
|
|
172
|
+
if (snapshot.approvedOn && snapshot.approvedBy) {
|
|
173
|
+
history.push({
|
|
174
|
+
timestamp: snapshot.approvedOn,
|
|
175
|
+
user: snapshot.approvedBy,
|
|
176
|
+
action: 'verified'
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return history;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const historyArray = buildHistory();
|
|
184
|
+
|
|
185
|
+
// 2c. Add history to documents
|
|
158
186
|
if (snapshot.documents && snapshot.documents.length > 0) {
|
|
159
187
|
snapshot.documents.forEach((doc, docIndex) => {
|
|
160
|
-
if (!('
|
|
161
|
-
updates[`snapshots.${index}.documents.${docIndex}.
|
|
162
|
-
changes.push(` - Snapshot ${index}: Added
|
|
188
|
+
if (!('history' in doc)) {
|
|
189
|
+
updates[`snapshots.${index}.documents.${docIndex}.history`] = historyArray;
|
|
190
|
+
changes.push(` - Snapshot ${index}: Added history (${historyArray.length} entries) to document ${docIndex}`);
|
|
163
191
|
}
|
|
164
192
|
});
|
|
165
193
|
}
|
|
166
194
|
|
|
167
|
-
// 2d. Add
|
|
195
|
+
// 2d. Add history to questions
|
|
168
196
|
if (snapshot.questions && snapshot.questions.length > 0) {
|
|
169
197
|
snapshot.questions.forEach((question, qIndex) => {
|
|
170
|
-
if (!('
|
|
171
|
-
updates[`snapshots.${index}.questions.${qIndex}.
|
|
172
|
-
changes.push(` - Snapshot ${index}: Added
|
|
198
|
+
if (!('history' in question)) {
|
|
199
|
+
updates[`snapshots.${index}.questions.${qIndex}.history`] = historyArray;
|
|
200
|
+
changes.push(` - Snapshot ${index}: Added history (${historyArray.length} entries) to question ${qIndex}`);
|
|
173
201
|
}
|
|
174
202
|
});
|
|
175
203
|
}
|
|
176
204
|
|
|
177
|
-
// 2e. Migrate metrics structure
|
|
205
|
+
// 2e. Migrate metrics structure
|
|
178
206
|
if (snapshot.metrics && snapshot.metrics.length > 0) {
|
|
179
207
|
const oldMetrics = [...snapshot.metrics];
|
|
180
208
|
const newMetrics = [];
|
|
181
209
|
|
|
210
|
+
// Document to metric mappings
|
|
211
|
+
const incomeStatementFields = ['Revenue', 'COGS', 'Gross Profit', 'Operating Expenses', 'EBITDA', 'Operating Income', 'Net Income'];
|
|
212
|
+
const balanceSheetFields = ['Assets', 'Liabilities', 'Equity'];
|
|
213
|
+
const cashFlowStatementFields = ['Operating Activities', 'Investing Activities', 'Financing Activities'];
|
|
214
|
+
|
|
215
|
+
// Find financial statement documents in this snapshot
|
|
216
|
+
const findDocument = (namePattern) => {
|
|
217
|
+
if (!snapshot.documents) return null;
|
|
218
|
+
return snapshot.documents.find(doc =>
|
|
219
|
+
doc.name && doc.name.toLowerCase().includes(namePattern.toLowerCase())
|
|
220
|
+
);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const incomeStatementDoc = findDocument('Income Statement');
|
|
224
|
+
const balanceSheetDoc = findDocument('Balance Sheet');
|
|
225
|
+
const cashFlowStatementDoc = findDocument('Cash Flow Statement');
|
|
226
|
+
|
|
227
|
+
// Helper to get source object for a metric name
|
|
228
|
+
const getSourceForMetric = (metricName) => {
|
|
229
|
+
if (incomeStatementFields.includes(metricName) && incomeStatementDoc && incomeStatementDoc.document) {
|
|
230
|
+
return { model: 'Document', ref: incomeStatementDoc.document };
|
|
231
|
+
}
|
|
232
|
+
if (balanceSheetFields.includes(metricName) && balanceSheetDoc && balanceSheetDoc.document) {
|
|
233
|
+
return { model: 'Document', ref: balanceSheetDoc.document };
|
|
234
|
+
}
|
|
235
|
+
if (cashFlowStatementFields.includes(metricName) && cashFlowStatementDoc && cashFlowStatementDoc.document) {
|
|
236
|
+
return { model: 'Document', ref: cashFlowStatementDoc.document };
|
|
237
|
+
}
|
|
238
|
+
return undefined;
|
|
239
|
+
};
|
|
240
|
+
|
|
182
241
|
// Check if metrics need conversion from old structure
|
|
183
242
|
const needsConversion = oldMetrics.some(m =>
|
|
184
243
|
('actual' in m || 'budget' in m || 'forecast' in m) && !('scenario' in m)
|
|
185
244
|
);
|
|
186
245
|
|
|
187
246
|
if (needsConversion) {
|
|
188
|
-
// Convert old structure
|
|
247
|
+
// Convert old structure { name, actual, budget, forecast, breakdown } to new structure
|
|
189
248
|
oldMetrics.forEach((metric) => {
|
|
190
|
-
|
|
249
|
+
const source = getSourceForMetric(metric.name);
|
|
250
|
+
|
|
251
|
+
// Create separate metric entries for each scenario that has a value
|
|
191
252
|
if ('actual' in metric && metric.actual !== undefined && metric.actual !== null) {
|
|
192
|
-
|
|
253
|
+
const newMetric = {
|
|
193
254
|
name: metric.name,
|
|
194
255
|
scenario: 'actual',
|
|
195
256
|
value: metric.actual,
|
|
196
|
-
breakdown: metric.breakdown,
|
|
197
|
-
verified: true
|
|
198
|
-
|
|
257
|
+
breakdown: metric.breakdown || {},
|
|
258
|
+
verified: true,
|
|
259
|
+
history: historyArray
|
|
260
|
+
};
|
|
261
|
+
if (source) newMetric.source = source;
|
|
262
|
+
newMetrics.push(newMetric);
|
|
199
263
|
}
|
|
200
264
|
|
|
201
265
|
if ('budget' in metric && metric.budget !== undefined && metric.budget !== null) {
|
|
202
|
-
|
|
266
|
+
const newMetric = {
|
|
203
267
|
name: metric.name,
|
|
204
268
|
scenario: 'budget',
|
|
205
269
|
value: metric.budget,
|
|
206
|
-
breakdown:
|
|
207
|
-
verified: true
|
|
208
|
-
|
|
270
|
+
breakdown: {},
|
|
271
|
+
verified: true,
|
|
272
|
+
history: historyArray
|
|
273
|
+
};
|
|
274
|
+
if (source) newMetric.source = source;
|
|
275
|
+
newMetrics.push(newMetric);
|
|
209
276
|
}
|
|
210
277
|
|
|
211
278
|
if ('forecast' in metric && metric.forecast !== undefined && metric.forecast !== null) {
|
|
212
|
-
|
|
279
|
+
const newMetric = {
|
|
213
280
|
name: metric.name,
|
|
214
281
|
scenario: 'forecast',
|
|
215
282
|
value: metric.forecast,
|
|
216
|
-
breakdown:
|
|
217
|
-
verified: true
|
|
218
|
-
|
|
283
|
+
breakdown: {},
|
|
284
|
+
verified: true,
|
|
285
|
+
history: historyArray
|
|
286
|
+
};
|
|
287
|
+
if (source) newMetric.source = source;
|
|
288
|
+
newMetrics.push(newMetric);
|
|
219
289
|
}
|
|
220
290
|
});
|
|
221
291
|
|
|
292
|
+
const metricsWithSource = newMetrics.filter(m => m.source).length;
|
|
222
293
|
updates[`snapshots.${index}.metrics`] = newMetrics;
|
|
223
|
-
changes.push(` - Snapshot ${index}: Converted ${oldMetrics.length} metric(s) to new structure (${newMetrics.length} entries)`);
|
|
294
|
+
changes.push(` - Snapshot ${index}: Converted ${oldMetrics.length} metric(s) to new structure (${newMetrics.length} entries, ${metricsWithSource} with source)`);
|
|
224
295
|
} else {
|
|
225
|
-
// Already in new structure, just add verified if missing
|
|
296
|
+
// Already in new structure, just add verified, history, and source if missing
|
|
226
297
|
oldMetrics.forEach((metric, mIndex) => {
|
|
227
298
|
if (!('verified' in metric)) {
|
|
228
299
|
updates[`snapshots.${index}.metrics.${mIndex}.verified`] = true;
|
|
229
300
|
changes.push(` - Snapshot ${index}: Added verified to metric ${mIndex}`);
|
|
230
301
|
}
|
|
302
|
+
if (!('history' in metric)) {
|
|
303
|
+
updates[`snapshots.${index}.metrics.${mIndex}.history`] = historyArray;
|
|
304
|
+
changes.push(` - Snapshot ${index}: Added history (${historyArray.length} entries) to metric ${mIndex}`);
|
|
305
|
+
}
|
|
306
|
+
if (!('source' in metric)) {
|
|
307
|
+
const source = getSourceForMetric(metric.name);
|
|
308
|
+
if (source) {
|
|
309
|
+
updates[`snapshots.${index}.metrics.${mIndex}.source`] = source;
|
|
310
|
+
changes.push(` - Snapshot ${index}: Added source to metric ${mIndex} (${metric.name})`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
231
313
|
});
|
|
232
314
|
}
|
|
233
315
|
}
|
|
234
316
|
});
|
|
235
317
|
}
|
|
236
318
|
|
|
319
|
+
// 3. Remove projections array from root level
|
|
320
|
+
if ('projections' in financial) {
|
|
321
|
+
updates.$unset = updates.$unset || {};
|
|
322
|
+
updates.$unset['projections'] = '';
|
|
323
|
+
changes.push(' - Removed projections array');
|
|
324
|
+
}
|
|
325
|
+
|
|
237
326
|
// Apply updates
|
|
238
327
|
if (Object.keys(updates).length > 0 && !DRY_RUN) {
|
|
239
328
|
const updateDoc = {};
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const mongoose = require('mongoose');
|
|
4
|
-
const models = require('../index')(mongoose, { suppressDebugLog: true });
|
|
5
|
-
const Organization = mongoose.model('Organization');
|
|
6
|
-
const db_uri = "mongodb://jason:T576G150HPXLA5q4BJ2o2zj747B5030x@production-shard-00-00-gv0f3.mongodb.net:27017,production-shard-00-01-gv0f3.mongodb.net:27017,production-shard-00-02-gv0f3.mongodb.net:27017/production?replicaSet=Production-shard-0&ssl=true&authSource=admin";
|
|
7
|
-
const _ = require('underscore');
|
|
8
|
-
const async = require('async');
|
|
9
|
-
|
|
10
|
-
let dirtyOrgs = [];
|
|
11
|
-
let savedOrgCount = 0;
|
|
12
|
-
|
|
13
|
-
const getHandle = function getHandle(url) {
|
|
14
|
-
|
|
15
|
-
let handle = "";
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
|
|
19
|
-
const segments = url.split("/");
|
|
20
|
-
|
|
21
|
-
// normal case, e.g., http://facebook.com/hello
|
|
22
|
-
if (segments[segments.length - 1]) handle = segments[segments.length - 1];
|
|
23
|
-
|
|
24
|
-
// trailing slash, e.g., http://facebook.com/hello/
|
|
25
|
-
else if (segments[segments.length - 2]) handle = segments[segments.length - 2];
|
|
26
|
-
|
|
27
|
-
else handle = "";
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
catch (err) {
|
|
31
|
-
console.log('Error for url', url);
|
|
32
|
-
console.error(err);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
//console.log(handle, url);
|
|
36
|
-
|
|
37
|
-
return handle;
|
|
38
|
-
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const saveOrg = function(org, cb) {
|
|
42
|
-
|
|
43
|
-
Organization.upsert(org, 'social-handle-transformation', function(err) {
|
|
44
|
-
if (err) console.error(err);
|
|
45
|
-
else {
|
|
46
|
-
savedOrgCount += 1;
|
|
47
|
-
//console.log(org.name);
|
|
48
|
-
}
|
|
49
|
-
return cb();
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
mongoose.connect(db_uri, function(err) {
|
|
55
|
-
|
|
56
|
-
if (err) {
|
|
57
|
-
console.error(err);
|
|
58
|
-
process.exit();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const page = 9;
|
|
62
|
-
const limit = 10000;
|
|
63
|
-
const skip = (page - 1) * limit;
|
|
64
|
-
|
|
65
|
-
console.log('Getting page', page);
|
|
66
|
-
|
|
67
|
-
Organization
|
|
68
|
-
.find({
|
|
69
|
-
$and: [
|
|
70
|
-
{ social: { $exists: true } },
|
|
71
|
-
{
|
|
72
|
-
$or: [
|
|
73
|
-
{ "social.facebook": { $exists: true } },
|
|
74
|
-
{ "social.twitter": { $exists: true } },
|
|
75
|
-
{ "social.linkedin": { $exists: true } }
|
|
76
|
-
]
|
|
77
|
-
}
|
|
78
|
-
]
|
|
79
|
-
})
|
|
80
|
-
.skip(skip)
|
|
81
|
-
.limit(limit)
|
|
82
|
-
.sort({"name": "asc"})
|
|
83
|
-
.exec(function(err, orgs) {
|
|
84
|
-
|
|
85
|
-
if (err) {
|
|
86
|
-
console.error(err);
|
|
87
|
-
process.exit();
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
console.log('Found', orgs.length, 'orgs');
|
|
91
|
-
|
|
92
|
-
orgs.forEach(function(org) {
|
|
93
|
-
|
|
94
|
-
let dirty = false;
|
|
95
|
-
|
|
96
|
-
if (org.social.facebook || org.social.twitter || org.social.linkedin) {
|
|
97
|
-
dirty = true;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (dirty) dirtyOrgs.push(org);
|
|
101
|
-
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
console.log(dirtyOrgs.length, 'orgs need to be saved');
|
|
105
|
-
|
|
106
|
-
async.each(dirtyOrgs, saveOrg, function(err) {
|
|
107
|
-
|
|
108
|
-
if (err) console.error(err);
|
|
109
|
-
|
|
110
|
-
console.log('Saved', savedOrgCount, 'orgs');
|
|
111
|
-
console.log('Done page', page);
|
|
112
|
-
|
|
113
|
-
process.exit();
|
|
114
|
-
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
});
|