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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payservedb",
3
- "version": "5.8.6",
3
+ "version": "5.8.8",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -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
- // Basic validation
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
- } else if (!this.units || this.units.length === 0) {
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
- } else if (this.managementFee.type === 'percentage' && this.managementFee.value > 100) {
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
- } else {
143
- next();
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
- // Sync with lease data
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
- // Basic indexes
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'
@@ -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: String,
32
- trim: true,
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;