payservedb 5.8.6 → 5.8.8
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/package.json +1 -1
- package/src/models/propertyManagerContract.js +310 -13
- package/src/models/user.js +3 -3
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@ const PropertyManagerContractSchema = new Schema(
|
|
|
9
9
|
},
|
|
10
10
|
propertyManager: {
|
|
11
11
|
type: mongoose.Schema.Types.ObjectId,
|
|
12
|
-
ref: 'User',
|
|
12
|
+
ref: 'User', // References global User collection
|
|
13
13
|
required: [true, 'Property manager is required']
|
|
14
14
|
},
|
|
15
15
|
units: [{
|
|
@@ -19,7 +19,7 @@ const PropertyManagerContractSchema = new Schema(
|
|
|
19
19
|
}],
|
|
20
20
|
customerId: {
|
|
21
21
|
type: mongoose.Schema.Types.ObjectId,
|
|
22
|
-
ref: 'Customer',
|
|
22
|
+
ref: 'Customer', // Can reference global or facility-specific Customer
|
|
23
23
|
required: [true, 'Customer ID is required']
|
|
24
24
|
},
|
|
25
25
|
facilityId: {
|
|
@@ -81,7 +81,17 @@ const PropertyManagerContractSchema = new Schema(
|
|
|
81
81
|
value: {
|
|
82
82
|
type: Number,
|
|
83
83
|
required: [true, 'Management fee value is required'],
|
|
84
|
-
min: [0, 'Management fee value cannot be negative']
|
|
84
|
+
min: [0, 'Management fee value cannot be negative'],
|
|
85
|
+
validate: {
|
|
86
|
+
validator: function(value) {
|
|
87
|
+
// If type is percentage, value should not exceed 100
|
|
88
|
+
if (this.managementFee.type === 'percentage') {
|
|
89
|
+
return value <= 100;
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
},
|
|
93
|
+
message: 'Management fee percentage cannot exceed 100%'
|
|
94
|
+
}
|
|
85
95
|
}
|
|
86
96
|
},
|
|
87
97
|
// GL Account configurations
|
|
@@ -123,7 +133,7 @@ const PropertyManagerContractSchema = new Schema(
|
|
|
123
133
|
},
|
|
124
134
|
createdBy: {
|
|
125
135
|
type: mongoose.Schema.Types.ObjectId,
|
|
126
|
-
ref: 'User'
|
|
136
|
+
ref: 'User' // References global User collection
|
|
127
137
|
}
|
|
128
138
|
},
|
|
129
139
|
{
|
|
@@ -131,23 +141,72 @@ const PropertyManagerContractSchema = new Schema(
|
|
|
131
141
|
}
|
|
132
142
|
);
|
|
133
143
|
|
|
134
|
-
//
|
|
144
|
+
// Enhanced pre-save validation
|
|
135
145
|
PropertyManagerContractSchema.pre('save', function (next) {
|
|
146
|
+
// Validate date range
|
|
136
147
|
if (this.startDate && this.endDate && this.startDate >= this.endDate) {
|
|
137
148
|
next(new Error('End date must be after start date'));
|
|
138
|
-
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Validate units array
|
|
153
|
+
if (!this.units || this.units.length === 0) {
|
|
139
154
|
next(new Error('At least one unit must be specified'));
|
|
140
|
-
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Validate management fee
|
|
159
|
+
if (!this.managementFee || !this.managementFee.type || this.managementFee.value === undefined) {
|
|
160
|
+
next(new Error('Management fee type and value are required'));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Validate management fee percentage
|
|
165
|
+
if (this.managementFee.type === 'percentage' && this.managementFee.value > 100) {
|
|
141
166
|
next(new Error('Management fee percentage cannot exceed 100%'));
|
|
142
|
-
|
|
143
|
-
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Validate management fee value is non-negative
|
|
171
|
+
if (this.managementFee.value < 0) {
|
|
172
|
+
next(new Error('Management fee value cannot be negative'));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Validate payment due date range
|
|
177
|
+
if (this.paymentDueDate && (this.paymentDueDate < 1 || this.paymentDueDate > 31)) {
|
|
178
|
+
next(new Error('Payment due date must be between 1 and 31'));
|
|
179
|
+
return;
|
|
144
180
|
}
|
|
181
|
+
|
|
182
|
+
// Validate required fields for Active status
|
|
183
|
+
if (this.status === 'Active') {
|
|
184
|
+
if (!this.startDate) {
|
|
185
|
+
next(new Error('Start date is required for Active contracts'));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (!this.endDate) {
|
|
189
|
+
next(new Error('End date is required for Active contracts'));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (!this.paymentDueDate) {
|
|
193
|
+
next(new Error('Payment due date is required for Active contracts'));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (!this.frequency) {
|
|
197
|
+
next(new Error('Payment frequency is required for Active contracts'));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
next();
|
|
145
203
|
});
|
|
146
204
|
|
|
147
|
-
//
|
|
205
|
+
// Method to sync with lease data
|
|
148
206
|
PropertyManagerContractSchema.methods.syncWithLeaseData = async function(leaseData) {
|
|
149
207
|
if (!leaseData) return;
|
|
150
208
|
|
|
209
|
+
// Update contract fields from lease data
|
|
151
210
|
this.startDate = leaseData.leaseTerms?.startDate || this.startDate;
|
|
152
211
|
this.endDate = leaseData.leaseTerms?.endDate || this.endDate;
|
|
153
212
|
this.paymentDueDate = leaseData.financialTerms?.paymentDueDate || this.paymentDueDate;
|
|
@@ -157,15 +216,253 @@ PropertyManagerContractSchema.methods.syncWithLeaseData = async function(leaseDa
|
|
|
157
216
|
this.nextInvoiceDate = leaseData.billingCycle?.nextInvoiceDate || this.nextInvoiceDate;
|
|
158
217
|
this.lastInvoiceDate = leaseData.billingCycle?.lastInvoiceDate || this.lastInvoiceDate;
|
|
159
218
|
|
|
219
|
+
// Automatically activate contract if all required fields are present
|
|
220
|
+
if (this.startDate && this.endDate && this.paymentDueDate && this.frequency && this.status === 'Inactive') {
|
|
221
|
+
this.status = 'Active';
|
|
222
|
+
}
|
|
223
|
+
|
|
160
224
|
return this.save();
|
|
161
225
|
};
|
|
162
226
|
|
|
163
|
-
//
|
|
227
|
+
// Method to check if contract can be activated
|
|
228
|
+
PropertyManagerContractSchema.methods.canBeActivated = function() {
|
|
229
|
+
return !!(this.startDate && this.endDate && this.paymentDueDate && this.frequency);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// Method to get missing required fields
|
|
233
|
+
PropertyManagerContractSchema.methods.getMissingRequiredFields = function() {
|
|
234
|
+
const requiredFields = ['startDate', 'endDate', 'paymentDueDate', 'frequency'];
|
|
235
|
+
return requiredFields.filter(field => !this[field]);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// Virtual for contract completion status
|
|
239
|
+
PropertyManagerContractSchema.virtual('isComplete').get(function() {
|
|
240
|
+
return this.canBeActivated();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Virtual for contract duration in days
|
|
244
|
+
PropertyManagerContractSchema.virtual('durationInDays').get(function() {
|
|
245
|
+
if (!this.startDate || !this.endDate) return null;
|
|
246
|
+
const diffTime = Math.abs(this.endDate - this.startDate);
|
|
247
|
+
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
248
|
+
});
|
|
249
|
+
// Virtual for contract remaining days
|
|
250
|
+
PropertyManagerContractSchema.virtual('remainingDays').get(function() {
|
|
251
|
+
if (!this.endDate) return null;
|
|
252
|
+
const now = new Date();
|
|
253
|
+
if (now > this.endDate) return 0;
|
|
254
|
+
const diffTime = Math.abs(this.endDate - now);
|
|
255
|
+
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Indexes for better query performance
|
|
164
259
|
PropertyManagerContractSchema.index({ customerId: 1, status: 1 });
|
|
165
|
-
PropertyManagerContractSchema.index({ facilityId: 1 });
|
|
166
|
-
PropertyManagerContractSchema.index({ propertyManager: 1 });
|
|
260
|
+
PropertyManagerContractSchema.index({ facilityId: 1, status: 1 });
|
|
261
|
+
PropertyManagerContractSchema.index({ propertyManager: 1, facilityId: 1 });
|
|
262
|
+
PropertyManagerContractSchema.index({ units: 1, status: 1 });
|
|
167
263
|
PropertyManagerContractSchema.index({ nextInvoiceDate: 1, status: 1 });
|
|
264
|
+
PropertyManagerContractSchema.index({ endDate: 1, status: 1 }); // For contract expiration queries
|
|
265
|
+
PropertyManagerContractSchema.index({ 'managementFee.type': 1, 'managementFee.value': 1 }); // For fee analysis
|
|
266
|
+
PropertyManagerContractSchema.index({ createdAt: -1 }); // For recent contracts
|
|
267
|
+
PropertyManagerContractSchema.index({ startDate: 1, endDate: 1 }); // For date range queries
|
|
268
|
+
|
|
269
|
+
// Compound indexes for common query patterns
|
|
270
|
+
PropertyManagerContractSchema.index({
|
|
271
|
+
facilityId: 1,
|
|
272
|
+
propertyManager: 1,
|
|
273
|
+
status: 1
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
PropertyManagerContractSchema.index({
|
|
277
|
+
facilityId: 1,
|
|
278
|
+
customerId: 1,
|
|
279
|
+
status: 1
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Text index for searching contract names
|
|
283
|
+
PropertyManagerContractSchema.index({
|
|
284
|
+
contractName: 'text'
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Static methods for common queries
|
|
288
|
+
PropertyManagerContractSchema.statics.findActiveContractsByFacility = function(facilityId) {
|
|
289
|
+
return this.find({
|
|
290
|
+
facilityId: facilityId,
|
|
291
|
+
status: 'Active'
|
|
292
|
+
}).populate('propertyManager', 'fullName email')
|
|
293
|
+
.populate('units', 'name unitType')
|
|
294
|
+
.populate('customerId', 'firstName lastName email');
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
PropertyManagerContractSchema.statics.findContractsByPropertyManager = function(propertyManagerId, facilityId) {
|
|
298
|
+
return this.find({
|
|
299
|
+
propertyManager: propertyManagerId,
|
|
300
|
+
facilityId: facilityId
|
|
301
|
+
}).populate('units', 'name unitType')
|
|
302
|
+
.populate('customerId', 'firstName lastName email')
|
|
303
|
+
.sort({ createdAt: -1 });
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
PropertyManagerContractSchema.statics.findExpiringContracts = function(facilityId, daysAhead = 30) {
|
|
307
|
+
const futureDate = new Date();
|
|
308
|
+
futureDate.setDate(futureDate.getDate() + daysAhead);
|
|
309
|
+
|
|
310
|
+
return this.find({
|
|
311
|
+
facilityId: facilityId,
|
|
312
|
+
status: 'Active',
|
|
313
|
+
endDate: {
|
|
314
|
+
$gte: new Date(),
|
|
315
|
+
$lte: futureDate
|
|
316
|
+
}
|
|
317
|
+
}).populate('propertyManager', 'fullName email')
|
|
318
|
+
.populate('units', 'name unitType')
|
|
319
|
+
.populate('customerId', 'firstName lastName email')
|
|
320
|
+
.sort({ endDate: 1 });
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
PropertyManagerContractSchema.statics.findContractsRequiringInvoicing = function(facilityId) {
|
|
324
|
+
const today = new Date();
|
|
325
|
+
today.setHours(0, 0, 0, 0);
|
|
326
|
+
|
|
327
|
+
return this.find({
|
|
328
|
+
facilityId: facilityId,
|
|
329
|
+
status: 'Active',
|
|
330
|
+
nextInvoiceDate: {
|
|
331
|
+
$lte: today
|
|
332
|
+
}
|
|
333
|
+
}).populate('propertyManager', 'fullName email')
|
|
334
|
+
.populate('units', 'name unitType')
|
|
335
|
+
.populate('customerId', 'firstName lastName email');
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// Instance methods for contract management
|
|
339
|
+
PropertyManagerContractSchema.methods.calculateNextInvoiceDate = function() {
|
|
340
|
+
if (!this.lastInvoiceDate && !this.startDate) return null;
|
|
341
|
+
|
|
342
|
+
const baseDate = this.lastInvoiceDate || this.startDate;
|
|
343
|
+
const nextDate = new Date(baseDate);
|
|
344
|
+
|
|
345
|
+
switch (this.frequency) {
|
|
346
|
+
case 'Monthly':
|
|
347
|
+
nextDate.setMonth(nextDate.getMonth() + 1);
|
|
348
|
+
break;
|
|
349
|
+
case 'Quarterly':
|
|
350
|
+
nextDate.setMonth(nextDate.getMonth() + 3);
|
|
351
|
+
break;
|
|
352
|
+
case 'Annually':
|
|
353
|
+
nextDate.setFullYear(nextDate.getFullYear() + 1);
|
|
354
|
+
break;
|
|
355
|
+
default:
|
|
356
|
+
nextDate.setMonth(nextDate.getMonth() + 1); // Default to monthly
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Set to the payment due date of the month
|
|
360
|
+
if (this.paymentDueDate) {
|
|
361
|
+
const lastDayOfMonth = new Date(nextDate.getFullYear(), nextDate.getMonth() + 1, 0).getDate();
|
|
362
|
+
const dueDate = Math.min(this.paymentDueDate, lastDayOfMonth);
|
|
363
|
+
nextDate.setDate(dueDate);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return nextDate;
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
PropertyManagerContractSchema.methods.updateInvoiceDates = async function(invoiceDate = new Date()) {
|
|
370
|
+
this.lastInvoiceDate = invoiceDate;
|
|
371
|
+
this.nextInvoiceDate = this.calculateNextInvoiceDate();
|
|
372
|
+
return this.save();
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
PropertyManagerContractSchema.methods.activateContract = async function() {
|
|
376
|
+
if (!this.canBeActivated()) {
|
|
377
|
+
throw new Error(`Cannot activate contract: missing required fields - ${this.getMissingRequiredFields().join(', ')}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
this.status = 'Active';
|
|
381
|
+
|
|
382
|
+
// Set next invoice date if not already set
|
|
383
|
+
if (!this.nextInvoiceDate) {
|
|
384
|
+
this.nextInvoiceDate = this.calculateNextInvoiceDate();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return this.save();
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
PropertyManagerContractSchema.methods.suspendContract = async function(reason) {
|
|
391
|
+
this.status = 'Suspended';
|
|
392
|
+
this.suspensionReason = reason;
|
|
393
|
+
return this.save();
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
PropertyManagerContractSchema.methods.terminateContract = async function(reason, terminationDate = new Date()) {
|
|
397
|
+
this.status = 'Terminated';
|
|
398
|
+
this.terminationReason = reason;
|
|
399
|
+
this.terminationDate = terminationDate;
|
|
400
|
+
return this.save();
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
PropertyManagerContractSchema.methods.completeContract = async function() {
|
|
404
|
+
this.status = 'Completed';
|
|
405
|
+
this.completionDate = new Date();
|
|
406
|
+
return this.save();
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
// Pre-findOneAndUpdate middleware to maintain data integrity
|
|
410
|
+
PropertyManagerContractSchema.pre(['findOneAndUpdate', 'updateOne', 'updateMany'], function() {
|
|
411
|
+
const update = this.getUpdate();
|
|
412
|
+
|
|
413
|
+
// If status is being changed to Active, ensure required fields are present
|
|
414
|
+
if (update.$set && update.$set.status === 'Active') {
|
|
415
|
+
const requiredFields = ['startDate', 'endDate', 'paymentDueDate', 'frequency'];
|
|
416
|
+
// Note: We can't validate here because we don't have access to the full document
|
|
417
|
+
// Validation should be done in the application logic before calling update
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Update the updatedAt field
|
|
421
|
+
if (update.$set) {
|
|
422
|
+
update.$set.updatedAt = new Date();
|
|
423
|
+
} else {
|
|
424
|
+
this.set({ updatedAt: new Date() });
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// Post-save middleware for logging and notifications
|
|
429
|
+
PropertyManagerContractSchema.post('save', function(doc) {
|
|
430
|
+
// Log contract creation/updates
|
|
431
|
+
console.log(`Property Manager Contract ${doc.isNew ? 'created' : 'updated'}: ${doc.contractName} (${doc._id})`);
|
|
432
|
+
|
|
433
|
+
// Here you could add webhook calls, email notifications, etc.
|
|
434
|
+
// Example: notify property manager of contract status changes
|
|
435
|
+
if (this.isModified('status')) {
|
|
436
|
+
console.log(`Contract status changed to: ${doc.status}`);
|
|
437
|
+
// Add notification logic here
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// Error handling for validation errors
|
|
442
|
+
PropertyManagerContractSchema.post('save', function(error, doc, next) {
|
|
443
|
+
if (error.name === 'ValidationError') {
|
|
444
|
+
const messages = Object.values(error.errors).map(err => err.message);
|
|
445
|
+
next(new Error(`Contract validation failed: ${messages.join(', ')}`));
|
|
446
|
+
} else {
|
|
447
|
+
next(error);
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// JSON transformation to remove sensitive data when returning to client
|
|
452
|
+
PropertyManagerContractSchema.methods.toJSON = function() {
|
|
453
|
+
const contract = this.toObject();
|
|
454
|
+
|
|
455
|
+
// Add computed fields
|
|
456
|
+
contract.isComplete = this.isComplete;
|
|
457
|
+
contract.canBeActivated = this.canBeActivated();
|
|
458
|
+
contract.missingRequiredFields = this.getMissingRequiredFields();
|
|
459
|
+
contract.durationInDays = this.durationInDays;
|
|
460
|
+
contract.remainingDays = this.remainingDays;
|
|
461
|
+
|
|
462
|
+
return contract;
|
|
463
|
+
};
|
|
168
464
|
|
|
465
|
+
// Export the schema
|
|
169
466
|
module.exports = {
|
|
170
467
|
schema: PropertyManagerContractSchema,
|
|
171
468
|
name: 'PropertyManagerContract'
|
package/src/models/user.js
CHANGED
|
@@ -28,8 +28,8 @@ const userSchema = new mongoose.Schema({
|
|
|
28
28
|
enum: ['Company', 'Project Manager', 'Universal', 'Core', 'Resident', 'Landlord', 'Supplier'],
|
|
29
29
|
},
|
|
30
30
|
department: {
|
|
31
|
-
type:
|
|
32
|
-
|
|
31
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
32
|
+
ref: 'FacilityDepartment',
|
|
33
33
|
},
|
|
34
34
|
role: {
|
|
35
35
|
type: String,
|
|
@@ -184,4 +184,4 @@ userSchema.index({ fullName: 'text', email: 'text' });
|
|
|
184
184
|
|
|
185
185
|
const User = mongoose.model('User', userSchema);
|
|
186
186
|
|
|
187
|
-
module.exports = User;
|
|
187
|
+
module.exports = User;
|