@dhyasama/totem-models 11.113.0 → 11.115.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
@@ -61,7 +61,7 @@ module.exports = function(mongoose, config) {
61
61
  month: { type: Number, default: 12 },
62
62
  day: { type: Number, default: 31 },
63
63
  },
64
- review: { type: Boolean, default: false },
64
+ approval: { type: Boolean, default: false },
65
65
 
66
66
  // For reminding customer about upcoming collections
67
67
  // Don't store recipients, just query admins at time of sending or check postmark post-facto
@@ -78,9 +78,9 @@ module.exports = function(mongoose, config) {
78
78
 
79
79
  currency: { type: String, default: 'USD' },
80
80
 
81
- review: { type: Boolean, default: false },
81
+ approval:{ type: Boolean, default: false },
82
82
 
83
- status: { type: String, enum: ['Created', 'Scheduled', 'Delivered', 'Opened', 'Overdue', 'In Review', 'Completed', 'Archived'] },
83
+ status: { type: String, enum: ['Created', 'Scheduled', 'Delivered', 'Opened', 'Delayed', 'Unapproved', 'Completed', 'Archived'] },
84
84
 
85
85
  year: { type: Number, default: 0 },
86
86
  period: { type: String, enum: ['FY', 'H1', 'H2', 'Q1', 'Q2', 'Q3', 'Q4', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] },
@@ -97,21 +97,49 @@ 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 },
101
+ history: [{
102
+ timestamp: { type: Date, required: true, default: Date.now },
103
+ user: { type: String, trim: true, required: true },
104
+ description: { type: String, trim: true, required: true }
105
+ }]
100
106
  }],
101
107
 
102
108
  metrics: [{
103
109
  _id: false,
104
110
  name: { type: String, trim: true },
105
- actual: { type: Number },
106
- budget: { type: Number },
107
- forecast: { type: Number },
108
- breakdown: { type: Schema.Types.Mixed }
111
+ scenario: { type: String, enum: ['actual', 'budget', 'forecast'] },
112
+ value: { type: Number },
113
+ breakdown: { type: Schema.Types.Mixed },
114
+ source: {
115
+ _id: false,
116
+ model: {
117
+ type: String,
118
+ enum: ['Document', 'Meeting', 'Message', 'Note']
119
+ },
120
+ ref: {
121
+ type: Schema.Types.ObjectId,
122
+ refPath: 'metrics.source.model',
123
+ },
124
+ },
125
+ verified: { type: Boolean, default: false },
126
+ history: [{
127
+ timestamp: { type: Date, required: true, default: Date.now },
128
+ user: { type: String, trim: true, required: true },
129
+ description: { type: String, trim: true, required: true }
130
+ }]
109
131
  }],
110
132
 
111
133
  questions: [{
112
134
  _id: false,
113
135
  prompt: { type: String, trim: true },
114
136
  response: { type: String, trim: true },
137
+ verified: { type: Boolean, default: false },
138
+ history: [{
139
+ timestamp: { type: Date, required: true, default: Date.now },
140
+ user: { type: String, trim: true, required: true },
141
+ description: { type: String, trim: true, required: true }
142
+ }]
115
143
  }],
116
144
 
117
145
  dueOn: { type: Date, index: false },
@@ -927,14 +955,14 @@ module.exports = function(mongoose, config) {
927
955
 
928
956
  };
929
957
 
