payservedb 8.3.3 → 8.3.4
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/.env +2 -2
- package/.idea/material_theme_project_new.xml +12 -0
- package/.idea/modules.xml +8 -0
- package/.idea/psdb.iml +12 -0
- package/.idea/vcs.xml +6 -0
- package/index.js +306 -306
- package/package.json +17 -17
- package/src/models/account.js +52 -52
- package/src/models/agent_departments.js +59 -59
- package/src/models/agent_notifications.js +53 -53
- package/src/models/agent_performance.js +127 -127
- package/src/models/agent_roles.js +77 -77
- package/src/models/agents.js +154 -154
- package/src/models/apilog.js +18 -18
- package/src/models/approvalsWorkflows.js +49 -49
- package/src/models/archivedapilog.js +18 -18
- package/src/models/asset.js +92 -92
- package/src/models/assetsAssignment.js +64 -64
- package/src/models/auditTrail.js +346 -346
- package/src/models/bankdetails.js +47 -43
- package/src/models/billerAddress.js +124 -124
- package/src/models/booking_invoice.js +151 -151
- package/src/models/bookinganalytics.js +63 -63
- package/src/models/bookingconfig.js +45 -45
- package/src/models/bookingproperty.js +122 -122
- package/src/models/bookingreservation.js +192 -192
- package/src/models/bookingrevenuerecord.js +84 -84
- package/src/models/budget.js +95 -95
- package/src/models/budgetCategory.js +19 -19
- package/src/models/campaigns.js +108 -108
- package/src/models/cashpayment.js +264 -264
- package/src/models/combinedUnits.js +62 -62
- package/src/models/common_area_electricity.js +38 -38
- package/src/models/common_area_generator.js +41 -41
- package/src/models/common_area_utility_alert.js +37 -37
- package/src/models/common_area_water.js +39 -39
- package/src/models/communication_status.js +33 -33
- package/src/models/communication_user_opt.js +32 -32
- package/src/models/company.js +53 -53
- package/src/models/coreBaseSettings.js +16 -16
- package/src/models/coreInvoiceSettings.js +100 -100
- package/src/models/counter_schema.js +21 -21
- package/src/models/country_tax.js +42 -42
- package/src/models/currency_settings.js +39 -39
- package/src/models/customer.js +210 -210
- package/src/models/customer_satisfaction_survey.js +278 -278
- package/src/models/customer_surveys.js +139 -139
- package/src/models/customer_tickets.js +239 -239
- package/src/models/dailyChecklist.js +312 -312
- package/src/models/default_payment_details.js +17 -17
- package/src/models/deliveryTimeMarks.js +18 -18
- package/src/models/document_type.js +19 -19
- package/src/models/dutyRosterChecklist.js +250 -250
- package/src/models/dutyroster.js +136 -136
- package/src/models/email.js +37 -37
- package/src/models/email_sms_queue.js +61 -61
- package/src/models/entry_exit.js +53 -53
- package/src/models/expense.js +99 -99
- package/src/models/expense_category.js +45 -45
- package/src/models/facility.js +62 -62
- package/src/models/facilityBillingPrices.js +29 -29
- package/src/models/facilityInvoice.js +223 -224
- package/src/models/facilityInvoicePayment.js +47 -52
- package/src/models/facilityInvoiceRecipient.js +32 -32
- package/src/models/facilityWalletTransactionsMetadata.js +236 -236
- package/src/models/facility_departements.js +20 -20
- package/src/models/facility_payment_details.js +20 -20
- package/src/models/facilityasset.js +25 -25
- package/src/models/faq.js +18 -18
- package/src/models/gl_account_double_entries.js +25 -25
- package/src/models/gl_accounts.js +56 -56
- package/src/models/gl_entries.js +49 -49
- package/src/models/goodsReceivedNotes.js +115 -115
- package/src/models/guard.js +47 -47
- package/src/models/handover.js +247 -247
- package/src/models/inspection_category.js +38 -38
- package/src/models/invoice.js +387 -387
- package/src/models/invoicing_schedule.js +36 -36
- package/src/models/item_inspection.js +96 -96
- package/src/models/knowledge_base.js +109 -109
- package/src/models/knowledge_base_rating.js +44 -44
- package/src/models/leaseagreement.js +236 -243
- package/src/models/leasetemplate.js +17 -17
- package/src/models/levy.js +223 -223
- package/src/models/levy_invoice_settings.js +26 -26
- package/src/models/levycontract.js +173 -173
- package/src/models/levytype.js +23 -23
- package/src/models/maintenance_service_vendor.js +38 -38
- package/src/models/maintenance_services.js +17 -17
- package/src/models/maintenancerequisition.js +31 -31
- package/src/models/master_workplan.js +32 -32
- package/src/models/master_workplan_child.js +34 -34
- package/src/models/message.js +38 -38
- package/src/models/module.js +21 -21
- package/src/models/notification.js +44 -44
- package/src/models/paymentTermsMarks.js +19 -19
- package/src/models/penalty.js +76 -76
- package/src/models/pendingCredentials.js +32 -32
- package/src/models/powerMeterCommunicationProtocol.js +17 -17
- package/src/models/powerMeterCustomerAccount.js +78 -78
- package/src/models/powerMeterCustomerBand.js +14 -14
- package/src/models/powerMeterDailyReading.js +30 -30
- package/src/models/powerMeterGateways.js +40 -40
- package/src/models/powerMeterMonthlyReading.js +34 -34
- package/src/models/powerMeterPowerCharges.js +85 -85
- package/src/models/powerMeterSettings.js +159 -159
- package/src/models/powerMeterSingleDayReading.js +32 -32
- package/src/models/powerMeters.js +116 -116
- package/src/models/powerMetersManufacturer.js +14 -14
- package/src/models/power_meter_account.js +81 -81
- package/src/models/power_meter_command_logs.js +30 -30
- package/src/models/power_meter_command_queue.js +33 -33
- package/src/models/power_meter_negative_balance.js +44 -44
- package/src/models/power_prepaid_credits.js +47 -47
- package/src/models/power_prepaid_debits.js +53 -53
- package/src/models/power_prepaid_orders.js +78 -78
- package/src/models/power_sms_notification.js +26 -26
- package/src/models/propertyManagerContract.js +556 -556
- package/src/models/propertyManagerRevenue.js +195 -195
- package/src/models/purchaseOrderInvoice.js +74 -74
- package/src/models/purchase_order.js +213 -213
- package/src/models/purchase_request.js +110 -110
- package/src/models/refresh_token.js +23 -23
- package/src/models/reminder.js +197 -197
- package/src/models/report.js +13 -13
- package/src/models/resident.js +121 -121
- package/src/models/rfq_details.js +131 -131
- package/src/models/rfq_response.js +153 -153
- package/src/models/service_charge_invoice_upload.js +42 -42
- package/src/models/service_charge_payments.js +27 -27
- package/src/models/servicerequest.js +55 -55
- package/src/models/settings.js +62 -62
- package/src/models/short_urls.js +21 -21
- package/src/models/smart_meter_daily_consumption.js +44 -44
- package/src/models/sms_africastalking.js +20 -20
- package/src/models/sms_balance_notification.js +26 -26
- package/src/models/sms_meliora.js +20 -20
- package/src/models/staff.js +36 -36
- package/src/models/stocksandspare.js +161 -161
- package/src/models/suppliers.js +74 -74
- package/src/models/tickets.js +173 -173
- package/src/models/tickets_category.js +72 -72
- package/src/models/unitManagementTemplate.js +44 -44
- package/src/models/unitasset.js +25 -25
- package/src/models/units.js +118 -118
- package/src/models/user.js +186 -186
- package/src/models/valueaddedservices.js +79 -79
- package/src/models/vas_invoices_upload.js +50 -50
- package/src/models/vas_payments.js +24 -24
- package/src/models/vasinvoice.js +192 -192
- package/src/models/vasvendor.js +57 -57
- package/src/models/visitLog.js +95 -95
- package/src/models/visitor.js +67 -67
- package/src/models/waitlist.js +45 -45
- package/src/models/wallet.js +44 -44
- package/src/models/wallet_transactions.js +50 -50
- package/src/models/water_invoice.js +351 -351
- package/src/models/water_meter_Command_Queue.js +33 -33
- package/src/models/water_meter_account.js +82 -82
- package/src/models/water_meter_billing.js +58 -58
- package/src/models/water_meter_communication.js +17 -17
- package/src/models/water_meter_communication_logs.js +39 -39
- package/src/models/water_meter_concentrator.js +70 -70
- package/src/models/water_meter_daily_history.js +32 -32
- package/src/models/water_meter_high_risk.js +36 -36
- package/src/models/water_meter_iot_cards.js +34 -34
- package/src/models/water_meter_manufacturer.js +35 -35
- package/src/models/water_meter_monthly_history.js +36 -36
- package/src/models/water_meter_negative_amounts.js +44 -44
- package/src/models/water_meter_settings.js +283 -276
- package/src/models/water_meter_single_day_history.js +34 -34
- package/src/models/water_meter_size.js +15 -15
- package/src/models/water_meters.js +133 -133
- package/src/models/water_meters_delivery.js +76 -76
- package/src/models/water_prepaid_credit.js +47 -47
- package/src/models/water_prepaid_debit.js +50 -50
- package/src/models/workorder.js +49 -49
|
@@ -1,556 +1,556 @@
|
|
|
1
|
-
const mongoose = require('mongoose');
|
|
2
|
-
const Schema = mongoose.Schema;
|
|
3
|
-
|
|
4
|
-
const PropertyManagerContractSchema = new Schema(
|
|
5
|
-
{
|
|
6
|
-
contractName: {
|
|
7
|
-
type: String,
|
|
8
|
-
required: [true, 'Contract name is required']
|
|
9
|
-
},
|
|
10
|
-
propertyManager: {
|
|
11
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
12
|
-
ref: 'User', // References global User collection
|
|
13
|
-
required: [true, 'Property manager is required']
|
|
14
|
-
},
|
|
15
|
-
units: [{
|
|
16
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
17
|
-
ref: 'Unit',
|
|
18
|
-
required: [true, 'At least one unit is required']
|
|
19
|
-
}],
|
|
20
|
-
customerId: {
|
|
21
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
22
|
-
ref: 'Customer', // Can reference global or facility-specific Customer
|
|
23
|
-
required: [true, 'Customer ID is required']
|
|
24
|
-
},
|
|
25
|
-
facilityId: {
|
|
26
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
27
|
-
ref: 'Facility',
|
|
28
|
-
required: [true, 'Facility ID is required']
|
|
29
|
-
},
|
|
30
|
-
// Lease term details
|
|
31
|
-
startDate: {
|
|
32
|
-
type: Date,
|
|
33
|
-
required: function() {
|
|
34
|
-
return this.status === 'Active';
|
|
35
|
-
}
|
|
36
|
-
},
|
|
37
|
-
endDate: {
|
|
38
|
-
type: Date,
|
|
39
|
-
required: function() {
|
|
40
|
-
return this.status === 'Active';
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
|
-
// Payment details - aligned with LeaseAgreement
|
|
44
|
-
paymentDueDate: {
|
|
45
|
-
type: Number,
|
|
46
|
-
required: function() {
|
|
47
|
-
return this.status === 'Active';
|
|
48
|
-
},
|
|
49
|
-
min: [1, 'Payment due date must be between 1 and 31'],
|
|
50
|
-
max: [31, 'Payment due date must be between 1 and 31']
|
|
51
|
-
},
|
|
52
|
-
frequency: {
|
|
53
|
-
type: String,
|
|
54
|
-
enum: ['Monthly', 'Quarterly', 'Annually'],
|
|
55
|
-
default: 'Monthly',
|
|
56
|
-
required: function() {
|
|
57
|
-
return this.status === 'Active';
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
nextInvoiceDate: {
|
|
61
|
-
type: Date
|
|
62
|
-
},
|
|
63
|
-
lastInvoiceDate: {
|
|
64
|
-
type: Date
|
|
65
|
-
},
|
|
66
|
-
autoSend: {
|
|
67
|
-
type: Boolean,
|
|
68
|
-
default: false
|
|
69
|
-
},
|
|
70
|
-
balanceBroughtForward: {
|
|
71
|
-
type: Number,
|
|
72
|
-
default: 0
|
|
73
|
-
},
|
|
74
|
-
// Management fee
|
|
75
|
-
managementFee: {
|
|
76
|
-
type: {
|
|
77
|
-
type: String,
|
|
78
|
-
enum: ['percentage', 'amount'],
|
|
79
|
-
required: [true, 'Management fee type is required']
|
|
80
|
-
},
|
|
81
|
-
value: {
|
|
82
|
-
type: Number,
|
|
83
|
-
required: [true, 'Management fee value is required'],
|
|
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
|
-
}
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
// GL Account configurations - updated to match sample data structure
|
|
98
|
-
invoiceDoubleEntryAccount: {
|
|
99
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
100
|
-
ref: 'GLAccountDoubleEntries'
|
|
101
|
-
},
|
|
102
|
-
paymentDoubleEntryAccount: {
|
|
103
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
104
|
-
ref: 'GLAccountDoubleEntries'
|
|
105
|
-
},
|
|
106
|
-
glAccounts: {
|
|
107
|
-
invoice: {
|
|
108
|
-
debit: {
|
|
109
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
110
|
-
ref: 'GLAccount'
|
|
111
|
-
},
|
|
112
|
-
credit: {
|
|
113
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
114
|
-
ref: 'GLAccount'
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
payment: {
|
|
118
|
-
debit: {
|
|
119
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
120
|
-
ref: 'GLAccount'
|
|
121
|
-
},
|
|
122
|
-
credit: {
|
|
123
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
124
|
-
ref: 'GLAccount'
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
status: {
|
|
129
|
-
type: String,
|
|
130
|
-
enum: ['Active', 'Inactive', 'Completed', 'Suspended', 'Terminated'],
|
|
131
|
-
default: 'Inactive',
|
|
132
|
-
required: [true, 'Status is required']
|
|
133
|
-
},
|
|
134
|
-
createdBy: {
|
|
135
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
136
|
-
ref: 'User' // References global User collection
|
|
137
|
-
},
|
|
138
|
-
// Additional fields for contract lifecycle management
|
|
139
|
-
suspensionReason: {
|
|
140
|
-
type: String
|
|
141
|
-
},
|
|
142
|
-
terminationReason: {
|
|
143
|
-
type: String
|
|
144
|
-
},
|
|
145
|
-
terminationDate: {
|
|
146
|
-
type: Date
|
|
147
|
-
},
|
|
148
|
-
completionDate: {
|
|
149
|
-
type: Date
|
|
150
|
-
}
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
timestamps: true,
|
|
154
|
-
// Ensure virtual fields are included in JSON output
|
|
155
|
-
toJSON: { virtuals: true },
|
|
156
|
-
toObject: { virtuals: true }
|
|
157
|
-
}
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
// Enhanced pre-save validation
|
|
161
|
-
PropertyManagerContractSchema.pre('save', function (next) {
|
|
162
|
-
// Validate date range
|
|
163
|
-
if (this.startDate && this.endDate && this.startDate >= this.endDate) {
|
|
164
|
-
next(new Error('End date must be after start date'));
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Validate units array
|
|
169
|
-
if (!this.units || this.units.length === 0) {
|
|
170
|
-
next(new Error('At least one unit must be specified'));
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Validate management fee
|
|
175
|
-
if (!this.managementFee || !this.managementFee.type || this.managementFee.value === undefined) {
|
|
176
|
-
next(new Error('Management fee type and value are required'));
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Validate management fee percentage
|
|
181
|
-
if (this.managementFee.type === 'percentage' && this.managementFee.value > 100) {
|
|
182
|
-
next(new Error('Management fee percentage cannot exceed 100%'));
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Validate management fee value is non-negative
|
|
187
|
-
if (this.managementFee.value < 0) {
|
|
188
|
-
next(new Error('Management fee value cannot be negative'));
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Validate payment due date range
|
|
193
|
-
if (this.paymentDueDate && (this.paymentDueDate < 1 || this.paymentDueDate > 31)) {
|
|
194
|
-
next(new Error('Payment due date must be between 1 and 31'));
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Validate required fields for Active status
|
|
199
|
-
if (this.status === 'Active') {
|
|
200
|
-
if (!this.startDate) {
|
|
201
|
-
next(new Error('Start date is required for Active contracts'));
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
if (!this.endDate) {
|
|
205
|
-
next(new Error('End date is required for Active contracts'));
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
if (!this.paymentDueDate) {
|
|
209
|
-
next(new Error('Payment due date is required for Active contracts'));
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
if (!this.frequency) {
|
|
213
|
-
next(new Error('Payment frequency is required for Active contracts'));
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
next();
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
// Method to sync with lease data
|
|
222
|
-
PropertyManagerContractSchema.methods.syncWithLeaseData = async function(leaseData) {
|
|
223
|
-
if (!leaseData) return;
|
|
224
|
-
|
|
225
|
-
// Update contract fields from lease data
|
|
226
|
-
this.startDate = leaseData.leaseTerms?.startDate || this.startDate;
|
|
227
|
-
this.endDate = leaseData.leaseTerms?.endDate || this.endDate;
|
|
228
|
-
this.paymentDueDate = leaseData.financialTerms?.paymentDueDate || this.paymentDueDate;
|
|
229
|
-
this.balanceBroughtForward = leaseData.financialTerms?.balanceBroughtForward || this.balanceBroughtForward;
|
|
230
|
-
this.frequency = leaseData.billingCycle?.frequency || this.frequency;
|
|
231
|
-
this.autoSend = leaseData.billingCycle?.autoSend !== undefined ? leaseData.billingCycle.autoSend : this.autoSend;
|
|
232
|
-
this.nextInvoiceDate = leaseData.billingCycle?.nextInvoiceDate || this.nextInvoiceDate;
|
|
233
|
-
this.lastInvoiceDate = leaseData.billingCycle?.lastInvoiceDate || this.lastInvoiceDate;
|
|
234
|
-
|
|
235
|
-
// Automatically activate contract if all required fields are present
|
|
236
|
-
if (this.startDate && this.endDate && this.paymentDueDate && this.frequency && this.status === 'Inactive') {
|
|
237
|
-
this.status = 'Active';
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return this.save();
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
// Method to check if contract can be activated
|
|
244
|
-
PropertyManagerContractSchema.methods.canBeActivated = function() {
|
|
245
|
-
return !!(this.startDate && this.endDate && this.paymentDueDate && this.frequency);
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
// Method to get missing required fields
|
|
249
|
-
PropertyManagerContractSchema.methods.getMissingRequiredFields = function() {
|
|
250
|
-
const requiredFields = ['startDate', 'endDate', 'paymentDueDate', 'frequency'];
|
|
251
|
-
return requiredFields.filter(field => !this[field]);
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
// Virtual for contract completion status
|
|
255
|
-
PropertyManagerContractSchema.virtual('isComplete').get(function() {
|
|
256
|
-
return this.canBeActivated();
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
// Virtual for contract duration in days
|
|
260
|
-
PropertyManagerContractSchema.virtual('durationInDays').get(function() {
|
|
261
|
-
if (!this.startDate || !this.endDate) return null;
|
|
262
|
-
const diffTime = Math.abs(this.endDate - this.startDate);
|
|
263
|
-
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
// Virtual for contract remaining days
|
|
267
|
-
PropertyManagerContractSchema.virtual('remainingDays').get(function() {
|
|
268
|
-
if (!this.endDate) return null;
|
|
269
|
-
const now = new Date();
|
|
270
|
-
if (now > this.endDate) return 0;
|
|
271
|
-
const diffTime = Math.abs(this.endDate - now);
|
|
272
|
-
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
// Virtual for management fee display
|
|
276
|
-
PropertyManagerContractSchema.virtual('managementFeeDisplay').get(function() {
|
|
277
|
-
if (!this.managementFee) return null;
|
|
278
|
-
|
|
279
|
-
if (this.managementFee.type === 'percentage') {
|
|
280
|
-
return `${this.managementFee.value}%`;
|
|
281
|
-
} else {
|
|
282
|
-
return `$${this.managementFee.value.toLocaleString()}`;
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
// Indexes for better query performance
|
|
287
|
-
PropertyManagerContractSchema.index({ customerId: 1, status: 1 });
|
|
288
|
-
PropertyManagerContractSchema.index({ facilityId: 1, status: 1 });
|
|
289
|
-
PropertyManagerContractSchema.index({ propertyManager: 1, facilityId: 1 });
|
|
290
|
-
PropertyManagerContractSchema.index({ units: 1, status: 1 });
|
|
291
|
-
PropertyManagerContractSchema.index({ nextInvoiceDate: 1, status: 1 });
|
|
292
|
-
PropertyManagerContractSchema.index({ endDate: 1, status: 1 }); // For contract expiration queries
|
|
293
|
-
PropertyManagerContractSchema.index({ 'managementFee.type': 1, 'managementFee.value': 1 }); // For fee analysis
|
|
294
|
-
PropertyManagerContractSchema.index({ createdAt: -1 }); // For recent contracts
|
|
295
|
-
PropertyManagerContractSchema.index({ startDate: 1, endDate: 1 }); // For date range queries
|
|
296
|
-
|
|
297
|
-
// Compound indexes for common query patterns
|
|
298
|
-
PropertyManagerContractSchema.index({
|
|
299
|
-
facilityId: 1,
|
|
300
|
-
propertyManager: 1,
|
|
301
|
-
status: 1
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
PropertyManagerContractSchema.index({
|
|
305
|
-
facilityId: 1,
|
|
306
|
-
customerId: 1,
|
|
307
|
-
status: 1
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
// Text index for searching contract names
|
|
311
|
-
PropertyManagerContractSchema.index({
|
|
312
|
-
contractName: 'text'
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
// Static methods for common queries
|
|
316
|
-
PropertyManagerContractSchema.statics.findActiveContractsByFacility = function(facilityId) {
|
|
317
|
-
return this.find({
|
|
318
|
-
facilityId: facilityId,
|
|
319
|
-
status: 'Active'
|
|
320
|
-
}).populate('propertyManager', 'fullName email')
|
|
321
|
-
.populate('units', 'name unitType')
|
|
322
|
-
.populate('customerId', 'firstName lastName email');
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
PropertyManagerContractSchema.statics.findContractsByPropertyManager = function(propertyManagerId, facilityId) {
|
|
326
|
-
return this.find({
|
|
327
|
-
propertyManager: propertyManagerId,
|
|
328
|
-
facilityId: facilityId
|
|
329
|
-
}).populate('units', 'name unitType')
|
|
330
|
-
.populate('customerId', 'firstName lastName email')
|
|
331
|
-
.sort({ createdAt: -1 });
|
|
332
|
-
};
|
|
333
|
-
|
|
334
|
-
PropertyManagerContractSchema.statics.findExpiringContracts = function(facilityId, daysAhead = 30) {
|
|
335
|
-
const futureDate = new Date();
|
|
336
|
-
futureDate.setDate(futureDate.getDate() + daysAhead);
|
|
337
|
-
|
|
338
|
-
return this.find({
|
|
339
|
-
facilityId: facilityId,
|
|
340
|
-
status: 'Active',
|
|
341
|
-
endDate: {
|
|
342
|
-
$gte: new Date(),
|
|
343
|
-
$lte: futureDate
|
|
344
|
-
}
|
|
345
|
-
}).populate('propertyManager', 'fullName email')
|
|
346
|
-
.populate('units', 'name unitType')
|
|
347
|
-
.populate('customerId', 'firstName lastName email')
|
|
348
|
-
.sort({ endDate: 1 });
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
PropertyManagerContractSchema.statics.findContractsRequiringInvoicing = function(facilityId) {
|
|
352
|
-
const today = new Date();
|
|
353
|
-
today.setHours(0, 0, 0, 0);
|
|
354
|
-
|
|
355
|
-
return this.find({
|
|
356
|
-
facilityId: facilityId,
|
|
357
|
-
status: 'Active',
|
|
358
|
-
nextInvoiceDate: {
|
|
359
|
-
$lte: today
|
|
360
|
-
}
|
|
361
|
-
}).populate('propertyManager', 'fullName email')
|
|
362
|
-
.populate('units', 'name unitType')
|
|
363
|
-
.populate('customerId', 'firstName lastName email');
|
|
364
|
-
};
|
|
365
|
-
|
|
366
|
-
PropertyManagerContractSchema.statics.findContractsByFeeType = function(facilityId, feeType) {
|
|
367
|
-
return this.find({
|
|
368
|
-
facilityId: facilityId,
|
|
369
|
-
'managementFee.type': feeType
|
|
370
|
-
}).populate('propertyManager', 'fullName email')
|
|
371
|
-
.populate('units', 'name unitType')
|
|
372
|
-
.populate('customerId', 'firstName lastName email')
|
|
373
|
-
.sort({ 'managementFee.value': -1 });
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
PropertyManagerContractSchema.statics.getContractStatistics = function(facilityId) {
|
|
377
|
-
return this.aggregate([
|
|
378
|
-
{
|
|
379
|
-
$match: { facilityId: mongoose.Types.ObjectId(facilityId) }
|
|
380
|
-
},
|
|
381
|
-
{
|
|
382
|
-
$group: {
|
|
383
|
-
_id: '$status',
|
|
384
|
-
count: { $sum: 1 },
|
|
385
|
-
totalFeeValue: { $sum: '$managementFee.value' },
|
|
386
|
-
avgFeeValue: { $avg: '$managementFee.value' }
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
]);
|
|
390
|
-
};
|
|
391
|
-
|
|
392
|
-
// Instance methods for contract management
|
|
393
|
-
PropertyManagerContractSchema.methods.calculateNextInvoiceDate = function() {
|
|
394
|
-
if (!this.lastInvoiceDate && !this.startDate) return null;
|
|
395
|
-
|
|
396
|
-
const baseDate = this.lastInvoiceDate || this.startDate;
|
|
397
|
-
const nextDate = new Date(baseDate);
|
|
398
|
-
|
|
399
|
-
switch (this.frequency) {
|
|
400
|
-
case 'Monthly':
|
|
401
|
-
nextDate.setMonth(nextDate.getMonth() + 1);
|
|
402
|
-
break;
|
|
403
|
-
case 'Quarterly':
|
|
404
|
-
nextDate.setMonth(nextDate.getMonth() + 3);
|
|
405
|
-
break;
|
|
406
|
-
case 'Annually':
|
|
407
|
-
nextDate.setFullYear(nextDate.getFullYear() + 1);
|
|
408
|
-
break;
|
|
409
|
-
default:
|
|
410
|
-
nextDate.setMonth(nextDate.getMonth() + 1); // Default to monthly
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Set to the payment due date of the month
|
|
414
|
-
if (this.paymentDueDate) {
|
|
415
|
-
const lastDayOfMonth = new Date(nextDate.getFullYear(), nextDate.getMonth() + 1, 0).getDate();
|
|
416
|
-
const dueDate = Math.min(this.paymentDueDate, lastDayOfMonth);
|
|
417
|
-
nextDate.setDate(dueDate);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
return nextDate;
|
|
421
|
-
};
|
|
422
|
-
|
|
423
|
-
PropertyManagerContractSchema.methods.updateInvoiceDates = async function(invoiceDate = new Date()) {
|
|
424
|
-
this.lastInvoiceDate = invoiceDate;
|
|
425
|
-
this.nextInvoiceDate = this.calculateNextInvoiceDate();
|
|
426
|
-
return this.save();
|
|
427
|
-
};
|
|
428
|
-
|
|
429
|
-
PropertyManagerContractSchema.methods.activateContract = async function() {
|
|
430
|
-
if (!this.canBeActivated()) {
|
|
431
|
-
throw new Error(`Cannot activate contract: missing required fields - ${this.getMissingRequiredFields().join(', ')}`);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
this.status = 'Active';
|
|
435
|
-
|
|
436
|
-
// Set next invoice date if not already set
|
|
437
|
-
if (!this.nextInvoiceDate) {
|
|
438
|
-
this.nextInvoiceDate = this.calculateNextInvoiceDate();
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
return this.save();
|
|
442
|
-
};
|
|
443
|
-
|
|
444
|
-
PropertyManagerContractSchema.methods.suspendContract = async function(reason) {
|
|
445
|
-
this.status = 'Suspended';
|
|
446
|
-
this.suspensionReason = reason;
|
|
447
|
-
return this.save();
|
|
448
|
-
};
|
|
449
|
-
|
|
450
|
-
PropertyManagerContractSchema.methods.terminateContract = async function(reason, terminationDate = new Date()) {
|
|
451
|
-
this.status = 'Terminated';
|
|
452
|
-
this.terminationReason = reason;
|
|
453
|
-
this.terminationDate = terminationDate;
|
|
454
|
-
return this.save();
|
|
455
|
-
};
|
|
456
|
-
|
|
457
|
-
PropertyManagerContractSchema.methods.completeContract = async function() {
|
|
458
|
-
this.status = 'Completed';
|
|
459
|
-
this.completionDate = new Date();
|
|
460
|
-
return this.save();
|
|
461
|
-
};
|
|
462
|
-
|
|
463
|
-
// Method to calculate management fee for a given amount
|
|
464
|
-
PropertyManagerContractSchema.methods.calculateManagementFee = function(baseAmount) {
|
|
465
|
-
if (!this.managementFee) return 0;
|
|
466
|
-
|
|
467
|
-
if (this.managementFee.type === 'percentage') {
|
|
468
|
-
return (baseAmount * this.managementFee.value) / 100;
|
|
469
|
-
} else {
|
|
470
|
-
return this.managementFee.value;
|
|
471
|
-
}
|
|
472
|
-
};
|
|
473
|
-
|
|
474
|
-
// Method to get contract summary
|
|
475
|
-
PropertyManagerContractSchema.methods.getContractSummary = function() {
|
|
476
|
-
return {
|
|
477
|
-
id: this._id,
|
|
478
|
-
contractName: this.contractName,
|
|
479
|
-
status: this.status,
|
|
480
|
-
duration: this.durationInDays,
|
|
481
|
-
remainingDays: this.remainingDays,
|
|
482
|
-
managementFee: this.managementFeeDisplay,
|
|
483
|
-
frequency: this.frequency,
|
|
484
|
-
nextInvoiceDate: this.nextInvoiceDate,
|
|
485
|
-
isComplete: this.isComplete
|
|
486
|
-
};
|
|
487
|
-
};
|
|
488
|
-
|
|
489
|
-
// Pre-findOneAndUpdate middleware to maintain data integrity
|
|
490
|
-
PropertyManagerContractSchema.pre(['findOneAndUpdate', 'updateOne', 'updateMany'], function() {
|
|
491
|
-
const update = this.getUpdate();
|
|
492
|
-
|
|
493
|
-
// If status is being changed to Active, ensure required fields are present
|
|
494
|
-
if (update.$set && update.$set.status === 'Active') {
|
|
495
|
-
const requiredFields = ['startDate', 'endDate', 'paymentDueDate', 'frequency'];
|
|
496
|
-
// Note: We can't validate here because we don't have access to the full document
|
|
497
|
-
// Validation should be done in the application logic before calling update
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Update the updatedAt field
|
|
501
|
-
if (update.$set) {
|
|
502
|
-
update.$set.updatedAt = new Date();
|
|
503
|
-
} else {
|
|
504
|
-
this.set({ updatedAt: new Date() });
|
|
505
|
-
}
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
// Post-save middleware for logging and notifications
|
|
509
|
-
PropertyManagerContractSchema.post('save', function(doc) {
|
|
510
|
-
// Log contract creation/updates
|
|
511
|
-
console.log(`Property Manager Contract ${doc.isNew ? 'created' : 'updated'}: ${doc.contractName} (${doc._id})`);
|
|
512
|
-
|
|
513
|
-
// Here you could add webhook calls, email notifications, etc.
|
|
514
|
-
// Example: notify property manager of contract status changes
|
|
515
|
-
if (this.isModified('status')) {
|
|
516
|
-
console.log(`Contract status changed to: ${doc.status}`);
|
|
517
|
-
// Add notification logic here
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// Log invoice date updates
|
|
521
|
-
if (this.isModified('nextInvoiceDate') || this.isModified('lastInvoiceDate')) {
|
|
522
|
-
console.log(`Invoice dates updated for contract: ${doc.contractName}`);
|
|
523
|
-
}
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
// Error handling for validation errors
|
|
527
|
-
PropertyManagerContractSchema.post('save', function(error, doc, next) {
|
|
528
|
-
if (error.name === 'ValidationError') {
|
|
529
|
-
const messages = Object.values(error.errors).map(err => err.message);
|
|
530
|
-
next(new Error(`Contract validation failed: ${messages.join(', ')}`));
|
|
531
|
-
} else {
|
|
532
|
-
next(error);
|
|
533
|
-
}
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
// JSON transformation to remove sensitive data when returning to client
|
|
537
|
-
PropertyManagerContractSchema.methods.toJSON = function() {
|
|
538
|
-
const contract = this.toObject();
|
|
539
|
-
|
|
540
|
-
// Add computed fields
|
|
541
|
-
contract.isComplete = this.isComplete;
|
|
542
|
-
contract.canBeActivated = this.canBeActivated();
|
|
543
|
-
contract.missingRequiredFields = this.getMissingRequiredFields();
|
|
544
|
-
contract.durationInDays = this.durationInDays;
|
|
545
|
-
contract.remainingDays = this.remainingDays;
|
|
546
|
-
contract.managementFeeDisplay = this.managementFeeDisplay;
|
|
547
|
-
contract.contractSummary = this.getContractSummary();
|
|
548
|
-
|
|
549
|
-
return contract;
|
|
550
|
-
};
|
|
551
|
-
|
|
552
|
-
// Export the schema
|
|
553
|
-
module.exports = {
|
|
554
|
-
schema: PropertyManagerContractSchema,
|
|
555
|
-
name: 'PropertyManagerContract'
|
|
556
|
-
};
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
const Schema = mongoose.Schema;
|
|
3
|
+
|
|
4
|
+
const PropertyManagerContractSchema = new Schema(
|
|
5
|
+
{
|
|
6
|
+
contractName: {
|
|
7
|
+
type: String,
|
|
8
|
+
required: [true, 'Contract name is required']
|
|
9
|
+
},
|
|
10
|
+
propertyManager: {
|
|
11
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
12
|
+
ref: 'User', // References global User collection
|
|
13
|
+
required: [true, 'Property manager is required']
|
|
14
|
+
},
|
|
15
|
+
units: [{
|
|
16
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
17
|
+
ref: 'Unit',
|
|
18
|
+
required: [true, 'At least one unit is required']
|
|
19
|
+
}],
|
|
20
|
+
customerId: {
|
|
21
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
22
|
+
ref: 'Customer', // Can reference global or facility-specific Customer
|
|
23
|
+
required: [true, 'Customer ID is required']
|
|
24
|
+
},
|
|
25
|
+
facilityId: {
|
|
26
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
27
|
+
ref: 'Facility',
|
|
28
|
+
required: [true, 'Facility ID is required']
|
|
29
|
+
},
|
|
30
|
+
// Lease term details
|
|
31
|
+
startDate: {
|
|
32
|
+
type: Date,
|
|
33
|
+
required: function() {
|
|
34
|
+
return this.status === 'Active';
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
endDate: {
|
|
38
|
+
type: Date,
|
|
39
|
+
required: function() {
|
|
40
|
+
return this.status === 'Active';
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
// Payment details - aligned with LeaseAgreement
|
|
44
|
+
paymentDueDate: {
|
|
45
|
+
type: Number,
|
|
46
|
+
required: function() {
|
|
47
|
+
return this.status === 'Active';
|
|
48
|
+
},
|
|
49
|
+
min: [1, 'Payment due date must be between 1 and 31'],
|
|
50
|
+
max: [31, 'Payment due date must be between 1 and 31']
|
|
51
|
+
},
|
|
52
|
+
frequency: {
|
|
53
|
+
type: String,
|
|
54
|
+
enum: ['Monthly', 'Quarterly', 'Annually'],
|
|
55
|
+
default: 'Monthly',
|
|
56
|
+
required: function() {
|
|
57
|
+
return this.status === 'Active';
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
nextInvoiceDate: {
|
|
61
|
+
type: Date
|
|
62
|
+
},
|
|
63
|
+
lastInvoiceDate: {
|
|
64
|
+
type: Date
|
|
65
|
+
},
|
|
66
|
+
autoSend: {
|
|
67
|
+
type: Boolean,
|
|
68
|
+
default: false
|
|
69
|
+
},
|
|
70
|
+
balanceBroughtForward: {
|
|
71
|
+
type: Number,
|
|
72
|
+
default: 0
|
|
73
|
+
},
|
|
74
|
+
// Management fee
|
|
75
|
+
managementFee: {
|
|
76
|
+
type: {
|
|
77
|
+
type: String,
|
|
78
|
+
enum: ['percentage', 'amount'],
|
|
79
|
+
required: [true, 'Management fee type is required']
|
|
80
|
+
},
|
|
81
|
+
value: {
|
|
82
|
+
type: Number,
|
|
83
|
+
required: [true, 'Management fee value is required'],
|
|
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
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
// GL Account configurations - updated to match sample data structure
|
|
98
|
+
invoiceDoubleEntryAccount: {
|
|
99
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
100
|
+
ref: 'GLAccountDoubleEntries'
|
|
101
|
+
},
|
|
102
|
+
paymentDoubleEntryAccount: {
|
|
103
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
104
|
+
ref: 'GLAccountDoubleEntries'
|
|
105
|
+
},
|
|
106
|
+
glAccounts: {
|
|
107
|
+
invoice: {
|
|
108
|
+
debit: {
|
|
109
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
110
|
+
ref: 'GLAccount'
|
|
111
|
+
},
|
|
112
|
+
credit: {
|
|
113
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
114
|
+
ref: 'GLAccount'
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
payment: {
|
|
118
|
+
debit: {
|
|
119
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
120
|
+
ref: 'GLAccount'
|
|
121
|
+
},
|
|
122
|
+
credit: {
|
|
123
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
124
|
+
ref: 'GLAccount'
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
status: {
|
|
129
|
+
type: String,
|
|
130
|
+
enum: ['Active', 'Inactive', 'Completed', 'Suspended', 'Terminated'],
|
|
131
|
+
default: 'Inactive',
|
|
132
|
+
required: [true, 'Status is required']
|
|
133
|
+
},
|
|
134
|
+
createdBy: {
|
|
135
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
136
|
+
ref: 'User' // References global User collection
|
|
137
|
+
},
|
|
138
|
+
// Additional fields for contract lifecycle management
|
|
139
|
+
suspensionReason: {
|
|
140
|
+
type: String
|
|
141
|
+
},
|
|
142
|
+
terminationReason: {
|
|
143
|
+
type: String
|
|
144
|
+
},
|
|
145
|
+
terminationDate: {
|
|
146
|
+
type: Date
|
|
147
|
+
},
|
|
148
|
+
completionDate: {
|
|
149
|
+
type: Date
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
timestamps: true,
|
|
154
|
+
// Ensure virtual fields are included in JSON output
|
|
155
|
+
toJSON: { virtuals: true },
|
|
156
|
+
toObject: { virtuals: true }
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Enhanced pre-save validation
|
|
161
|
+
PropertyManagerContractSchema.pre('save', function (next) {
|
|
162
|
+
// Validate date range
|
|
163
|
+
if (this.startDate && this.endDate && this.startDate >= this.endDate) {
|
|
164
|
+
next(new Error('End date must be after start date'));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Validate units array
|
|
169
|
+
if (!this.units || this.units.length === 0) {
|
|
170
|
+
next(new Error('At least one unit must be specified'));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Validate management fee
|
|
175
|
+
if (!this.managementFee || !this.managementFee.type || this.managementFee.value === undefined) {
|
|
176
|
+
next(new Error('Management fee type and value are required'));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Validate management fee percentage
|
|
181
|
+
if (this.managementFee.type === 'percentage' && this.managementFee.value > 100) {
|
|
182
|
+
next(new Error('Management fee percentage cannot exceed 100%'));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Validate management fee value is non-negative
|
|
187
|
+
if (this.managementFee.value < 0) {
|
|
188
|
+
next(new Error('Management fee value cannot be negative'));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Validate payment due date range
|
|
193
|
+
if (this.paymentDueDate && (this.paymentDueDate < 1 || this.paymentDueDate > 31)) {
|
|
194
|
+
next(new Error('Payment due date must be between 1 and 31'));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Validate required fields for Active status
|
|
199
|
+
if (this.status === 'Active') {
|
|
200
|
+
if (!this.startDate) {
|
|
201
|
+
next(new Error('Start date is required for Active contracts'));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (!this.endDate) {
|
|
205
|
+
next(new Error('End date is required for Active contracts'));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (!this.paymentDueDate) {
|
|
209
|
+
next(new Error('Payment due date is required for Active contracts'));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (!this.frequency) {
|
|
213
|
+
next(new Error('Payment frequency is required for Active contracts'));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
next();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Method to sync with lease data
|
|
222
|
+
PropertyManagerContractSchema.methods.syncWithLeaseData = async function(leaseData) {
|
|
223
|
+
if (!leaseData) return;
|
|
224
|
+
|
|
225
|
+
// Update contract fields from lease data
|
|
226
|
+
this.startDate = leaseData.leaseTerms?.startDate || this.startDate;
|
|
227
|
+
this.endDate = leaseData.leaseTerms?.endDate || this.endDate;
|
|
228
|
+
this.paymentDueDate = leaseData.financialTerms?.paymentDueDate || this.paymentDueDate;
|
|
229
|
+
this.balanceBroughtForward = leaseData.financialTerms?.balanceBroughtForward || this.balanceBroughtForward;
|
|
230
|
+
this.frequency = leaseData.billingCycle?.frequency || this.frequency;
|
|
231
|
+
this.autoSend = leaseData.billingCycle?.autoSend !== undefined ? leaseData.billingCycle.autoSend : this.autoSend;
|
|
232
|
+
this.nextInvoiceDate = leaseData.billingCycle?.nextInvoiceDate || this.nextInvoiceDate;
|
|
233
|
+
this.lastInvoiceDate = leaseData.billingCycle?.lastInvoiceDate || this.lastInvoiceDate;
|
|
234
|
+
|
|
235
|
+
// Automatically activate contract if all required fields are present
|
|
236
|
+
if (this.startDate && this.endDate && this.paymentDueDate && this.frequency && this.status === 'Inactive') {
|
|
237
|
+
this.status = 'Active';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return this.save();
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// Method to check if contract can be activated
|
|
244
|
+
PropertyManagerContractSchema.methods.canBeActivated = function() {
|
|
245
|
+
return !!(this.startDate && this.endDate && this.paymentDueDate && this.frequency);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Method to get missing required fields
|
|
249
|
+
PropertyManagerContractSchema.methods.getMissingRequiredFields = function() {
|
|
250
|
+
const requiredFields = ['startDate', 'endDate', 'paymentDueDate', 'frequency'];
|
|
251
|
+
return requiredFields.filter(field => !this[field]);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// Virtual for contract completion status
|
|
255
|
+
PropertyManagerContractSchema.virtual('isComplete').get(function() {
|
|
256
|
+
return this.canBeActivated();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Virtual for contract duration in days
|
|
260
|
+
PropertyManagerContractSchema.virtual('durationInDays').get(function() {
|
|
261
|
+
if (!this.startDate || !this.endDate) return null;
|
|
262
|
+
const diffTime = Math.abs(this.endDate - this.startDate);
|
|
263
|
+
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Virtual for contract remaining days
|
|
267
|
+
PropertyManagerContractSchema.virtual('remainingDays').get(function() {
|
|
268
|
+
if (!this.endDate) return null;
|
|
269
|
+
const now = new Date();
|
|
270
|
+
if (now > this.endDate) return 0;
|
|
271
|
+
const diffTime = Math.abs(this.endDate - now);
|
|
272
|
+
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Virtual for management fee display
|
|
276
|
+
PropertyManagerContractSchema.virtual('managementFeeDisplay').get(function() {
|
|
277
|
+
if (!this.managementFee) return null;
|
|
278
|
+
|
|
279
|
+
if (this.managementFee.type === 'percentage') {
|
|
280
|
+
return `${this.managementFee.value}%`;
|
|
281
|
+
} else {
|
|
282
|
+
return `$${this.managementFee.value.toLocaleString()}`;
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Indexes for better query performance
|
|
287
|
+
PropertyManagerContractSchema.index({ customerId: 1, status: 1 });
|
|
288
|
+
PropertyManagerContractSchema.index({ facilityId: 1, status: 1 });
|
|
289
|
+
PropertyManagerContractSchema.index({ propertyManager: 1, facilityId: 1 });
|
|
290
|
+
PropertyManagerContractSchema.index({ units: 1, status: 1 });
|
|
291
|
+
PropertyManagerContractSchema.index({ nextInvoiceDate: 1, status: 1 });
|
|
292
|
+
PropertyManagerContractSchema.index({ endDate: 1, status: 1 }); // For contract expiration queries
|
|
293
|
+
PropertyManagerContractSchema.index({ 'managementFee.type': 1, 'managementFee.value': 1 }); // For fee analysis
|
|
294
|
+
PropertyManagerContractSchema.index({ createdAt: -1 }); // For recent contracts
|
|
295
|
+
PropertyManagerContractSchema.index({ startDate: 1, endDate: 1 }); // For date range queries
|
|
296
|
+
|
|
297
|
+
// Compound indexes for common query patterns
|
|
298
|
+
PropertyManagerContractSchema.index({
|
|
299
|
+
facilityId: 1,
|
|
300
|
+
propertyManager: 1,
|
|
301
|
+
status: 1
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
PropertyManagerContractSchema.index({
|
|
305
|
+
facilityId: 1,
|
|
306
|
+
customerId: 1,
|
|
307
|
+
status: 1
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Text index for searching contract names
|
|
311
|
+
PropertyManagerContractSchema.index({
|
|
312
|
+
contractName: 'text'
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Static methods for common queries
|
|
316
|
+
PropertyManagerContractSchema.statics.findActiveContractsByFacility = function(facilityId) {
|
|
317
|
+
return this.find({
|
|
318
|
+
facilityId: facilityId,
|
|
319
|
+
status: 'Active'
|
|
320
|
+
}).populate('propertyManager', 'fullName email')
|
|
321
|
+
.populate('units', 'name unitType')
|
|
322
|
+
.populate('customerId', 'firstName lastName email');
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
PropertyManagerContractSchema.statics.findContractsByPropertyManager = function(propertyManagerId, facilityId) {
|
|
326
|
+
return this.find({
|
|
327
|
+
propertyManager: propertyManagerId,
|
|
328
|
+
facilityId: facilityId
|
|
329
|
+
}).populate('units', 'name unitType')
|
|
330
|
+
.populate('customerId', 'firstName lastName email')
|
|
331
|
+
.sort({ createdAt: -1 });
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
PropertyManagerContractSchema.statics.findExpiringContracts = function(facilityId, daysAhead = 30) {
|
|
335
|
+
const futureDate = new Date();
|
|
336
|
+
futureDate.setDate(futureDate.getDate() + daysAhead);
|
|
337
|
+
|
|
338
|
+
return this.find({
|
|
339
|
+
facilityId: facilityId,
|
|
340
|
+
status: 'Active',
|
|
341
|
+
endDate: {
|
|
342
|
+
$gte: new Date(),
|
|
343
|
+
$lte: futureDate
|
|
344
|
+
}
|
|
345
|
+
}).populate('propertyManager', 'fullName email')
|
|
346
|
+
.populate('units', 'name unitType')
|
|
347
|
+
.populate('customerId', 'firstName lastName email')
|
|
348
|
+
.sort({ endDate: 1 });
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
PropertyManagerContractSchema.statics.findContractsRequiringInvoicing = function(facilityId) {
|
|
352
|
+
const today = new Date();
|
|
353
|
+
today.setHours(0, 0, 0, 0);
|
|
354
|
+
|
|
355
|
+
return this.find({
|
|
356
|
+
facilityId: facilityId,
|
|
357
|
+
status: 'Active',
|
|
358
|
+
nextInvoiceDate: {
|
|
359
|
+
$lte: today
|
|
360
|
+
}
|
|
361
|
+
}).populate('propertyManager', 'fullName email')
|
|
362
|
+
.populate('units', 'name unitType')
|
|
363
|
+
.populate('customerId', 'firstName lastName email');
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
PropertyManagerContractSchema.statics.findContractsByFeeType = function(facilityId, feeType) {
|
|
367
|
+
return this.find({
|
|
368
|
+
facilityId: facilityId,
|
|
369
|
+
'managementFee.type': feeType
|
|
370
|
+
}).populate('propertyManager', 'fullName email')
|
|
371
|
+
.populate('units', 'name unitType')
|
|
372
|
+
.populate('customerId', 'firstName lastName email')
|
|
373
|
+
.sort({ 'managementFee.value': -1 });
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
PropertyManagerContractSchema.statics.getContractStatistics = function(facilityId) {
|
|
377
|
+
return this.aggregate([
|
|
378
|
+
{
|
|
379
|
+
$match: { facilityId: mongoose.Types.ObjectId(facilityId) }
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
$group: {
|
|
383
|
+
_id: '$status',
|
|
384
|
+
count: { $sum: 1 },
|
|
385
|
+
totalFeeValue: { $sum: '$managementFee.value' },
|
|
386
|
+
avgFeeValue: { $avg: '$managementFee.value' }
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
]);
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// Instance methods for contract management
|
|
393
|
+
PropertyManagerContractSchema.methods.calculateNextInvoiceDate = function() {
|
|
394
|
+
if (!this.lastInvoiceDate && !this.startDate) return null;
|
|
395
|
+
|
|
396
|
+
const baseDate = this.lastInvoiceDate || this.startDate;
|
|
397
|
+
const nextDate = new Date(baseDate);
|
|
398
|
+
|
|
399
|
+
switch (this.frequency) {
|
|
400
|
+
case 'Monthly':
|
|
401
|
+
nextDate.setMonth(nextDate.getMonth() + 1);
|
|
402
|
+
break;
|
|
403
|
+
case 'Quarterly':
|
|
404
|
+
nextDate.setMonth(nextDate.getMonth() + 3);
|
|
405
|
+
break;
|
|
406
|
+
case 'Annually':
|
|
407
|
+
nextDate.setFullYear(nextDate.getFullYear() + 1);
|
|
408
|
+
break;
|
|
409
|
+
default:
|
|
410
|
+
nextDate.setMonth(nextDate.getMonth() + 1); // Default to monthly
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Set to the payment due date of the month
|
|
414
|
+
if (this.paymentDueDate) {
|
|
415
|
+
const lastDayOfMonth = new Date(nextDate.getFullYear(), nextDate.getMonth() + 1, 0).getDate();
|
|
416
|
+
const dueDate = Math.min(this.paymentDueDate, lastDayOfMonth);
|
|
417
|
+
nextDate.setDate(dueDate);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return nextDate;
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
PropertyManagerContractSchema.methods.updateInvoiceDates = async function(invoiceDate = new Date()) {
|
|
424
|
+
this.lastInvoiceDate = invoiceDate;
|
|
425
|
+
this.nextInvoiceDate = this.calculateNextInvoiceDate();
|
|
426
|
+
return this.save();
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
PropertyManagerContractSchema.methods.activateContract = async function() {
|
|
430
|
+
if (!this.canBeActivated()) {
|
|
431
|
+
throw new Error(`Cannot activate contract: missing required fields - ${this.getMissingRequiredFields().join(', ')}`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
this.status = 'Active';
|
|
435
|
+
|
|
436
|
+
// Set next invoice date if not already set
|
|
437
|
+
if (!this.nextInvoiceDate) {
|
|
438
|
+
this.nextInvoiceDate = this.calculateNextInvoiceDate();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return this.save();
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
PropertyManagerContractSchema.methods.suspendContract = async function(reason) {
|
|
445
|
+
this.status = 'Suspended';
|
|
446
|
+
this.suspensionReason = reason;
|
|
447
|
+
return this.save();
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
PropertyManagerContractSchema.methods.terminateContract = async function(reason, terminationDate = new Date()) {
|
|
451
|
+
this.status = 'Terminated';
|
|
452
|
+
this.terminationReason = reason;
|
|
453
|
+
this.terminationDate = terminationDate;
|
|
454
|
+
return this.save();
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
PropertyManagerContractSchema.methods.completeContract = async function() {
|
|
458
|
+
this.status = 'Completed';
|
|
459
|
+
this.completionDate = new Date();
|
|
460
|
+
return this.save();
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// Method to calculate management fee for a given amount
|
|
464
|
+
PropertyManagerContractSchema.methods.calculateManagementFee = function(baseAmount) {
|
|
465
|
+
if (!this.managementFee) return 0;
|
|
466
|
+
|
|
467
|
+
if (this.managementFee.type === 'percentage') {
|
|
468
|
+
return (baseAmount * this.managementFee.value) / 100;
|
|
469
|
+
} else {
|
|
470
|
+
return this.managementFee.value;
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
// Method to get contract summary
|
|
475
|
+
PropertyManagerContractSchema.methods.getContractSummary = function() {
|
|
476
|
+
return {
|
|
477
|
+
id: this._id,
|
|
478
|
+
contractName: this.contractName,
|
|
479
|
+
status: this.status,
|
|
480
|
+
duration: this.durationInDays,
|
|
481
|
+
remainingDays: this.remainingDays,
|
|
482
|
+
managementFee: this.managementFeeDisplay,
|
|
483
|
+
frequency: this.frequency,
|
|
484
|
+
nextInvoiceDate: this.nextInvoiceDate,
|
|
485
|
+
isComplete: this.isComplete
|
|
486
|
+
};
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
// Pre-findOneAndUpdate middleware to maintain data integrity
|
|
490
|
+
PropertyManagerContractSchema.pre(['findOneAndUpdate', 'updateOne', 'updateMany'], function() {
|
|
491
|
+
const update = this.getUpdate();
|
|
492
|
+
|
|
493
|
+
// If status is being changed to Active, ensure required fields are present
|
|
494
|
+
if (update.$set && update.$set.status === 'Active') {
|
|
495
|
+
const requiredFields = ['startDate', 'endDate', 'paymentDueDate', 'frequency'];
|
|
496
|
+
// Note: We can't validate here because we don't have access to the full document
|
|
497
|
+
// Validation should be done in the application logic before calling update
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Update the updatedAt field
|
|
501
|
+
if (update.$set) {
|
|
502
|
+
update.$set.updatedAt = new Date();
|
|
503
|
+
} else {
|
|
504
|
+
this.set({ updatedAt: new Date() });
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// Post-save middleware for logging and notifications
|
|
509
|
+
PropertyManagerContractSchema.post('save', function(doc) {
|
|
510
|
+
// Log contract creation/updates
|
|
511
|
+
console.log(`Property Manager Contract ${doc.isNew ? 'created' : 'updated'}: ${doc.contractName} (${doc._id})`);
|
|
512
|
+
|
|
513
|
+
// Here you could add webhook calls, email notifications, etc.
|
|
514
|
+
// Example: notify property manager of contract status changes
|
|
515
|
+
if (this.isModified('status')) {
|
|
516
|
+
console.log(`Contract status changed to: ${doc.status}`);
|
|
517
|
+
// Add notification logic here
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Log invoice date updates
|
|
521
|
+
if (this.isModified('nextInvoiceDate') || this.isModified('lastInvoiceDate')) {
|
|
522
|
+
console.log(`Invoice dates updated for contract: ${doc.contractName}`);
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// Error handling for validation errors
|
|
527
|
+
PropertyManagerContractSchema.post('save', function(error, doc, next) {
|
|
528
|
+
if (error.name === 'ValidationError') {
|
|
529
|
+
const messages = Object.values(error.errors).map(err => err.message);
|
|
530
|
+
next(new Error(`Contract validation failed: ${messages.join(', ')}`));
|
|
531
|
+
} else {
|
|
532
|
+
next(error);
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// JSON transformation to remove sensitive data when returning to client
|
|
537
|
+
PropertyManagerContractSchema.methods.toJSON = function() {
|
|
538
|
+
const contract = this.toObject();
|
|
539
|
+
|
|
540
|
+
// Add computed fields
|
|
541
|
+
contract.isComplete = this.isComplete;
|
|
542
|
+
contract.canBeActivated = this.canBeActivated();
|
|
543
|
+
contract.missingRequiredFields = this.getMissingRequiredFields();
|
|
544
|
+
contract.durationInDays = this.durationInDays;
|
|
545
|
+
contract.remainingDays = this.remainingDays;
|
|
546
|
+
contract.managementFeeDisplay = this.managementFeeDisplay;
|
|
547
|
+
contract.contractSummary = this.getContractSummary();
|
|
548
|
+
|
|
549
|
+
return contract;
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
// Export the schema
|
|
553
|
+
module.exports = {
|
|
554
|
+
schema: PropertyManagerContractSchema,
|
|
555
|
+
name: 'PropertyManagerContract'
|
|
556
|
+
};
|