@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 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
- verified: { type: Boolean, default: false },
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', 'verified'], required: true },
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', 'verified'], required: true },
143
+ action: { type: String, enum: ['updated', 'deleted'], required: true },
145
144
  }]
146
145
  }],
147
146
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dhyasama/totem-models",
3
- "version": "11.119.0",
3
+ "version": "11.121.0",
4
4
  "author": "Jason Reynolds",
5
5
  "license": "UNLICENSED",
6
6
  "description": "Models for Totem platform",
@@ -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 values in snapshots:
9
- * - 'Overdue' 'Delayed'
10
- * - 'In Review' 'Unapproved'
11
- * 3. Add verified: true to all existing documents[], metrics[], questions[]
12
- * 4. Convert metrics from actual/budget/forecast fields to scenario/value structure
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 === 'Overdue') {
150
- updates[`snapshots.${index}.status`] = 'Delayed';
151
- changes.push(` - Snapshot ${index}: Changed status 'Overdue' → 'Delayed'`);
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
- // 2c. Add verified: true to documents
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 (!('verified' in doc)) {
161
- updates[`snapshots.${index}.documents.${docIndex}.verified`] = true;
162
- changes.push(` - Snapshot ${index}: Added verified to document ${docIndex}`);
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 verified: true to questions
195
+ // 2d. Add history to questions
168
196
  if (snapshot.questions && snapshot.questions.length > 0) {
169
197
  snapshot.questions.forEach((question, qIndex) => {
170
- if (!('verified' in question)) {
171
- updates[`snapshots.${index}.questions.${qIndex}.verified`] = true;
172
- changes.push(` - Snapshot ${index}: Added verified to question ${qIndex}`);
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 and add verified
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 (actual/budget/forecast) to new structure (scenario/value)
247
+ // Convert old structure { name, actual, budget, forecast, breakdown } to new structure
189
248
  oldMetrics.forEach((metric) => {
190
- // Create separate metric entries for each scenario
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
- newMetrics.push({
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
- newMetrics.push({
266
+ const newMetric = {
203
267
  name: metric.name,
204
268
  scenario: 'budget',
205
269
  value: metric.budget,
206
- breakdown: metric.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
- newMetrics.push({
279
+ const newMetric = {
213
280
  name: metric.name,
214
281
  scenario: 'forecast',
215
282
  value: metric.forecast,
216
- breakdown: metric.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
- });