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.
Files changed (232) hide show
  1. package/.env +2 -2
  2. package/ZOHO_INTEGRATION_SCHEMA.md +644 -644
  3. package/index.js +418 -418
  4. package/package.json +17 -17
  5. package/src/models/InvoiceWithholdingTax.js +67 -67
  6. package/src/models/account.js +59 -59
  7. package/src/models/agent_departments.js +59 -59
  8. package/src/models/agent_notifications.js +53 -53
  9. package/src/models/agent_performance.js +127 -127
  10. package/src/models/agent_roles.js +77 -77
  11. package/src/models/agents.js +154 -154
  12. package/src/models/apilog.js +18 -18
  13. package/src/models/approvalsWorkflows.js +49 -49
  14. package/src/models/archivedapilog.js +18 -18
  15. package/src/models/asset.js +92 -92
  16. package/src/models/assetsAssignment.js +64 -64
  17. package/src/models/auditTrail.js +346 -346
  18. package/src/models/auto_reply_rule.js +68 -68
  19. package/src/models/bankdetails.js +47 -47
  20. package/src/models/billerAddress.js +124 -124
  21. package/src/models/booking_invoice.js +187 -187
  22. package/src/models/bookinganalytics.js +63 -63
  23. package/src/models/bookingconfig.js +74 -74
  24. package/src/models/bookingproperty.js +211 -211
  25. package/src/models/bookingreservation.js +298 -298
  26. package/src/models/bookingrevenuerecord.js +84 -84
  27. package/src/models/budget.js +95 -95
  28. package/src/models/budgetCategory.js +19 -19
  29. package/src/models/campaigns.js +108 -108
  30. package/src/models/cancellation_policy.js +56 -56
  31. package/src/models/cashpayment.js +289 -289
  32. package/src/models/combinedUnits.js +62 -62
  33. package/src/models/combined_invoice.js +424 -424
  34. package/src/models/common_area_electricity.js +38 -38
  35. package/src/models/common_area_generator.js +41 -41
  36. package/src/models/common_area_utility_alert.js +37 -37
  37. package/src/models/common_area_water.js +39 -39
  38. package/src/models/communication_status.js +33 -33
  39. package/src/models/communication_user_opt.js +32 -32
  40. package/src/models/community_guidelines.js +35 -35
  41. package/src/models/company.js +53 -53
  42. package/src/models/coreBaseSettings.js +16 -16
  43. package/src/models/coreInvoiceSettings.js +100 -100
  44. package/src/models/counter_schema.js +21 -21
  45. package/src/models/country_tax.js +42 -42
  46. package/src/models/currency_settings.js +39 -39
  47. package/src/models/customer.js +251 -251
  48. package/src/models/customer_preference.js +52 -52
  49. package/src/models/customer_satisfaction_survey.js +297 -297
  50. package/src/models/customer_surveys.js +139 -139
  51. package/src/models/customer_tickets.js +237 -237
  52. package/src/models/dailyChecklist.js +312 -312
  53. package/src/models/default_payment_details.js +17 -17
  54. package/src/models/deliveryTimeMarks.js +18 -18
  55. package/src/models/document_type.js +19 -19
  56. package/src/models/dutyRosterChecklist.js +250 -250
  57. package/src/models/dutyroster.js +136 -136
  58. package/src/models/email.js +37 -37
  59. package/src/models/email_cc_config.js +48 -48
  60. package/src/models/email_sms_queue.js +61 -61
  61. package/src/models/email_thread.js +35 -35
  62. package/src/models/entry_exit.js +53 -53
  63. package/src/models/expense.js +99 -99
  64. package/src/models/expense_category.js +45 -45
  65. package/src/models/facility.js +100 -100
  66. package/src/models/facilityBillingPrices.js +29 -29
  67. package/src/models/facilityInvoice.js +240 -240
  68. package/src/models/facilityInvoicePayment.js +52 -52
  69. package/src/models/facilityInvoiceRecipient.js +32 -32
  70. package/src/models/facilityWalletTransactionsMetadata.js +236 -236
  71. package/src/models/facility_departements.js +20 -20
  72. package/src/models/facility_etims_config.js +116 -116
  73. package/src/models/facility_payment_details.js +20 -20
  74. package/src/models/facility_rating.js +78 -78
  75. package/src/models/facilityasset.js +25 -25
  76. package/src/models/faq.js +15 -15
  77. package/src/models/gl_account_double_entries.js +25 -25
  78. package/src/models/gl_accounts.js +56 -56
  79. package/src/models/gl_entries.js +49 -49
  80. package/src/models/goodsReceivedNotes.js +115 -115
  81. package/src/models/guard.js +47 -47
  82. package/src/models/handover.js +258 -258
  83. package/src/models/inspection_category.js +38 -38
  84. package/src/models/invoice.js +598 -569
  85. package/src/models/invoiceCreditAdjustment.js +45 -45
  86. package/src/models/invoice_edit_log.js +81 -81
  87. package/src/models/invoice_generation_approval.js +86 -86
  88. package/src/models/invoicing_schedule.js +40 -40
  89. package/src/models/item_inspection.js +96 -96
  90. package/src/models/knowledge_base.js +109 -109
  91. package/src/models/knowledge_base_rating.js +44 -44
  92. package/src/models/leaseagreement.js +266 -266
  93. package/src/models/leasetemplate.js +17 -17
  94. package/src/models/levy.js +231 -231
  95. package/src/models/levy_deposits.js +114 -114
  96. package/src/models/levy_invoice_settings.js +26 -26
  97. package/src/models/levy_payment_receipt.js +156 -156
  98. package/src/models/levycontract.js +213 -213
  99. package/src/models/levytype.js +23 -23
  100. package/src/models/maintenance_service_vendor.js +38 -38
  101. package/src/models/maintenance_services.js +17 -17
  102. package/src/models/maintenancerequisition.js +31 -31
  103. package/src/models/master_workplan.js +32 -32
  104. package/src/models/master_workplan_child.js +34 -34
  105. package/src/models/message.js +38 -38
  106. package/src/models/module.js +21 -21
  107. package/src/models/movein_amenity.js +13 -13
  108. package/src/models/movein_application.js +28 -28
  109. package/src/models/movein_audit_log.js +21 -21
  110. package/src/models/movein_booking.js +33 -33
  111. package/src/models/movein_commission.js +46 -46
  112. package/src/models/movein_conversation.js +25 -25
  113. package/src/models/movein_deal.js +79 -79
  114. package/src/models/movein_disbursement.js +36 -36
  115. package/src/models/movein_email_thread.js +19 -19
  116. package/src/models/movein_enquiry.js +16 -16
  117. package/src/models/movein_featured_package.js +16 -16
  118. package/src/models/movein_handoff_token.js +16 -16
  119. package/src/models/movein_invoice.js +24 -24
  120. package/src/models/movein_landlord.js +18 -18
  121. package/src/models/movein_landlord_user.js +20 -20
  122. package/src/models/movein_lease.js +37 -37
  123. package/src/models/movein_message.js +27 -27
  124. package/src/models/movein_notification.js +27 -27
  125. package/src/models/movein_otp.js +14 -14
  126. package/src/models/movein_payment.js +46 -46
  127. package/src/models/movein_reminder.js +77 -77
  128. package/src/models/movein_reservation.js +31 -31
  129. package/src/models/movein_settings.js +25 -25
  130. package/src/models/movein_tenancy.js +55 -55
  131. package/src/models/movein_unit.js +59 -59
  132. package/src/models/movein_user.js +15 -15
  133. package/src/models/movein_viewing_slot.js +21 -21
  134. package/src/models/movein_want_unit.js +20 -20
  135. package/src/models/notification.js +44 -44
  136. package/src/models/paymentTermsMarks.js +19 -19
  137. package/src/models/penalty.js +76 -76
  138. package/src/models/pendingCredentials.js +32 -32
  139. package/src/models/powerMeterCommunicationProtocol.js +17 -17
  140. package/src/models/powerMeterCustomerAccount.js +78 -78
  141. package/src/models/powerMeterCustomerBand.js +14 -14
  142. package/src/models/powerMeterDailyReading.js +30 -30
  143. package/src/models/powerMeterGateways.js +40 -40
  144. package/src/models/powerMeterMonthlyReading.js +34 -34
  145. package/src/models/powerMeterPowerCharges.js +85 -85
  146. package/src/models/powerMeterSettings.js +211 -211
  147. package/src/models/powerMeterSingleDayReading.js +32 -32
  148. package/src/models/powerMeters.js +149 -149
  149. package/src/models/powerMetersManufacturer.js +14 -14
  150. package/src/models/power_invoice.js +371 -371
  151. package/src/models/power_meter_account.js +81 -81
  152. package/src/models/power_meter_command_logs.js +30 -30
  153. package/src/models/power_meter_command_queue.js +33 -33
  154. package/src/models/power_meter_negative_balance.js +44 -44
  155. package/src/models/power_prepaid_credits.js +47 -47
  156. package/src/models/power_prepaid_debits.js +53 -53
  157. package/src/models/power_prepaid_orders.js +78 -78
  158. package/src/models/power_sms_notification.js +26 -26
  159. package/src/models/privacy_policy.js +19 -19
  160. package/src/models/propertyManagementSettings.js +55 -55
  161. package/src/models/propertyManagerContract.js +611 -611
  162. package/src/models/propertyManagerRevenue.js +195 -195
  163. package/src/models/propertyManagerRevenueEntry.js +118 -118
  164. package/src/models/purchaseOrderInvoice.js +74 -74
  165. package/src/models/purchase_order.js +213 -213
  166. package/src/models/purchase_request.js +110 -110
  167. package/src/models/quickbooks_config.js +52 -52
  168. package/src/models/recipient_group.js +61 -61
  169. package/src/models/recipient_group_member.js +62 -62
  170. package/src/models/refresh_token.js +23 -23
  171. package/src/models/reminder.js +197 -197
  172. package/src/models/report.js +13 -13
  173. package/src/models/resident.js +121 -121
  174. package/src/models/rfq_details.js +131 -131
  175. package/src/models/rfq_response.js +153 -153
  176. package/src/models/service_charge_invoice_upload.js +42 -42
  177. package/src/models/service_charge_payments.js +27 -27
  178. package/src/models/servicerequest.js +55 -55
  179. package/src/models/settings.js +62 -62
  180. package/src/models/short_urls.js +21 -21
  181. package/src/models/smart_meter_daily_consumption.js +44 -44
  182. package/src/models/sms_africastalking.js +20 -20
  183. package/src/models/sms_balance_notification.js +26 -26
  184. package/src/models/sms_meliora.js +20 -20
  185. package/src/models/staff.js +36 -36
  186. package/src/models/stocksandspare.js +161 -161
  187. package/src/models/suppliers.js +79 -79
  188. package/src/models/terms_and_conditions.js +19 -19
  189. package/src/models/tickets.js +186 -186
  190. package/src/models/tickets_category.js +72 -72
  191. package/src/models/unitManagementTemplate.js +44 -44
  192. package/src/models/unitasset.js +25 -25
  193. package/src/models/units.js +148 -148
  194. package/src/models/user.js +186 -186
  195. package/src/models/valueaddedservices.js +50 -50
  196. package/src/models/vas_invoices_upload.js +50 -50
  197. package/src/models/vas_payments.js +24 -24
  198. package/src/models/vasinvoice.js +196 -196
  199. package/src/models/vasvendor.js +52 -52
  200. package/src/models/visitLog.js +108 -108
  201. package/src/models/visitor.js +67 -67
  202. package/src/models/waitlist.js +45 -45
  203. package/src/models/wallet.js +44 -44
  204. package/src/models/wallet_transaction.js +67 -67
  205. package/src/models/wallet_transactions.js +72 -72
  206. package/src/models/water_invoice.js +362 -362
  207. package/src/models/water_meter_Command_Queue.js +33 -33
  208. package/src/models/water_meter_account.js +86 -86
  209. package/src/models/water_meter_billing.js +58 -58
  210. package/src/models/water_meter_combined_accounts.js +92 -92
  211. package/src/models/water_meter_communication.js +17 -17
  212. package/src/models/water_meter_communication_logs.js +39 -39
  213. package/src/models/water_meter_concentrator.js +77 -77
  214. package/src/models/water_meter_daily_history.js +32 -32
  215. package/src/models/water_meter_high_risk.js +36 -36
  216. package/src/models/water_meter_iot_cards.js +34 -34
  217. package/src/models/water_meter_loan_deduction.js +134 -134
  218. package/src/models/water_meter_manufacturer.js +35 -35
  219. package/src/models/water_meter_monthly_history.js +36 -36
  220. package/src/models/water_meter_negative_amounts.js +44 -44
  221. package/src/models/water_meter_settings.js +303 -303
  222. package/src/models/water_meter_single_day_history.js +34 -34
  223. package/src/models/water_meter_size.js +15 -15
  224. package/src/models/water_meters.js +176 -176
  225. package/src/models/water_meters_delivery.js +76 -76
  226. package/src/models/water_prepaid_credit.js +47 -47
  227. package/src/models/water_prepaid_debit.js +50 -50
  228. package/src/models/whatsapp_conversation.js +23 -23
  229. package/src/models/workorder.js +49 -49
  230. package/src/models/zohoAccount.js +453 -453
  231. package/src/models/zohoIntegration.js +262 -262
  232. 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
  };