@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 +44 -24
- package/package.json +1 -1
- package/scripts/migrate-financials-schema.js +279 -0
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
|
-
|
|
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
|
-
|
|
81
|
+
approval:{ type: Boolean, default: false },
|
|
82
82
|
|
|
83
|
-
status: { type: String, enum: ['Created', 'Scheduled', 'Delivered', 'Opened', '
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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.
|
|
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', '
|
|
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
|
|
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
|
|
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(
|
|
1021
|
-
else if(!
|
|
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(
|
|
1024
|
-
else if(
|
|
1025
|
-
else if(
|
|
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
|
@@ -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
|
+
}
|