930
- Financials.statics.getPassivelyOverdue = function getPassivelyOverdue(cb) {
958
+ Financials.statics.getOverdue = function getOverdue(cb) {
931
959
 
932
960
  const self = this;
933
961
 
934
962
  const query = self.find({
935
963
  'snapshots': {
936
964
  $elemMatch: {
937
- 'status': { $nin: ['Completed', 'Archived', 'Overdue'] },
965
+ 'status': { $nin: ['Completed', 'Archived', 'Delayed'] },
938
966
  'dueOn': { $lte: new Date() }
939
967
  }
940
968
  }
@@ -944,14 +972,6 @@ module.exports = function(mongoose, config) {
944
972
 
945
973
  };
946
974
 
947
- Financials.statics.markAsOverdue = function markAsOverdue(financials, cb) {
948
-
949
- if (!financials) { return cb(new Error('financials is required'), null); }
950
-
951
- financials.save(cb);
952
-
953
- };
954
-
955
975
  Financials.statics.updateProperty = function updateProperty(id, key, value, cb) {
956
976
 
957
977
  const self = this;
@@ -1000,12 +1020,12 @@ module.exports = function(mongoose, config) {
1000
1020
 
1001
1021
  if (!snapshot) return;
1002
1022
 
1003
- var needsReview = snapshot.review;
1023
+ var needsApproval = snapshot.approval;
1004
1024
  var isSubmitted = snapshot.submittedOn;
1005
1025
  var isApproved = snapshot.approvedOn;
1006
1026
  var isRejected = snapshot.rejectedOn && snapshot.rejection;
1007
1027
  var isArchived = snapshot.archivedOn;
1008
- var isOverdue = snapshot.dueOn ? moment(new Date()).isAfter(moment(snapshot.dueOn)) : false;
1028
+ var isDelayed = snapshot.dueOn ? moment(new Date()).isAfter(moment(snapshot.dueOn)) : false;
1009
1029
 
1010
1030
  var isOpened = false;
1011
1031
  var isDelivered = false;
@@ -1017,12 +1037,12 @@ module.exports = function(mongoose, config) {
1017
1037
  }
1018
1038
 
1019
1039
  if(!snapshot.uuid) snapshot.status = 'Completed'; // no uuid means the snapshot was created from the google sheet and should always be completed
1020
- else if(needsReview && isApproved) snapshot.status = 'Completed';
1021
- else if(!needsReview && isSubmitted) snapshot.status = 'Completed';
1040
+ else if(needsApproval && isApproved) snapshot.status = 'Completed';
1041
+ else if(!needsApproval && isSubmitted) snapshot.status = 'Completed';
1022
1042
  else if(isArchived) snapshot.status = 'Archived';
1023
- else if(needsReview && isSubmitted && !isRejected) snapshot.status = 'In Review';
1024
- else if(needsReview && isSubmitted && isRejected) snapshot.status = 'Opened';
1025
- else if(isOverdue) snapshot.status = 'Overdue';
1043
+ else if(needsApproval && isSubmitted && !isRejected) snapshot.status = 'Unapproved';
1044
+ else if(needsApproval && isSubmitted && isRejected) snapshot.status = 'Opened';
1045
+ else if(isDelayed) snapshot.status = 'Delayed';
1026
1046
  else if(isDelivered && isOpened) snapshot.status = 'Opened';
1027
1047
  else if(isDelivered) snapshot.status = 'Delivered';
1028
1048
  else if(isScheduled) snapshot.status = 'Scheduled'; // when a snapshot is scheduled, but not yet sent (checks every 5 minutes)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dhyasama/totem-models",
3
- "version": "11.113.0",
3
+ "version": "11.115.0",
4
4
  "author": "Jason Reynolds",
5
5
  "license": "UNLICENSED",
6
6
  "description": "Models for Totem platform",
@@ -0,0 +1,279 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Migration script for Financials model schema changes
5
+ *
6
+ * Changes being migrated:
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
13
+ *
14
+ * Usage:
15
+ * node scripts/migrate-financials-schema.js <customerId> [--config <configPath>] [--dry-run]
16
+ * node scripts/migrate-financials-schema.js <customerId> [--db-uri <mongoUri>] [--dry-run]
17
+ *
18
+ * Examples:
19
+ * # Use totem-web production config
20
+ * NODE_ENV=production node scripts/migrate-financials-schema.js 5cdc3340e67922775af918ca --config ../totem-web/config/config.js --dry-run
21
+ *
22
+ * # Use DB_URI environment variable
23
+ * DB_URI="mongodb://..." node scripts/migrate-financials-schema.js 5cdc3340e67922775af918ca --dry-run
24
+ */
25
+
26
+ const mongoose = require('mongoose');
27
+ const path = require('path');
28
+
29
+ // Parse command line arguments
30
+ const args = process.argv.slice(2);
31
+ const CUSTOMER_ID = args[0];
32
+ const DRY_RUN = args.includes('--dry-run');
33
+
34
+ // Get config path or db uri
35
+ let configPath = null;
36
+ let dbUri = null;
37
+
38
+ const configIndex = args.indexOf('--config');
39
+ if (configIndex !== -1 && args[configIndex + 1]) {
40
+ configPath = path.resolve(args[configIndex + 1]);
41
+ }
42
+
43
+ const dbUriIndex = args.indexOf('--db-uri');
44
+ if (dbUriIndex !== -1 && args[dbUriIndex + 1]) {
45
+ dbUri = args[dbUriIndex + 1];
46
+ }
47
+
48
+ // Check for DB_URI environment variable
49
+ if (!dbUri && process.env.DB_URI) {
50
+ dbUri = process.env.DB_URI;
51
+ }
52
+
53
+ // Validate arguments
54
+ if (!CUSTOMER_ID || CUSTOMER_ID.startsWith('--')) {
55
+ console.error('Error: Customer ID is required');
56
+ console.error('Usage: node scripts/migrate-financials-schema.js <customerId> [--config <configPath>] [--db-uri <mongoUri>] [--dry-run]');
57
+ process.exit(1);
58
+ }
59
+
60
+ if (!configPath && !dbUri) {
61
+ console.error('Error: Either --config, --db-uri, or DB_URI environment variable is required');
62
+ console.error('Usage: node scripts/migrate-financials-schema.js <customerId> [--config <configPath>] [--db-uri <mongoUri>] [--dry-run]');
63
+ process.exit(1);
64
+ }
65
+
66
+ // Load config if provided
67
+ if (configPath) {
68
+ console.log(`Loading config from: ${configPath}`);
69
+ const config = require(configPath);
70
+ dbUri = config.db.uri;
71
+ }
72
+
73
+ console.log('Migration Configuration:');
74
+ console.log(' Customer ID:', CUSTOMER_ID);
75
+ console.log(' Database URI:', dbUri.replace(/\/\/[^:]+:[^@]+@/, '//*****:*****@')); // Mask credentials
76
+ console.log(' Dry Run:', DRY_RUN);
77
+ console.log('');
78
+
79
+ // Connect to MongoDB
80
+ const mongooseOptions = {
81
+ useNewUrlParser: true,
82
+ useUnifiedTopology: true
83
+ };
84
+
85
+ mongoose.connect(dbUri, mongooseOptions);
86
+
87
+ const db = mongoose.connection;
88
+
89
+ db.on('error', (err) => {
90
+ console.error('MongoDB connection error:', err);
91
+ process.exit(1);
92
+ });
93
+
94
+ db.once('open', async () => {
95
+ console.log('Connected to MongoDB');
96
+
97
+ try {
98
+ await migrateFinancials();
99
+ console.log('\nMigration completed successfully!');
100
+ process.exit(0);
101
+ } catch (err) {
102
+ console.error('\nMigration failed:', err);
103
+ process.exit(1);
104
+ }
105
+ });
106
+
107
+ async function migrateFinancials() {
108
+ const Financials = db.collection('financials');
109
+
110
+ // Find all financials for this customer
111
+ const query = { customer: mongoose.Types.ObjectId(CUSTOMER_ID) };
112
+ const financials = await Financials.find(query).toArray();
113
+
114
+ console.log(`Found ${financials.length} financial document(s) for customer ${CUSTOMER_ID}\n`);
115
+
116
+ if (financials.length === 0) {
117
+ console.log('No documents to migrate.');
118
+ return;
119
+ }
120
+
121
+ let migratedCount = 0;
122
+ let unchangedCount = 0;
123
+
124
+ for (const financial of financials) {
125
+ const updates = {};
126
+ const changes = [];
127
+
128
+ // 1. Migrate recurring.review → recurring.approval
129
+ if (financial.recurring && 'review' in financial.recurring) {
130
+ updates['recurring.approval'] = financial.recurring.review;
131
+ updates.$unset = updates.$unset || {};
132
+ updates.$unset['recurring.review'] = '';
133
+ changes.push(' - Renamed recurring.review → recurring.approval');
134
+ }
135
+
136
+ // 2. Migrate snapshots
137
+ if (financial.snapshots && financial.snapshots.length > 0) {
138
+ financial.snapshots.forEach((snapshot, index) => {
139
+
140
+ // 2a. Rename snapshot.review → snapshot.approval
141
+ if ('review' in snapshot) {
142
+ updates[`snapshots.${index}.approval`] = snapshot.review;
143
+ updates.$unset = updates.$unset || {};
144
+ updates.$unset[`snapshots.${index}.review`] = '';
145
+ changes.push(` - Snapshot ${index}: Renamed review → approval`);
146
+ }
147
+
148
+ // 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'`);
155
+ }
156
+
157
+ // 2c. Add verified: true to documents
158
+ if (snapshot.documents && snapshot.documents.length > 0) {
159
+ 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}`);
163
+ }
164
+ });
165
+ }
166
+
167
+ // 2d. Add verified: true to questions
168
+ if (snapshot.questions && snapshot.questions.length > 0) {
169
+ 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}`);
173
+ }
174
+ });
175
+ }
176
+
177
+ // 2e. Migrate metrics structure and add verified
178
+ if (snapshot.metrics && snapshot.metrics.length > 0) {
179
+ const oldMetrics = [...snapshot.metrics];
180
+ const newMetrics = [];
181
+
182
+ // Check if metrics need conversion from old structure
183
+ const needsConversion = oldMetrics.some(m =>
184
+ ('actual' in m || 'budget' in m || 'forecast' in m) && !('scenario' in m)
185
+ );
186
+
187
+ if (needsConversion) {
188
+ // Convert old structure (actual/budget/forecast) to new structure (scenario/value)
189
+ oldMetrics.forEach((metric) => {
190
+ // Create separate metric entries for each scenario
191
+ if ('actual' in metric && metric.actual !== undefined && metric.actual !== null) {
192
+ newMetrics.push({
193
+ name: metric.name,
194
+ scenario: 'actual',
195
+ value: metric.actual,
196
+ breakdown: metric.breakdown,
197
+ verified: true
198
+ });
199
+ }
200
+
201
+ if ('budget' in metric && metric.budget !== undefined && metric.budget !== null) {
202
+ newMetrics.push({
203
+ name: metric.name,
204
+ scenario: 'budget',
205
+ value: metric.budget,
206
+ breakdown: metric.breakdown,
207
+ verified: true
208
+ });
209
+ }
210
+
211
+ if ('forecast' in metric && metric.forecast !== undefined && metric.forecast !== null) {
212
+ newMetrics.push({
213
+ name: metric.name,
214
+ scenario: 'forecast',
215
+ value: metric.forecast,
216
+ breakdown: metric.breakdown,
217
+ verified: true
218
+ });
219
+ }
220
+ });
221
+
222
+ updates[`snapshots.${index}.metrics`] = newMetrics;
223
+ changes.push(` - Snapshot ${index}: Converted ${oldMetrics.length} metric(s) to new structure (${newMetrics.length} entries)`);
224
+ } else {
225
+ // Already in new structure, just add verified if missing
226
+ oldMetrics.forEach((metric, mIndex) => {
227
+ if (!('verified' in metric)) {
228
+ updates[`snapshots.${index}.metrics.${mIndex}.verified`] = true;
229
+ changes.push(` - Snapshot ${index}: Added verified to metric ${mIndex}`);
230
+ }
231
+ });
232
+ }
233
+ }
234
+ });
235
+ }
236
+
237
+ // Apply updates
238
+ if (Object.keys(updates).length > 0 && !DRY_RUN) {
239
+ const updateDoc = {};
240
+
241
+ // Separate $set and $unset operations
242
+ if (updates.$unset) {
243
+ updateDoc.$unset = updates.$unset;
244
+ delete updates.$unset;
245
+ }
246
+
247
+ if (Object.keys(updates).length > 0) {
248
+ updateDoc.$set = updates;
249
+ }
250
+
251
+ await Financials.updateOne(
252
+ { _id: financial._id },
253
+ updateDoc
254
+ );
255
+
256
+ console.log(`Migrated financial ${financial._id}:`);
257
+ changes.forEach(change => console.log(change));
258
+ console.log('');
259
+ migratedCount++;
260
+ } else if (Object.keys(updates).length > 0 && DRY_RUN) {
261
+ console.log(`[DRY RUN] Would migrate financial ${financial._id}:`);
262
+ changes.forEach(change => console.log(change));
263
+ console.log('');
264
+ migratedCount++;
265
+ } else {
266
+ unchangedCount++;
267
+ }
268
+ }
269
+
270
+ console.log('\n=== Migration Summary ===');
271
+ console.log(`Documents migrated: ${migratedCount}`);
272
+ console.log(`Documents unchanged: ${unchangedCount}`);
273
+ console.log(`Total documents: ${financials.length}`);
274
+
275
+ if (DRY_RUN) {
276
+ console.log('\nThis was a dry run. No changes were made to the database.');
277
+ console.log('Run without --dry-run to apply changes.');
278
+ }
279
+ }