@dhyasama/totem-models 1.0.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.
@@ -0,0 +1,357 @@
1
+ "use strict";
2
+
3
+ module.exports = function(mongoose, config) {
4
+
5
+ var
6
+
7
+ Schema = mongoose.Schema,
8
+ env = process.env.NODE_ENV || 'development',
9
+ s3 = require('@dhyasama/ffvc-s3'),
10
+ utils = require('@dhyasama/ffvc-utilities'),
11
+ Crypto = require('@dhyasama/ffvc-crypto'),
12
+ crypto = new Crypto({'key': config.crypto.key}),
13
+ _ = require('underscore');
14
+
15
+ var Document = new Schema({
16
+
17
+ lpid: { type: Schema.Types.ObjectId, required: true },
18
+
19
+ fundInvolved: {
20
+ type: Schema.ObjectId,
21
+ ref: 'Fund'
22
+ },
23
+
24
+ published: { type: Boolean, required: true },
25
+
26
+ filename: { type: String, required: true },
27
+
28
+ filetype: { type: String, required: true },
29
+
30
+ filesize: { type: String, required: true },
31
+
32
+ s3key: { type: String, required: true },
33
+
34
+ doctype: {
35
+ type: String,
36
+ required: true,
37
+ enum: ['annual-audit', 'portfolio-holding', 'capital-call', 'k1', 'distribution', 'quarterly-account-statement'] },
38
+
39
+ scope: {
40
+ type: String,
41
+ required: true,
42
+ enum: ['lp', 'fund'] },
43
+
44
+ // for fund-level docs:
45
+ // upload one doc to s3 with this id in the key
46
+ // e.g., an annual audit for ff Rose would be stored at documents/[scopeid] and shared by all lps in ff Rose
47
+ // for lp-level docs:
48
+ // used to get a batch of docs that were uploaded together
49
+ // e.g., a capital call for ff Sapphire requires a separate document to be uploaded for each lp in the fund
50
+ // scopeid allows retrieving all of the doc records at once for admin purposes
51
+ // unlike fund level docs, these are stored at documents/[_id] so they can be retrieved on an individual basis
52
+ scopeid: { type: String, required: true },
53
+
54
+ year: { type: Number, required: false },
55
+
56
+ quarter: {
57
+ type: String,
58
+ required: false,
59
+ enum: [null, '', 'Q1', 'Q2', 'Q3', 'Q4'] },
60
+
61
+ capitalCall: {
62
+ noticeNumber: { type: Number, required: false },
63
+ noticeDate: { type: Date, required: false },
64
+ dueDate: { type: Date, required: false }
65
+ },
66
+
67
+ description: { type: String, required: false },
68
+
69
+ history: [{
70
+ timestamp: { type: Date, required: true },
71
+ username: { type: String, required: true },
72
+ action: { type: String, required: true, enum: ['created', 'published', 'unpublished', 'replaced'] }
73
+ }]
74
+
75
+ });
76
+
77
+
78
+
79
+ /////////////////////
80
+ // INSTANCE METHODS
81
+ /////////////////////
82
+
83
+ Document.methods.friendlyFundName = function() {
84
+ var fund = this.fundInvolved.name;
85
+ return fund;
86
+ };
87
+
88
+ Document.methods.friendlyDocType = function() {
89
+ var doctype = this.doctype ? utils.capitalizeFirstLetters(this.doctype.toLowerCase().split('-').join(' ')) : '';
90
+ return doctype;
91
+ };
92
+
93
+ Document.methods.friendlyDocumentName = function() {
94
+ var year = this.year ? this.year + ' ' : '';
95
+ var quarter = this.quarter ? this.quarter + ' ' : '';
96
+ var callNum = this.doctype == 'capital-call' && utils.isNumeric(this.capitalCall.noticeNumber) ? ' #' + this.capitalCall.noticeNumber : '';
97
+ var desc = this.doctype == 'distribution' ? ' - ' + this.description : '';
98
+
99
+ return year + quarter + this.friendlyDocType() + callNum + desc;
100
+
101
+ };
102
+
103
+ Document.methods.friendlyFilesize = function() {
104
+ return utils.humanizeFilesize(this.filesize);
105
+ };
106
+
107
+ Document.methods.encryptId = function() {
108
+ return crypto.encrypt(this.id);
109
+ };
110
+
111
+
112
+
113
+ ////////////////////////
114
+ // STATICS
115
+ ////////////////////////
116
+
117
+ Document.statics.exists = function(meta, cb) {
118
+
119
+ var model = this;
120
+
121
+ var done = function(err, docs) {
122
+ return cb(!err && docs.length >= 1, docs[0]);
123
+ };
124
+
125
+ switch(meta.doctype.toLowerCase()) {
126
+ case 'annual-audit':
127
+ listAnnualAudits(model, meta.fundId, meta.year, done);
128
+ break;
129
+ case 'capital-call':
130
+ listCapitalCalls(model, meta.fundId, meta.callNumber, done);
131
+ break;
132
+ case 'portfolio-holding':
133
+ listPortfolioHoldings(model, meta.fundId, meta.year, meta.quarter, done);
134
+ break;
135
+ case 'k1':
136
+ listK1s(model, meta.fundId, meta.year, done);
137
+ break;
138
+ case 'quarterly-account-statement':
139
+ listQuarterlyAccountStatements(model, meta.fundId, meta.year, meta.quarter, done);
140
+ break;
141
+ default:
142
+ return cb(false, null);
143
+ break;
144
+ }
145
+
146
+ };
147
+
148
+ Document.statics.getById = function (cipher, cb) {
149
+
150
+ var id = crypto.decrypt(cipher);
151
+
152
+ this
153
+ .findById(id)
154
+ .populate({
155
+ path: 'lpid',
156
+ model: 'LimitedPartner',
157
+ select: 'name shortName id'
158
+ })
159
+ .populate('fundInvolved')
160
+ .exec(function (err, document) {
161
+ if (document) { document.secureid = cipher; }
162
+ document.secureid = cipher;
163
+ cb(err, document);
164
+ });
165
+
166
+ };
167
+
168
+ Document.statics.list = function (cb) {
169
+
170
+ this
171
+ .find()
172
+ .populate({
173
+ path: 'lpid',
174
+ model: 'LimitedPartner',
175
+ select: 'name shortName id'
176
+ })
177
+ .populate('fundInvolved')
178
+ .sort({ year: -1, quarter: -1 })
179
+ .exec(function (err, documents) {
180
+ encryptDocumentIds(documents);
181
+ cb(err, documents);
182
+ });
183
+
184
+ };
185
+
186
+ Document.statics.listAllForLP = function (lpid, cb) {
187
+
188
+ this
189
+ .find({ lpid: lpid, published: true })
190
+ .populate('fundInvolved')
191
+ .sort({ year: -1, quarter: -1, fund: 1 })
192
+ .exec(function (err, documents) {
193
+ encryptDocumentIds(documents);
194
+ return cb(err, documents);
195
+ });
196
+
197
+ };
198
+
199
+ Document.statics.listForScopeId = function (cipher, cb) {
200
+
201
+ // lp-level docs are grouped to scope id to faciliate pulling a batch all at once
202
+
203
+ var id = crypto.decrypt(cipher);
204
+
205
+ this
206
+ .find({ 'scopeid': id })
207
+ .populate({
208
+ path: 'lpid',
209
+ model: 'LimitedPartner',
210
+ select: 'name shortName id'
211
+ })
212
+ .populate('fundInvolved')
213
+ .exec(function (err, documents) {
214
+ console.log(documents.length);
215
+ encryptDocumentIds(documents);
216
+ return cb(err, _.sortBy(documents, function(doc) {
217
+ var lowered = doc.lpid && doc.lpid.name ? doc.lpid.name.toLowerCase() : 'Unknown';
218
+ return lowered;
219
+ }));
220
+ });
221
+
222
+ };
223
+
224
+ Document.statics.listFundLevel = function (cb) {
225
+
226
+ this
227
+ .find({ 'scope': 'fund' })
228
+ .populate({
229
+ path: 'lpid',
230
+ model: 'LimitedPartner',
231
+ select: 'name shortName id'
232
+ })
233
+ .populate('fundInvolved')
234
+ .sort({ year: -1, quarter: -1 })
235
+ .exec(function (err, documents) {
236
+ var docs = _.uniq(documents, false, function(doc) {
237
+ return doc.scopeid;
238
+ });
239
+ encryptDocumentIds(docs);
240
+ cb(err, docs);
241
+ });
242
+
243
+ };
244
+
245
+ Document.statics.listLpLevel = function (cb) {
246
+
247
+ this
248
+ .find({ 'scope': 'lp' })
249
+ .populate({
250
+ path: 'lpid',
251
+ model: 'LimitedPartner',
252
+ select: 'name shortName id'
253
+ })
254
+ .populate('fundInvolved')
255
+ .sort({ year: -1 })
256
+ .exec(function (err, documents) {
257
+ var docs = _.uniq(documents, false, function(doc) {
258
+ return doc.scopeid;
259
+ });
260
+ encryptDocumentIds(docs);
261
+ cb(err, docs);
262
+ });
263
+
264
+ };
265
+
266
+ Document.statics.upsert = function(doc, cb) {
267
+ return save(doc, cb);
268
+ };
269
+
270
+
271
+
272
+ ////////////////////////
273
+ // HELPERS
274
+ ////////////////////////
275
+
276
+ function encryptDocumentIds(documents) {
277
+
278
+ var result = _.map(documents, function(doc) {
279
+ doc.secureid = crypto.encrypt(doc.id);
280
+ return doc;
281
+ });
282
+
283
+ return result;
284
+
285
+ }
286
+
287
+ function listAnnualAudits(model, fundId, year, cb) {
288
+
289
+ model
290
+ .find({ doctype: 'annual-audit', fundInvolved: fundId, year: year })
291
+ .sort({ year: -1 })
292
+ .exec(function (err, documents) {
293
+ encryptDocumentIds(documents);
294
+ cb(err, documents);
295
+ });
296
+
297
+ };
298
+
299
+ function listCapitalCalls(model, fundId, callNumber, cb) {
300
+
301
+ model
302
+ .find({ doctype: 'capital-call', fundInvolved: fundId, 'capitalCall.noticeNumber': callNumber })
303
+ .sort({ 'capitalCall.noticeNumber': -1 })
304
+ .exec(function (err, documents) {
305
+ encryptDocumentIds(documents);
306
+ cb(err, documents);
307
+ });
308
+
309
+ };
310
+
311
+ function listK1s(model, fundId, year, cb) {
312
+
313
+ model
314
+ .find({ doctype: 'k1', fundInvolved: fundId, year: year })
315
+ .sort({ 'year': -1 })
316
+ .exec(function (err, documents) {
317
+ encryptDocumentIds(documents);
318
+ cb(err, documents);
319
+ });
320
+
321
+ };
322
+
323
+ function listPortfolioHoldings(model, fundId, year, quarter, cb) {
324
+
325
+ model
326
+ .find({ doctype: 'portfolio-holding', fundInvolved: fundId, year: year, quarter: quarter })
327
+ .sort({ year: -1, quarter: -1 })
328
+ .exec(function (err, documents) {
329
+ return cb(err, encryptDocumentIds(documents));
330
+ });
331
+
332
+ };
333
+
334
+ function listQuarterlyAccountStatements(model, fundId, year, quarter, cb) {
335
+
336
+ model
337
+ .find({ doctype: 'quarterly-account-statement', fundInvolved: fundId, year: year, quarter: quarter })
338
+ .sort({ year: -1, quarter: -1 })
339
+ .exec(function (err, documents) {
340
+ encryptDocumentIds(documents);
341
+ cb(err, documents);
342
+ });
343
+
344
+ };
345
+
346
+ function save(document, cb) {
347
+ document.save(function(err, updatedDocument) {
348
+ cb(err, updatedDocument);
349
+ });
350
+ }
351
+
352
+ Document.set('autoIndex', (env == 'development'));
353
+ Document.on('index', function(err) { console.log('error building document indexes: ' + err); });
354
+
355
+ mongoose.model('Document', Document);
356
+
357
+ };
package/lib/Fund.js ADDED
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+
3
+ module.exports = function(mongoose, config) {
4
+
5
+ var Schema = mongoose.Schema
6
+ var env = process.env.NODE_ENV || 'development';
7
+
8
+ var Fund = new Schema({
9
+
10
+ name: { type: String, required: true },
11
+ slug: { type: String, required: true, unique: true },
12
+ shortName: { type: String, required: true },
13
+ spv: { type: Boolean, default: false },
14
+ hexColorCode: { type: String, required: true, default: '#cccccc' },
15
+ closeDate: { type: Date },
16
+ amount: { type: Number },
17
+ crunchbase: { uuid: { type: String, default: '', unique: true } }
18
+
19
+ });
20
+
21
+ /************* BASIC FIELDS **********************/
22
+
23
+ Fund.methods.newBasic = function(field, value) {
24
+
25
+ var compoundField = false;
26
+ if(field.split(".").length > 1) {
27
+ compoundField = true;
28
+ }
29
+
30
+ if(!compoundField) {
31
+ this[field] = value;
32
+ } else {
33
+ var parentField = field.split('.')[0];
34
+ var childField = field.split('.')[1];
35
+
36
+ this[parentField][childField] = value;
37
+ }
38
+
39
+ };
40
+
41
+ Fund.methods.updateBasic = function(field, value) {
42
+
43
+ var compoundField = false;
44
+ if(field.split(".").length > 1) {
45
+ compoundField = true;
46
+ }
47
+
48
+ if(!compoundField) {
49
+ var currentVal = this[field];
50
+ this[field] = value;
51
+ } else {
52
+ var parentField = field.split('.')[0];
53
+ var childField = field.split('.')[1];
54
+
55
+ var currentVal = this[parentField][childField];
56
+ this[parentField][childField] = value;
57
+ }
58
+
59
+ var oldFieldObj = {
60
+ field: field,
61
+ value: currentVal,
62
+ end: new Date()
63
+ };
64
+
65
+ this.previous.push(oldFieldObj);
66
+
67
+ };
68
+
69
+ ///////////////////////////////////////
70
+
71
+ Fund.statics.getById = function (id, cb) {
72
+ this
73
+ .findById(id)
74
+ .exec(cb);
75
+ };
76
+
77
+ Fund.statics.getBySlug = function (slug, cb) {
78
+ this
79
+ .find({slug: slug})
80
+ .exec(cb);
81
+ };
82
+
83
+ Fund.statics.findBySlugs = function (slugs, cb) {
84
+ this
85
+ .find({ 'slug': { $in : slugs }})
86
+ .exec(cb);
87
+ };
88
+
89
+ Fund.statics.getCount = function (cb) {
90
+ this
91
+ .count({})
92
+ .exec(cb);
93
+ };
94
+
95
+ Fund.statics.list = function (cb) {
96
+ this
97
+ .find()
98
+ .exec(cb);
99
+ };
100
+
101
+ Fund.statics.search = function (query, options, cb) {
102
+
103
+ if (!cb) throw new Error('cb is required');
104
+ if (!query) return cb(null, []);
105
+
106
+ var conditions = {
107
+ 'name': new RegExp(query.query, "i"),
108
+ 'deleted': {$ne: true}
109
+ };
110
+
111
+ this
112
+ .find(conditions)
113
+ .sort('name')
114
+ .exec(cb);
115
+
116
+ };
117
+
118
+ Fund.statics.upsert = function(fund, username, cb) {
119
+
120
+ if (!fund) { return cb(new Error('Fund is required'), null); }
121
+ if (!username) { return cb(new Error('Username is required'), null); }
122
+
123
+ fund.save(cb);
124
+
125
+ };
126
+
127
+ Fund.post('remove', function(doc) {
128
+ // Debt.debts.internal.fund
129
+ // Document.fundInvolved
130
+ // Investment.internal.fund
131
+ // Investor.funds
132
+ // LimitedPartner.fundsInvested.fund
133
+ // Message.fundsInvolved
134
+ // News.fundsInvolved
135
+ // PortfolioCompany.fundsInvested
136
+ });
137
+
138
+ Fund.set('autoIndex', true);
139
+ Fund.on('index', function(err) { console.log('error building fund indexes: ' + err); });
140
+
141
+ mongoose.model('Fund', Fund);
142
+
143
+ };