payservedb 9.6.0 → 9.6.2
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/ZOHO_INTEGRATION_SCHEMA.md +644 -644
- package/index.js +418 -418
- package/package.json +17 -17
- package/src/models/InvoiceWithholdingTax.js +67 -67
- package/src/models/account.js +59 -59
- 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/auto_reply_rule.js +68 -68
- package/src/models/bankdetails.js +47 -47
- package/src/models/billerAddress.js +124 -124
- package/src/models/booking_invoice.js +187 -187
- package/src/models/bookinganalytics.js +63 -63
- package/src/models/bookingconfig.js +74 -74
- package/src/models/bookingproperty.js +211 -211
- package/src/models/bookingreservation.js +298 -298
- 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/cancellation_policy.js +56 -56
- package/src/models/cashpayment.js +289 -289
- package/src/models/combinedUnits.js +62 -62
- package/src/models/combined_invoice.js +424 -424
- 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/community_guidelines.js +35 -35
- 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 +251 -251
- package/src/models/customer_preference.js +52 -52
- package/src/models/customer_satisfaction_survey.js +297 -297
- package/src/models/customer_surveys.js +139 -139
- package/src/models/customer_tickets.js +237 -237
- 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_cc_config.js +48 -48
- package/src/models/email_sms_queue.js +61 -61
- package/src/models/email_thread.js +35 -35
- 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 +100 -100
- package/src/models/facilityBillingPrices.js +29 -29
- package/src/models/facilityInvoice.js +240 -240
- package/src/models/facilityInvoicePayment.js +52 -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_etims_config.js +116 -116
- package/src/models/facility_payment_details.js +20 -20
- package/src/models/facility_rating.js +78 -78
- package/src/models/facilityasset.js +25 -25
- package/src/models/faq.js +15 -15
- 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 +258 -258
- package/src/models/inspection_category.js +38 -38
- package/src/models/invoice.js +598 -569
- package/src/models/invoiceCreditAdjustment.js +45 -45
- package/src/models/invoice_edit_log.js +81 -81
- package/src/models/invoice_generation_approval.js +86 -86
- package/src/models/invoicing_schedule.js +40 -40
- 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 +266 -266
- package/src/models/leasetemplate.js +17 -17
- package/src/models/levy.js +231 -231
- package/src/models/levy_deposits.js +114 -114
- package/src/models/levy_invoice_settings.js +26 -26
- package/src/models/levy_payment_receipt.js +156 -156
- package/src/models/levycontract.js +213 -213
- 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/movein_amenity.js +13 -13
- package/src/models/movein_application.js +28 -28
- package/src/models/movein_audit_log.js +21 -21
- package/src/models/movein_booking.js +33 -33
- package/src/models/movein_commission.js +46 -46
- package/src/models/movein_conversation.js +25 -25
- package/src/models/movein_deal.js +79 -79
- package/src/models/movein_disbursement.js +36 -36
- package/src/models/movein_email_thread.js +19 -19
- package/src/models/movein_enquiry.js +16 -16
- package/src/models/movein_featured_package.js +16 -16
- package/src/models/movein_handoff_token.js +16 -16
- package/src/models/movein_invoice.js +24 -24
- package/src/models/movein_landlord.js +18 -18
- package/src/models/movein_landlord_user.js +20 -20
- package/src/models/movein_lease.js +37 -37
- package/src/models/movein_message.js +27 -27
- package/src/models/movein_notification.js +27 -27
- package/src/models/movein_otp.js +14 -14
- package/src/models/movein_payment.js +46 -46
- package/src/models/movein_reminder.js +77 -77
- package/src/models/movein_reservation.js +31 -31
- package/src/models/movein_settings.js +25 -25
- package/src/models/movein_tenancy.js +55 -55
- package/src/models/movein_unit.js +59 -59
- package/src/models/movein_user.js +15 -15
- package/src/models/movein_viewing_slot.js +21 -21
- package/src/models/movein_want_unit.js +20 -20
- 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 +211 -211
- package/src/models/powerMeterSingleDayReading.js +32 -32
- package/src/models/powerMeters.js +149 -149
- package/src/models/powerMetersManufacturer.js +14 -14
- package/src/models/power_invoice.js +371 -371
- 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/privacy_policy.js +19 -19
- package/src/models/propertyManagementSettings.js +55 -55
- package/src/models/propertyManagerContract.js +611 -611
- package/src/models/propertyManagerRevenue.js +195 -195
- package/src/models/propertyManagerRevenueEntry.js +118 -118
- 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/quickbooks_config.js +52 -52
- package/src/models/recipient_group.js +61 -61
- package/src/models/recipient_group_member.js +62 -62
- 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 +79 -79
- package/src/models/terms_and_conditions.js +19 -19
- package/src/models/tickets.js +186 -186
- 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 +148 -148
- package/src/models/user.js +186 -186
- package/src/models/valueaddedservices.js +50 -50
- package/src/models/vas_invoices_upload.js +50 -50
- package/src/models/vas_payments.js +24 -24
- package/src/models/vasinvoice.js +196 -196
- package/src/models/vasvendor.js +52 -52
- package/src/models/visitLog.js +108 -108
- 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_transaction.js +67 -67
- package/src/models/wallet_transactions.js +72 -72
- package/src/models/water_invoice.js +362 -362
- package/src/models/water_meter_Command_Queue.js +33 -33
- package/src/models/water_meter_account.js +86 -86
- package/src/models/water_meter_billing.js +58 -58
- package/src/models/water_meter_combined_accounts.js +92 -92
- 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 +77 -77
- 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_loan_deduction.js +134 -134
- 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 +303 -303
- 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 +176 -176
- 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/whatsapp_conversation.js +23 -23
- package/src/models/workorder.js +49 -49
- package/src/models/zohoAccount.js +453 -453
- package/src/models/zohoIntegration.js +262 -262
- package/src/models/zohoItem.js +504 -504
|
@@ -1,612 +1,612 @@
|
|
|
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
|
-
// Per-contract lease commission override — when set (value present)
|
|
98
|
-
// it takes precedence over the facility-wide
|
|
99
|
-
// PropertyManagementSettings default. Lease commission is the
|
|
100
|
-
// one-time fee earned when the property manager places a new
|
|
101
|
-
// tenant (first paid rent invoice).
|
|
102
|
-
leaseFee: {
|
|
103
|
-
type: {
|
|
104
|
-
type: String,
|
|
105
|
-
enum: ['percentage', 'amount']
|
|
106
|
-
},
|
|
107
|
-
value: {
|
|
108
|
-
type: Number,
|
|
109
|
-
min: [0, 'Lease commission value cannot be negative'],
|
|
110
|
-
validate: {
|
|
111
|
-
validator: function (value) {
|
|
112
|
-
if (this.leaseFee && this.leaseFee.type === 'percentage') {
|
|
113
|
-
return value <= 100;
|
|
114
|
-
}
|
|
115
|
-
return true;
|
|
116
|
-
},
|
|
117
|
-
message: 'Lease commission percentage cannot exceed 100%'
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
// Currency for fixed-amount managementFee/leaseFee values above.
|
|
122
|
-
// Only meaningful when the fee type is 'amount' (percentage fees are
|
|
123
|
-
// currency-agnostic).
|
|
124
|
-
currencyId: {
|
|
125
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
126
|
-
ref: 'Currency'
|
|
127
|
-
},
|
|
128
|
-
// Per-unit fee overrides. Each unit in `units` may have a different
|
|
129
|
-
// management/lease fee here; a unit with no entry here falls back to
|
|
130
|
-
// the contract-level managementFee/leaseFee/currencyId above. This is
|
|
131
|
-
// what lets one contract cover multiple units at different rates.
|
|
132
|
-
unitFees: [{
|
|
133
|
-
unitId: {
|
|
134
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
135
|
-
ref: 'Unit',
|
|
136
|
-
required: true
|
|
137
|
-
},
|
|
138
|
-
managementFee: {
|
|
139
|
-
type: {
|
|
140
|
-
type: String,
|
|
141
|
-
enum: ['percentage', 'amount']
|
|
142
|
-
},
|
|
143
|
-
value: {
|
|
144
|
-
type: Number,
|
|
145
|
-
min: [0, 'Management fee value cannot be negative']
|
|
146
|
-
}
|
|
147
|
-
},
|
|
148
|
-
leaseFee: {
|
|
149
|
-
type: {
|
|
150
|
-
type: String,
|
|
151
|
-
enum: ['percentage', 'amount']
|
|
152
|
-
},
|
|
153
|
-
value: {
|
|
154
|
-
type: Number,
|
|
155
|
-
min: [0, 'Lease commission value cannot be negative']
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
currencyId: {
|
|
159
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
160
|
-
ref: 'Currency'
|
|
161
|
-
}
|
|
162
|
-
}],
|
|
163
|
-
// GL Account configurations - updated to match sample data structure
|
|
164
|
-
invoiceDoubleEntryAccount: {
|
|
165
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
166
|
-
ref: 'GLAccountDoubleEntries'
|
|
167
|
-
},
|
|
168
|
-
paymentDoubleEntryAccount: {
|
|
169
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
170
|
-
ref: 'GLAccountDoubleEntries'
|
|
171
|
-
},
|
|
172
|
-
glAccounts: {
|
|
173
|
-
invoice: {
|
|
174
|
-
debit: {
|
|
175
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
176
|
-
ref: 'GLAccount'
|
|
177
|
-
},
|
|
178
|
-
credit: {
|
|
179
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
180
|
-
ref: 'GLAccount'
|
|
181
|
-
}
|
|
182
|
-
},
|
|
183
|
-
payment: {
|
|
184
|
-
debit: {
|
|
185
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
186
|
-
ref: 'GLAccount'
|
|
187
|
-
},
|
|
188
|
-
credit: {
|
|
189
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
190
|
-
ref: 'GLAccount'
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
},
|
|
194
|
-
status: {
|
|
195
|
-
type: String,
|
|
196
|
-
enum: ['Active', 'Inactive', 'Completed', 'Suspended', 'Terminated'],
|
|
197
|
-
// Contracts default Active on creation - billing eligibility is
|
|
198
|
-
// driven by lease existence, not contract status. Status is now
|
|
199
|
-
// a purely manual lifecycle field the property manager controls.
|
|
200
|
-
default: 'Active',
|
|
201
|
-
required: [true, 'Status is required']
|
|
202
|
-
},
|
|
203
|
-
createdBy: {
|
|
204
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
205
|
-
ref: 'User' // References global User collection
|
|
206
|
-
},
|
|
207
|
-
// Additional fields for contract lifecycle management
|
|
208
|
-
suspensionReason: {
|
|
209
|
-
type: String
|
|
210
|
-
},
|
|
211
|
-
terminationReason: {
|
|
212
|
-
type: String
|
|
213
|
-
},
|
|
214
|
-
terminationDate: {
|
|
215
|
-
type: Date
|
|
216
|
-
},
|
|
217
|
-
completionDate: {
|
|
218
|
-
type: Date
|
|
219
|
-
}
|
|
220
|
-
},
|
|
221
|
-
{
|
|
222
|
-
timestamps: true,
|
|
223
|
-
// Ensure virtual fields are included in JSON output
|
|
224
|
-
toJSON: { virtuals: true },
|
|
225
|
-
toObject: { virtuals: true }
|
|
226
|
-
}
|
|
227
|
-
);
|
|
228
|
-
|
|
229
|
-
// Enhanced pre-save validation
|
|
230
|
-
PropertyManagerContractSchema.pre('save', function (next) {
|
|
231
|
-
// Validate date range
|
|
232
|
-
if (this.startDate && this.endDate && this.startDate >= this.endDate) {
|
|
233
|
-
next(new Error('End date must be after start date'));
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Validate units array
|
|
238
|
-
if (!this.units || this.units.length === 0) {
|
|
239
|
-
next(new Error('At least one unit must be specified'));
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Validate management fee
|
|
244
|
-
if (!this.managementFee || !this.managementFee.type || this.managementFee.value === undefined) {
|
|
245
|
-
next(new Error('Management fee type and value are required'));
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Validate management fee percentage
|
|
250
|
-
if (this.managementFee.type === 'percentage' && this.managementFee.value > 100) {
|
|
251
|
-
next(new Error('Management fee percentage cannot exceed 100%'));
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Validate management fee value is non-negative
|
|
256
|
-
if (this.managementFee.value < 0) {
|
|
257
|
-
next(new Error('Management fee value cannot be negative'));
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Validate payment due date range
|
|
262
|
-
if (this.paymentDueDate && (this.paymentDueDate < 1 || this.paymentDueDate > 31)) {
|
|
263
|
-
next(new Error('Payment due date must be between 1 and 31'));
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Note: startDate/endDate/paymentDueDate/frequency are no longer
|
|
268
|
-
// required for Active status - contracts are always Active regardless
|
|
269
|
-
// of whether a lease has populated those fields yet. They remain
|
|
270
|
-
// required only for billing (see invoice_service's eligibility checks,
|
|
271
|
-
// which additionally require an active lease before generating an
|
|
272
|
-
// invoice).
|
|
273
|
-
|
|
274
|
-
next();
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
// Method to sync with lease data
|
|
278
|
-
PropertyManagerContractSchema.methods.syncWithLeaseData = async function(leaseData) {
|
|
279
|
-
if (!leaseData) return;
|
|
280
|
-
|
|
281
|
-
// Update contract fields from lease data
|
|
282
|
-
this.startDate = leaseData.leaseTerms?.startDate || this.startDate;
|
|
283
|
-
this.endDate = leaseData.leaseTerms?.endDate || this.endDate;
|
|
284
|
-
this.paymentDueDate = leaseData.financialTerms?.paymentDueDate || this.paymentDueDate;
|
|
285
|
-
this.balanceBroughtForward = leaseData.financialTerms?.balanceBroughtForward || this.balanceBroughtForward;
|
|
286
|
-
this.frequency = leaseData.billingCycle?.frequency || this.frequency;
|
|
287
|
-
this.autoSend = leaseData.billingCycle?.autoSend !== undefined ? leaseData.billingCycle.autoSend : this.autoSend;
|
|
288
|
-
this.nextInvoiceDate = leaseData.billingCycle?.nextInvoiceDate || this.nextInvoiceDate;
|
|
289
|
-
this.lastInvoiceDate = leaseData.billingCycle?.lastInvoiceDate || this.lastInvoiceDate;
|
|
290
|
-
|
|
291
|
-
// Automatically activate contract if all required fields are present
|
|
292
|
-
if (this.startDate && this.endDate && this.paymentDueDate && this.frequency && this.status === 'Inactive') {
|
|
293
|
-
this.status = 'Active';
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return this.save();
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
// Method to check if contract can be activated
|
|
300
|
-
PropertyManagerContractSchema.methods.canBeActivated = function() {
|
|
301
|
-
return !!(this.startDate && this.endDate && this.paymentDueDate && this.frequency);
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
// Method to get missing required fields
|
|
305
|
-
PropertyManagerContractSchema.methods.getMissingRequiredFields = function() {
|
|
306
|
-
const requiredFields = ['startDate', 'endDate', 'paymentDueDate', 'frequency'];
|
|
307
|
-
return requiredFields.filter(field => !this[field]);
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
// Virtual for contract completion status
|
|
311
|
-
PropertyManagerContractSchema.virtual('isComplete').get(function() {
|
|
312
|
-
return this.canBeActivated();
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
// Virtual for contract duration in days
|
|
316
|
-
PropertyManagerContractSchema.virtual('durationInDays').get(function() {
|
|
317
|
-
if (!this.startDate || !this.endDate) return null;
|
|
318
|
-
const diffTime = Math.abs(this.endDate - this.startDate);
|
|
319
|
-
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
// Virtual for contract remaining days
|
|
323
|
-
PropertyManagerContractSchema.virtual('remainingDays').get(function() {
|
|
324
|
-
if (!this.endDate) return null;
|
|
325
|
-
const now = new Date();
|
|
326
|
-
if (now > this.endDate) return 0;
|
|
327
|
-
const diffTime = Math.abs(this.endDate - now);
|
|
328
|
-
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
// Virtual for management fee display
|
|
332
|
-
PropertyManagerContractSchema.virtual('managementFeeDisplay').get(function() {
|
|
333
|
-
if (!this.managementFee) return null;
|
|
334
|
-
|
|
335
|
-
if (this.managementFee.type === 'percentage') {
|
|
336
|
-
return `${this.managementFee.value}%`;
|
|
337
|
-
} else {
|
|
338
|
-
return `$${this.managementFee.value.toLocaleString()}`;
|
|
339
|
-
}
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
// Indexes for better query performance
|
|
343
|
-
PropertyManagerContractSchema.index({ customerId: 1, status: 1 });
|
|
344
|
-
PropertyManagerContractSchema.index({ facilityId: 1, status: 1 });
|
|
345
|
-
PropertyManagerContractSchema.index({ propertyManager: 1, facilityId: 1 });
|
|
346
|
-
PropertyManagerContractSchema.index({ units: 1, status: 1 });
|
|
347
|
-
PropertyManagerContractSchema.index({ nextInvoiceDate: 1, status: 1 });
|
|
348
|
-
PropertyManagerContractSchema.index({ endDate: 1, status: 1 }); // For contract expiration queries
|
|
349
|
-
PropertyManagerContractSchema.index({ 'managementFee.type': 1, 'managementFee.value': 1 }); // For fee analysis
|
|
350
|
-
PropertyManagerContractSchema.index({ createdAt: -1 }); // For recent contracts
|
|
351
|
-
PropertyManagerContractSchema.index({ startDate: 1, endDate: 1 }); // For date range queries
|
|
352
|
-
|
|
353
|
-
// Compound indexes for common query patterns
|
|
354
|
-
PropertyManagerContractSchema.index({
|
|
355
|
-
facilityId: 1,
|
|
356
|
-
propertyManager: 1,
|
|
357
|
-
status: 1
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
PropertyManagerContractSchema.index({
|
|
361
|
-
facilityId: 1,
|
|
362
|
-
customerId: 1,
|
|
363
|
-
status: 1
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
// Text index for searching contract names
|
|
367
|
-
PropertyManagerContractSchema.index({
|
|
368
|
-
contractName: 'text'
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
// Static methods for common queries
|
|
372
|
-
PropertyManagerContractSchema.statics.findActiveContractsByFacility = function(facilityId) {
|
|
373
|
-
return this.find({
|
|
374
|
-
facilityId: facilityId,
|
|
375
|
-
status: 'Active'
|
|
376
|
-
}).populate('propertyManager', 'fullName email')
|
|
377
|
-
.populate('units', 'name unitType')
|
|
378
|
-
.populate('customerId', 'firstName lastName email');
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
PropertyManagerContractSchema.statics.findContractsByPropertyManager = function(propertyManagerId, facilityId) {
|
|
382
|
-
return this.find({
|
|
383
|
-
propertyManager: propertyManagerId,
|
|
384
|
-
facilityId: facilityId
|
|
385
|
-
}).populate('units', 'name unitType')
|
|
386
|
-
.populate('customerId', 'firstName lastName email')
|
|
387
|
-
.sort({ createdAt: -1 });
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
PropertyManagerContractSchema.statics.findExpiringContracts = function(facilityId, daysAhead = 30) {
|
|
391
|
-
const futureDate = new Date();
|
|
392
|
-
futureDate.setDate(futureDate.getDate() + daysAhead);
|
|
393
|
-
|
|
394
|
-
return this.find({
|
|
395
|
-
facilityId: facilityId,
|
|
396
|
-
status: 'Active',
|
|
397
|
-
endDate: {
|
|
398
|
-
$gte: new Date(),
|
|
399
|
-
$lte: futureDate
|
|
400
|
-
}
|
|
401
|
-
}).populate('propertyManager', 'fullName email')
|
|
402
|
-
.populate('units', 'name unitType')
|
|
403
|
-
.populate('customerId', 'firstName lastName email')
|
|
404
|
-
.sort({ endDate: 1 });
|
|
405
|
-
};
|
|
406
|
-
|
|
407
|
-
PropertyManagerContractSchema.statics.findContractsRequiringInvoicing = function(facilityId) {
|
|
408
|
-
const today = new Date();
|
|
409
|
-
today.setHours(0, 0, 0, 0);
|
|
410
|
-
|
|
411
|
-
return this.find({
|
|
412
|
-
facilityId: facilityId,
|
|
413
|
-
status: 'Active',
|
|
414
|
-
nextInvoiceDate: {
|
|
415
|
-
$lte: today
|
|
416
|
-
}
|
|
417
|
-
}).populate('propertyManager', 'fullName email')
|
|
418
|
-
.populate('units', 'name unitType')
|
|
419
|
-
.populate('customerId', 'firstName lastName email');
|
|
420
|
-
};
|
|
421
|
-
|
|
422
|
-
PropertyManagerContractSchema.statics.findContractsByFeeType = function(facilityId, feeType) {
|
|
423
|
-
return this.find({
|
|
424
|
-
facilityId: facilityId,
|
|
425
|
-
'managementFee.type': feeType
|
|
426
|
-
}).populate('propertyManager', 'fullName email')
|
|
427
|
-
.populate('units', 'name unitType')
|
|
428
|
-
.populate('customerId', 'firstName lastName email')
|
|
429
|
-
.sort({ 'managementFee.value': -1 });
|
|
430
|
-
};
|
|
431
|
-
|
|
432
|
-
PropertyManagerContractSchema.statics.getContractStatistics = function(facilityId) {
|
|
433
|
-
return this.aggregate([
|
|
434
|
-
{
|
|
435
|
-
$match: { facilityId: mongoose.Types.ObjectId(facilityId) }
|
|
436
|
-
},
|
|
437
|
-
{
|
|
438
|
-
$group: {
|
|
439
|
-
_id: '$status',
|
|
440
|
-
count: { $sum: 1 },
|
|
441
|
-
totalFeeValue: { $sum: '$managementFee.value' },
|
|
442
|
-
avgFeeValue: { $avg: '$managementFee.value' }
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
]);
|
|
446
|
-
};
|
|
447
|
-
|
|
448
|
-
// Instance methods for contract management
|
|
449
|
-
PropertyManagerContractSchema.methods.calculateNextInvoiceDate = function() {
|
|
450
|
-
if (!this.lastInvoiceDate && !this.startDate) return null;
|
|
451
|
-
|
|
452
|
-
const baseDate = this.lastInvoiceDate || this.startDate;
|
|
453
|
-
const nextDate = new Date(baseDate);
|
|
454
|
-
|
|
455
|
-
switch (this.frequency) {
|
|
456
|
-
case 'Monthly':
|
|
457
|
-
nextDate.setMonth(nextDate.getMonth() + 1);
|
|
458
|
-
break;
|
|
459
|
-
case 'Quarterly':
|
|
460
|
-
nextDate.setMonth(nextDate.getMonth() + 3);
|
|
461
|
-
break;
|
|
462
|
-
case 'Annually':
|
|
463
|
-
nextDate.setFullYear(nextDate.getFullYear() + 1);
|
|
464
|
-
break;
|
|
465
|
-
default:
|
|
466
|
-
nextDate.setMonth(nextDate.getMonth() + 1); // Default to monthly
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// Set to the payment due date of the month
|
|
470
|
-
if (this.paymentDueDate) {
|
|
471
|
-
const lastDayOfMonth = new Date(nextDate.getFullYear(), nextDate.getMonth() + 1, 0).getDate();
|
|
472
|
-
const dueDate = Math.min(this.paymentDueDate, lastDayOfMonth);
|
|
473
|
-
nextDate.setDate(dueDate);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
return nextDate;
|
|
477
|
-
};
|
|
478
|
-
|
|
479
|
-
PropertyManagerContractSchema.methods.updateInvoiceDates = async function(invoiceDate = new Date()) {
|
|
480
|
-
this.lastInvoiceDate = invoiceDate;
|
|
481
|
-
this.nextInvoiceDate = this.calculateNextInvoiceDate();
|
|
482
|
-
return this.save();
|
|
483
|
-
};
|
|
484
|
-
|
|
485
|
-
PropertyManagerContractSchema.methods.activateContract = async function() {
|
|
486
|
-
if (!this.canBeActivated()) {
|
|
487
|
-
throw new Error(`Cannot activate contract: missing required fields - ${this.getMissingRequiredFields().join(', ')}`);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
this.status = 'Active';
|
|
491
|
-
|
|
492
|
-
// Set next invoice date if not already set
|
|
493
|
-
if (!this.nextInvoiceDate) {
|
|
494
|
-
this.nextInvoiceDate = this.calculateNextInvoiceDate();
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
return this.save();
|
|
498
|
-
};
|
|
499
|
-
|
|
500
|
-
PropertyManagerContractSchema.methods.suspendContract = async function(reason) {
|
|
501
|
-
this.status = 'Suspended';
|
|
502
|
-
this.suspensionReason = reason;
|
|
503
|
-
return this.save();
|
|
504
|
-
};
|
|
505
|
-
|
|
506
|
-
PropertyManagerContractSchema.methods.terminateContract = async function(reason, terminationDate = new Date()) {
|
|
507
|
-
this.status = 'Terminated';
|
|
508
|
-
this.terminationReason = reason;
|
|
509
|
-
this.terminationDate = terminationDate;
|
|
510
|
-
return this.save();
|
|
511
|
-
};
|
|
512
|
-
|
|
513
|
-
PropertyManagerContractSchema.methods.completeContract = async function() {
|
|
514
|
-
this.status = 'Completed';
|
|
515
|
-
this.completionDate = new Date();
|
|
516
|
-
return this.save();
|
|
517
|
-
};
|
|
518
|
-
|
|
519
|
-
// Method to calculate management fee for a given amount
|
|
520
|
-
PropertyManagerContractSchema.methods.calculateManagementFee = function(baseAmount) {
|
|
521
|
-
if (!this.managementFee) return 0;
|
|
522
|
-
|
|
523
|
-
if (this.managementFee.type === 'percentage') {
|
|
524
|
-
return (baseAmount * this.managementFee.value) / 100;
|
|
525
|
-
} else {
|
|
526
|
-
return this.managementFee.value;
|
|
527
|
-
}
|
|
528
|
-
};
|
|
529
|
-
|
|
530
|
-
// Method to get contract summary
|
|
531
|
-
PropertyManagerContractSchema.methods.getContractSummary = function() {
|
|
532
|
-
return {
|
|
533
|
-
id: this._id,
|
|
534
|
-
contractName: this.contractName,
|
|
535
|
-
status: this.status,
|
|
536
|
-
duration: this.durationInDays,
|
|
537
|
-
remainingDays: this.remainingDays,
|
|
538
|
-
managementFee: this.managementFeeDisplay,
|
|
539
|
-
frequency: this.frequency,
|
|
540
|
-
nextInvoiceDate: this.nextInvoiceDate,
|
|
541
|
-
isComplete: this.isComplete
|
|
542
|
-
};
|
|
543
|
-
};
|
|
544
|
-
|
|
545
|
-
// Pre-findOneAndUpdate middleware to maintain data integrity
|
|
546
|
-
PropertyManagerContractSchema.pre(['findOneAndUpdate', 'updateOne', 'updateMany'], function() {
|
|
547
|
-
const update = this.getUpdate();
|
|
548
|
-
|
|
549
|
-
// If status is being changed to Active, ensure required fields are present
|
|
550
|
-
if (update.$set && update.$set.status === 'Active') {
|
|
551
|
-
const requiredFields = ['startDate', 'endDate', 'paymentDueDate', 'frequency'];
|
|
552
|
-
// Note: We can't validate here because we don't have access to the full document
|
|
553
|
-
// Validation should be done in the application logic before calling update
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// Update the updatedAt field
|
|
557
|
-
if (update.$set) {
|
|
558
|
-
update.$set.updatedAt = new Date();
|
|
559
|
-
} else {
|
|
560
|
-
this.set({ updatedAt: new Date() });
|
|
561
|
-
}
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
// Post-save middleware for logging and notifications
|
|
565
|
-
PropertyManagerContractSchema.post('save', function(doc) {
|
|
566
|
-
// Log contract creation/updates
|
|
567
|
-
console.log(`Property Manager Contract ${doc.isNew ? 'created' : 'updated'}: ${doc.contractName} (${doc._id})`);
|
|
568
|
-
|
|
569
|
-
// Here you could add webhook calls, email notifications, etc.
|
|
570
|
-
// Example: notify property manager of contract status changes
|
|
571
|
-
if (this.isModified('status')) {
|
|
572
|
-
console.log(`Contract status changed to: ${doc.status}`);
|
|
573
|
-
// Add notification logic here
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// Log invoice date updates
|
|
577
|
-
if (this.isModified('nextInvoiceDate') || this.isModified('lastInvoiceDate')) {
|
|
578
|
-
console.log(`Invoice dates updated for contract: ${doc.contractName}`);
|
|
579
|
-
}
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
// Error handling for validation errors
|
|
583
|
-
PropertyManagerContractSchema.post('save', function(error, doc, next) {
|
|
584
|
-
if (error.name === 'ValidationError') {
|
|
585
|
-
const messages = Object.values(error.errors).map(err => err.message);
|
|
586
|
-
next(new Error(`Contract validation failed: ${messages.join(', ')}`));
|
|
587
|
-
} else {
|
|
588
|
-
next(error);
|
|
589
|
-
}
|
|
590
|
-
});
|
|
591
|
-
|
|
592
|
-
// JSON transformation to remove sensitive data when returning to client
|
|
593
|
-
PropertyManagerContractSchema.methods.toJSON = function() {
|
|
594
|
-
const contract = this.toObject();
|
|
595
|
-
|
|
596
|
-
// Add computed fields
|
|
597
|
-
contract.isComplete = this.isComplete;
|
|
598
|
-
contract.canBeActivated = this.canBeActivated();
|
|
599
|
-
contract.missingRequiredFields = this.getMissingRequiredFields();
|
|
600
|
-
contract.durationInDays = this.durationInDays;
|
|
601
|
-
contract.remainingDays = this.remainingDays;
|
|
602
|
-
contract.managementFeeDisplay = this.managementFeeDisplay;
|
|
603
|
-
contract.contractSummary = this.getContractSummary();
|
|
604
|
-
|
|
605
|
-
return contract;
|
|
606
|
-
};
|
|
607
|
-
|
|
608
|
-
// Export the schema
|
|
609
|
-
module.exports = {
|
|
610
|
-
schema: PropertyManagerContractSchema,
|
|
611
|
-
name: 'PropertyManagerContract'
|
|
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
|
+
// Per-contract lease commission override — when set (value present)
|
|
98
|
+
// it takes precedence over the facility-wide
|
|
99
|
+
// PropertyManagementSettings default. Lease commission is the
|
|
100
|
+
// one-time fee earned when the property manager places a new
|
|
101
|
+
// tenant (first paid rent invoice).
|
|
102
|
+
leaseFee: {
|
|
103
|
+
type: {
|
|
104
|
+
type: String,
|
|
105
|
+
enum: ['percentage', 'amount']
|
|
106
|
+
},
|
|
107
|
+
value: {
|
|
108
|
+
type: Number,
|
|
109
|
+
min: [0, 'Lease commission value cannot be negative'],
|
|
110
|
+
validate: {
|
|
111
|
+
validator: function (value) {
|
|
112
|
+
if (this.leaseFee && this.leaseFee.type === 'percentage') {
|
|
113
|
+
return value <= 100;
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
},
|
|
117
|
+
message: 'Lease commission percentage cannot exceed 100%'
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
// Currency for fixed-amount managementFee/leaseFee values above.
|
|
122
|
+
// Only meaningful when the fee type is 'amount' (percentage fees are
|
|
123
|
+
// currency-agnostic).
|
|
124
|
+
currencyId: {
|
|
125
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
126
|
+
ref: 'Currency'
|
|
127
|
+
},
|
|
128
|
+
// Per-unit fee overrides. Each unit in `units` may have a different
|
|
129
|
+
// management/lease fee here; a unit with no entry here falls back to
|
|
130
|
+
// the contract-level managementFee/leaseFee/currencyId above. This is
|
|
131
|
+
// what lets one contract cover multiple units at different rates.
|
|
132
|
+
unitFees: [{
|
|
133
|
+
unitId: {
|
|
134
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
135
|
+
ref: 'Unit',
|
|
136
|
+
required: true
|
|
137
|
+
},
|
|
138
|
+
managementFee: {
|
|
139
|
+
type: {
|
|
140
|
+
type: String,
|
|
141
|
+
enum: ['percentage', 'amount']
|
|
142
|
+
},
|
|
143
|
+
value: {
|
|
144
|
+
type: Number,
|
|
145
|
+
min: [0, 'Management fee value cannot be negative']
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
leaseFee: {
|
|
149
|
+
type: {
|
|
150
|
+
type: String,
|
|
151
|
+
enum: ['percentage', 'amount']
|
|
152
|
+
},
|
|
153
|
+
value: {
|
|
154
|
+
type: Number,
|
|
155
|
+
min: [0, 'Lease commission value cannot be negative']
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
currencyId: {
|
|
159
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
160
|
+
ref: 'Currency'
|
|
161
|
+
}
|
|
162
|
+
}],
|
|
163
|
+
// GL Account configurations - updated to match sample data structure
|
|
164
|
+
invoiceDoubleEntryAccount: {
|
|
165
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
166
|
+
ref: 'GLAccountDoubleEntries'
|
|
167
|
+
},
|
|
168
|
+
paymentDoubleEntryAccount: {
|
|
169
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
170
|
+
ref: 'GLAccountDoubleEntries'
|
|
171
|
+
},
|
|
172
|
+
glAccounts: {
|
|
173
|
+
invoice: {
|
|
174
|
+
debit: {
|
|
175
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
176
|
+
ref: 'GLAccount'
|
|
177
|
+
},
|
|
178
|
+
credit: {
|
|
179
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
180
|
+
ref: 'GLAccount'
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
payment: {
|
|
184
|
+
debit: {
|
|
185
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
186
|
+
ref: 'GLAccount'
|
|
187
|
+
},
|
|
188
|
+
credit: {
|
|
189
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
190
|
+
ref: 'GLAccount'
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
status: {
|
|
195
|
+
type: String,
|
|
196
|
+
enum: ['Active', 'Inactive', 'Completed', 'Suspended', 'Terminated'],
|
|
197
|
+
// Contracts default Active on creation - billing eligibility is
|
|
198
|
+
// driven by lease existence, not contract status. Status is now
|
|
199
|
+
// a purely manual lifecycle field the property manager controls.
|
|
200
|
+
default: 'Active',
|
|
201
|
+
required: [true, 'Status is required']
|
|
202
|
+
},
|
|
203
|
+
createdBy: {
|
|
204
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
205
|
+
ref: 'User' // References global User collection
|
|
206
|
+
},
|
|
207
|
+
// Additional fields for contract lifecycle management
|
|
208
|
+
suspensionReason: {
|
|
209
|
+
type: String
|
|
210
|
+
},
|
|
211
|
+
terminationReason: {
|
|
212
|
+
type: String
|
|
213
|
+
},
|
|
214
|
+
terminationDate: {
|
|
215
|
+
type: Date
|
|
216
|
+
},
|
|
217
|
+
completionDate: {
|
|
218
|
+
type: Date
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
timestamps: true,
|
|
223
|
+
// Ensure virtual fields are included in JSON output
|
|
224
|
+
toJSON: { virtuals: true },
|
|
225
|
+
toObject: { virtuals: true }
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// Enhanced pre-save validation
|
|
230
|
+
PropertyManagerContractSchema.pre('save', function (next) {
|
|
231
|
+
// Validate date range
|
|
232
|
+
if (this.startDate && this.endDate && this.startDate >= this.endDate) {
|
|
233
|
+
next(new Error('End date must be after start date'));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Validate units array
|
|
238
|
+
if (!this.units || this.units.length === 0) {
|
|
239
|
+
next(new Error('At least one unit must be specified'));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Validate management fee
|
|
244
|
+
if (!this.managementFee || !this.managementFee.type || this.managementFee.value === undefined) {
|
|
245
|
+
next(new Error('Management fee type and value are required'));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Validate management fee percentage
|
|
250
|
+
if (this.managementFee.type === 'percentage' && this.managementFee.value > 100) {
|
|
251
|
+
next(new Error('Management fee percentage cannot exceed 100%'));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Validate management fee value is non-negative
|
|
256
|
+
if (this.managementFee.value < 0) {
|
|
257
|
+
next(new Error('Management fee value cannot be negative'));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Validate payment due date range
|
|
262
|
+
if (this.paymentDueDate && (this.paymentDueDate < 1 || this.paymentDueDate > 31)) {
|
|
263
|
+
next(new Error('Payment due date must be between 1 and 31'));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Note: startDate/endDate/paymentDueDate/frequency are no longer
|
|
268
|
+
// required for Active status - contracts are always Active regardless
|
|
269
|
+
// of whether a lease has populated those fields yet. They remain
|
|
270
|
+
// required only for billing (see invoice_service's eligibility checks,
|
|
271
|
+
// which additionally require an active lease before generating an
|
|
272
|
+
// invoice).
|
|
273
|
+
|
|
274
|
+
next();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Method to sync with lease data
|
|
278
|
+
PropertyManagerContractSchema.methods.syncWithLeaseData = async function(leaseData) {
|
|
279
|
+
if (!leaseData) return;
|
|
280
|
+
|
|
281
|
+
// Update contract fields from lease data
|
|
282
|
+
this.startDate = leaseData.leaseTerms?.startDate || this.startDate;
|
|
283
|
+
this.endDate = leaseData.leaseTerms?.endDate || this.endDate;
|
|
284
|
+
this.paymentDueDate = leaseData.financialTerms?.paymentDueDate || this.paymentDueDate;
|
|
285
|
+
this.balanceBroughtForward = leaseData.financialTerms?.balanceBroughtForward || this.balanceBroughtForward;
|
|
286
|
+
this.frequency = leaseData.billingCycle?.frequency || this.frequency;
|
|
287
|
+
this.autoSend = leaseData.billingCycle?.autoSend !== undefined ? leaseData.billingCycle.autoSend : this.autoSend;
|
|
288
|
+
this.nextInvoiceDate = leaseData.billingCycle?.nextInvoiceDate || this.nextInvoiceDate;
|
|
289
|
+
this.lastInvoiceDate = leaseData.billingCycle?.lastInvoiceDate || this.lastInvoiceDate;
|
|
290
|
+
|
|
291
|
+
// Automatically activate contract if all required fields are present
|
|
292
|
+
if (this.startDate && this.endDate && this.paymentDueDate && this.frequency && this.status === 'Inactive') {
|
|
293
|
+
this.status = 'Active';
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return this.save();
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// Method to check if contract can be activated
|
|
300
|
+
PropertyManagerContractSchema.methods.canBeActivated = function() {
|
|
301
|
+
return !!(this.startDate && this.endDate && this.paymentDueDate && this.frequency);
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// Method to get missing required fields
|
|
305
|
+
PropertyManagerContractSchema.methods.getMissingRequiredFields = function() {
|
|
306
|
+
const requiredFields = ['startDate', 'endDate', 'paymentDueDate', 'frequency'];
|
|
307
|
+
return requiredFields.filter(field => !this[field]);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// Virtual for contract completion status
|
|
311
|
+
PropertyManagerContractSchema.virtual('isComplete').get(function() {
|
|
312
|
+
return this.canBeActivated();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Virtual for contract duration in days
|
|
316
|
+
PropertyManagerContractSchema.virtual('durationInDays').get(function() {
|
|
317
|
+
if (!this.startDate || !this.endDate) return null;
|
|
318
|
+
const diffTime = Math.abs(this.endDate - this.startDate);
|
|
319
|
+
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// Virtual for contract remaining days
|
|
323
|
+
PropertyManagerContractSchema.virtual('remainingDays').get(function() {
|
|
324
|
+
if (!this.endDate) return null;
|
|
325
|
+
const now = new Date();
|
|
326
|
+
if (now > this.endDate) return 0;
|
|
327
|
+
const diffTime = Math.abs(this.endDate - now);
|
|
328
|
+
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Virtual for management fee display
|
|
332
|
+
PropertyManagerContractSchema.virtual('managementFeeDisplay').get(function() {
|
|
333
|
+
if (!this.managementFee) return null;
|
|
334
|
+
|
|
335
|
+
if (this.managementFee.type === 'percentage') {
|
|
336
|
+
return `${this.managementFee.value}%`;
|
|
337
|
+
} else {
|
|
338
|
+
return `$${this.managementFee.value.toLocaleString()}`;
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Indexes for better query performance
|
|
343
|
+
PropertyManagerContractSchema.index({ customerId: 1, status: 1 });
|
|
344
|
+
PropertyManagerContractSchema.index({ facilityId: 1, status: 1 });
|
|
345
|
+
PropertyManagerContractSchema.index({ propertyManager: 1, facilityId: 1 });
|
|
346
|
+
PropertyManagerContractSchema.index({ units: 1, status: 1 });
|
|
347
|
+
PropertyManagerContractSchema.index({ nextInvoiceDate: 1, status: 1 });
|
|
348
|
+
PropertyManagerContractSchema.index({ endDate: 1, status: 1 }); // For contract expiration queries
|
|
349
|
+
PropertyManagerContractSchema.index({ 'managementFee.type': 1, 'managementFee.value': 1 }); // For fee analysis
|
|
350
|
+
PropertyManagerContractSchema.index({ createdAt: -1 }); // For recent contracts
|
|
351
|
+
PropertyManagerContractSchema.index({ startDate: 1, endDate: 1 }); // For date range queries
|
|
352
|
+
|
|
353
|
+
// Compound indexes for common query patterns
|
|
354
|
+
PropertyManagerContractSchema.index({
|
|
355
|
+
facilityId: 1,
|
|
356
|
+
propertyManager: 1,
|
|
357
|
+
status: 1
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
PropertyManagerContractSchema.index({
|
|
361
|
+
facilityId: 1,
|
|
362
|
+
customerId: 1,
|
|
363
|
+
status: 1
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Text index for searching contract names
|
|
367
|
+
PropertyManagerContractSchema.index({
|
|
368
|
+
contractName: 'text'
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Static methods for common queries
|
|
372
|
+
PropertyManagerContractSchema.statics.findActiveContractsByFacility = function(facilityId) {
|
|
373
|
+
return this.find({
|
|
374
|
+
facilityId: facilityId,
|
|
375
|
+
status: 'Active'
|
|
376
|
+
}).populate('propertyManager', 'fullName email')
|
|
377
|
+
.populate('units', 'name unitType')
|
|
378
|
+
.populate('customerId', 'firstName lastName email');
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
PropertyManagerContractSchema.statics.findContractsByPropertyManager = function(propertyManagerId, facilityId) {
|
|
382
|
+
return this.find({
|
|
383
|
+
propertyManager: propertyManagerId,
|
|
384
|
+
facilityId: facilityId
|
|
385
|
+
}).populate('units', 'name unitType')
|
|
386
|
+
.populate('customerId', 'firstName lastName email')
|
|
387
|
+
.sort({ createdAt: -1 });
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
PropertyManagerContractSchema.statics.findExpiringContracts = function(facilityId, daysAhead = 30) {
|
|
391
|
+
const futureDate = new Date();
|
|
392
|
+
futureDate.setDate(futureDate.getDate() + daysAhead);
|
|
393
|
+
|
|
394
|
+
return this.find({
|
|
395
|
+
facilityId: facilityId,
|
|
396
|
+
status: 'Active',
|
|
397
|
+
endDate: {
|
|
398
|
+
$gte: new Date(),
|
|
399
|
+
$lte: futureDate
|
|
400
|
+
}
|
|
401
|
+
}).populate('propertyManager', 'fullName email')
|
|
402
|
+
.populate('units', 'name unitType')
|
|
403
|
+
.populate('customerId', 'firstName lastName email')
|
|
404
|
+
.sort({ endDate: 1 });
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
PropertyManagerContractSchema.statics.findContractsRequiringInvoicing = function(facilityId) {
|
|
408
|
+
const today = new Date();
|
|
409
|
+
today.setHours(0, 0, 0, 0);
|
|
410
|
+
|
|
411
|
+
return this.find({
|
|
412
|
+
facilityId: facilityId,
|
|
413
|
+
status: 'Active',
|
|
414
|
+
nextInvoiceDate: {
|
|
415
|
+
$lte: today
|
|
416
|
+
}
|
|
417
|
+
}).populate('propertyManager', 'fullName email')
|
|
418
|
+
.populate('units', 'name unitType')
|
|
419
|
+
.populate('customerId', 'firstName lastName email');
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
PropertyManagerContractSchema.statics.findContractsByFeeType = function(facilityId, feeType) {
|
|
423
|
+
return this.find({
|
|
424
|
+
facilityId: facilityId,
|
|
425
|
+
'managementFee.type': feeType
|
|
426
|
+
}).populate('propertyManager', 'fullName email')
|
|
427
|
+
.populate('units', 'name unitType')
|
|
428
|
+
.populate('customerId', 'firstName lastName email')
|
|
429
|
+
.sort({ 'managementFee.value': -1 });
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
PropertyManagerContractSchema.statics.getContractStatistics = function(facilityId) {
|
|
433
|
+
return this.aggregate([
|
|
434
|
+
{
|
|
435
|
+
$match: { facilityId: mongoose.Types.ObjectId(facilityId) }
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
$group: {
|
|
439
|
+
_id: '$status',
|
|
440
|
+
count: { $sum: 1 },
|
|
441
|
+
totalFeeValue: { $sum: '$managementFee.value' },
|
|
442
|
+
avgFeeValue: { $avg: '$managementFee.value' }
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
]);
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
// Instance methods for contract management
|
|
449
|
+
PropertyManagerContractSchema.methods.calculateNextInvoiceDate = function() {
|
|
450
|
+
if (!this.lastInvoiceDate && !this.startDate) return null;
|
|
451
|
+
|
|
452
|
+
const baseDate = this.lastInvoiceDate || this.startDate;
|
|
453
|
+
const nextDate = new Date(baseDate);
|
|
454
|
+
|
|
455
|
+
switch (this.frequency) {
|
|
456
|
+
case 'Monthly':
|
|
457
|
+
nextDate.setMonth(nextDate.getMonth() + 1);
|
|
458
|
+
break;
|
|
459
|
+
case 'Quarterly':
|
|
460
|
+
nextDate.setMonth(nextDate.getMonth() + 3);
|
|
461
|
+
break;
|
|
462
|
+
case 'Annually':
|
|
463
|
+
nextDate.setFullYear(nextDate.getFullYear() + 1);
|
|
464
|
+
break;
|
|
465
|
+
default:
|
|
466
|
+
nextDate.setMonth(nextDate.getMonth() + 1); // Default to monthly
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Set to the payment due date of the month
|
|
470
|
+
if (this.paymentDueDate) {
|
|
471
|
+
const lastDayOfMonth = new Date(nextDate.getFullYear(), nextDate.getMonth() + 1, 0).getDate();
|
|
472
|
+
const dueDate = Math.min(this.paymentDueDate, lastDayOfMonth);
|
|
473
|
+
nextDate.setDate(dueDate);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return nextDate;
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
PropertyManagerContractSchema.methods.updateInvoiceDates = async function(invoiceDate = new Date()) {
|
|
480
|
+
this.lastInvoiceDate = invoiceDate;
|
|
481
|
+
this.nextInvoiceDate = this.calculateNextInvoiceDate();
|
|
482
|
+
return this.save();
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
PropertyManagerContractSchema.methods.activateContract = async function() {
|
|
486
|
+
if (!this.canBeActivated()) {
|
|
487
|
+
throw new Error(`Cannot activate contract: missing required fields - ${this.getMissingRequiredFields().join(', ')}`);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
this.status = 'Active';
|
|
491
|
+
|
|
492
|
+
// Set next invoice date if not already set
|
|
493
|
+
if (!this.nextInvoiceDate) {
|
|
494
|
+
this.nextInvoiceDate = this.calculateNextInvoiceDate();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return this.save();
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
PropertyManagerContractSchema.methods.suspendContract = async function(reason) {
|
|
501
|
+
this.status = 'Suspended';
|
|
502
|
+
this.suspensionReason = reason;
|
|
503
|
+
return this.save();
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
PropertyManagerContractSchema.methods.terminateContract = async function(reason, terminationDate = new Date()) {
|
|
507
|
+
this.status = 'Terminated';
|
|
508
|
+
this.terminationReason = reason;
|
|
509
|
+
this.terminationDate = terminationDate;
|
|
510
|
+
return this.save();
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
PropertyManagerContractSchema.methods.completeContract = async function() {
|
|
514
|
+
this.status = 'Completed';
|
|
515
|
+
this.completionDate = new Date();
|
|
516
|
+
return this.save();
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
// Method to calculate management fee for a given amount
|
|
520
|
+
PropertyManagerContractSchema.methods.calculateManagementFee = function(baseAmount) {
|
|
521
|
+
if (!this.managementFee) return 0;
|
|
522
|
+
|
|
523
|
+
if (this.managementFee.type === 'percentage') {
|
|
524
|
+
return (baseAmount * this.managementFee.value) / 100;
|
|
525
|
+
} else {
|
|
526
|
+
return this.managementFee.value;
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
// Method to get contract summary
|
|
531
|
+
PropertyManagerContractSchema.methods.getContractSummary = function() {
|
|
532
|
+
return {
|
|
533
|
+
id: this._id,
|
|
534
|
+
contractName: this.contractName,
|
|
535
|
+
status: this.status,
|
|
536
|
+
duration: this.durationInDays,
|
|
537
|
+
remainingDays: this.remainingDays,
|
|
538
|
+
managementFee: this.managementFeeDisplay,
|
|
539
|
+
frequency: this.frequency,
|
|
540
|
+
nextInvoiceDate: this.nextInvoiceDate,
|
|
541
|
+
isComplete: this.isComplete
|
|
542
|
+
};
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
// Pre-findOneAndUpdate middleware to maintain data integrity
|
|
546
|
+
PropertyManagerContractSchema.pre(['findOneAndUpdate', 'updateOne', 'updateMany'], function() {
|
|
547
|
+
const update = this.getUpdate();
|
|
548
|
+
|
|
549
|
+
// If status is being changed to Active, ensure required fields are present
|
|
550
|
+
if (update.$set && update.$set.status === 'Active') {
|
|
551
|
+
const requiredFields = ['startDate', 'endDate', 'paymentDueDate', 'frequency'];
|
|
552
|
+
// Note: We can't validate here because we don't have access to the full document
|
|
553
|
+
// Validation should be done in the application logic before calling update
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Update the updatedAt field
|
|
557
|
+
if (update.$set) {
|
|
558
|
+
update.$set.updatedAt = new Date();
|
|
559
|
+
} else {
|
|
560
|
+
this.set({ updatedAt: new Date() });
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
// Post-save middleware for logging and notifications
|
|
565
|
+
PropertyManagerContractSchema.post('save', function(doc) {
|
|
566
|
+
// Log contract creation/updates
|
|
567
|
+
console.log(`Property Manager Contract ${doc.isNew ? 'created' : 'updated'}: ${doc.contractName} (${doc._id})`);
|
|
568
|
+
|
|
569
|
+
// Here you could add webhook calls, email notifications, etc.
|
|
570
|
+
// Example: notify property manager of contract status changes
|
|
571
|
+
if (this.isModified('status')) {
|
|
572
|
+
console.log(`Contract status changed to: ${doc.status}`);
|
|
573
|
+
// Add notification logic here
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Log invoice date updates
|
|
577
|
+
if (this.isModified('nextInvoiceDate') || this.isModified('lastInvoiceDate')) {
|
|
578
|
+
console.log(`Invoice dates updated for contract: ${doc.contractName}`);
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// Error handling for validation errors
|
|
583
|
+
PropertyManagerContractSchema.post('save', function(error, doc, next) {
|
|
584
|
+
if (error.name === 'ValidationError') {
|
|
585
|
+
const messages = Object.values(error.errors).map(err => err.message);
|
|
586
|
+
next(new Error(`Contract validation failed: ${messages.join(', ')}`));
|
|
587
|
+
} else {
|
|
588
|
+
next(error);
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// JSON transformation to remove sensitive data when returning to client
|
|
593
|
+
PropertyManagerContractSchema.methods.toJSON = function() {
|
|
594
|
+
const contract = this.toObject();
|
|
595
|
+
|
|
596
|
+
// Add computed fields
|
|
597
|
+
contract.isComplete = this.isComplete;
|
|
598
|
+
contract.canBeActivated = this.canBeActivated();
|
|
599
|
+
contract.missingRequiredFields = this.getMissingRequiredFields();
|
|
600
|
+
contract.durationInDays = this.durationInDays;
|
|
601
|
+
contract.remainingDays = this.remainingDays;
|
|
602
|
+
contract.managementFeeDisplay = this.managementFeeDisplay;
|
|
603
|
+
contract.contractSummary = this.getContractSummary();
|
|
604
|
+
|
|
605
|
+
return contract;
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
// Export the schema
|
|
609
|
+
module.exports = {
|
|
610
|
+
schema: PropertyManagerContractSchema,
|
|
611
|
+
name: 'PropertyManagerContract'
|
|
612
612
|
};
|