payservedb 8.3.0 → 8.3.1
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/index.js +306 -306
- package/package.json +17 -17
- package/src/models/account.js +52 -52
- package/src/models/agent_departments.js +59 -59
- package/src/models/agent_notifications.js +53 -53
- package/src/models/agent_performance.js +127 -127
- package/src/models/agent_roles.js +77 -77
- package/src/models/agents.js +154 -154
- package/src/models/apilog.js +18 -18
- package/src/models/approvalsWorkflows.js +49 -49
- package/src/models/archivedapilog.js +18 -18
- package/src/models/asset.js +92 -92
- package/src/models/assetsAssignment.js +64 -64
- package/src/models/auditTrail.js +346 -346
- package/src/models/bankdetails.js +43 -47
- package/src/models/billerAddress.js +124 -124
- package/src/models/booking_invoice.js +151 -151
- package/src/models/bookinganalytics.js +63 -63
- package/src/models/bookingconfig.js +45 -45
- package/src/models/bookingproperty.js +122 -122
- package/src/models/bookingreservation.js +192 -192
- package/src/models/bookingrevenuerecord.js +84 -84
- package/src/models/budget.js +95 -95
- package/src/models/budgetCategory.js +19 -19
- package/src/models/campaigns.js +108 -108
- package/src/models/cashpayment.js +264 -264
- package/src/models/combinedUnits.js +62 -62
- package/src/models/common_area_electricity.js +38 -38
- package/src/models/common_area_generator.js +41 -41
- package/src/models/common_area_utility_alert.js +37 -37
- package/src/models/common_area_water.js +39 -39
- package/src/models/communication_status.js +33 -33
- package/src/models/communication_user_opt.js +32 -32
- package/src/models/company.js +53 -53
- package/src/models/coreBaseSettings.js +16 -16
- package/src/models/coreInvoiceSettings.js +100 -100
- package/src/models/counter_schema.js +21 -21
- package/src/models/country_tax.js +42 -42
- package/src/models/currency_settings.js +39 -39
- package/src/models/customer.js +210 -210
- package/src/models/customer_satisfaction_survey.js +278 -278
- package/src/models/customer_surveys.js +139 -139
- package/src/models/customer_tickets.js +239 -239
- package/src/models/dailyChecklist.js +312 -312
- package/src/models/default_payment_details.js +17 -17
- package/src/models/deliveryTimeMarks.js +18 -18
- package/src/models/document_type.js +19 -19
- package/src/models/dutyRosterChecklist.js +250 -250
- package/src/models/dutyroster.js +136 -136
- package/src/models/email.js +37 -37
- package/src/models/email_sms_queue.js +61 -61
- package/src/models/entry_exit.js +53 -53
- package/src/models/expense.js +99 -99
- package/src/models/expense_category.js +45 -45
- package/src/models/facility.js +62 -62
- package/src/models/facilityBillingPrices.js +29 -29
- package/src/models/facilityInvoice.js +224 -223
- package/src/models/facilityInvoicePayment.js +52 -47
- package/src/models/facilityInvoiceRecipient.js +32 -32
- package/src/models/facilityWalletTransactionsMetadata.js +236 -236
- package/src/models/facility_departements.js +20 -20
- package/src/models/facility_payment_details.js +20 -20
- package/src/models/facilityasset.js +25 -25
- package/src/models/faq.js +18 -18
- package/src/models/gl_account_double_entries.js +25 -25
- package/src/models/gl_accounts.js +56 -56
- package/src/models/gl_entries.js +49 -49
- package/src/models/goodsReceivedNotes.js +115 -115
- package/src/models/guard.js +47 -47
- package/src/models/handover.js +247 -247
- package/src/models/inspection_category.js +38 -38
- package/src/models/invoice.js +387 -387
- package/src/models/invoicing_schedule.js +36 -36
- package/src/models/item_inspection.js +96 -96
- package/src/models/knowledge_base.js +109 -109
- package/src/models/knowledge_base_rating.js +44 -44
- package/src/models/leaseagreement.js +243 -243
- package/src/models/leasetemplate.js +17 -17
- package/src/models/levy.js +223 -223
- package/src/models/levy_invoice_settings.js +26 -26
- package/src/models/levycontract.js +173 -173
- package/src/models/levytype.js +23 -23
- package/src/models/maintenance_service_vendor.js +38 -38
- package/src/models/maintenance_services.js +17 -17
- package/src/models/maintenancerequisition.js +31 -31
- package/src/models/master_workplan.js +32 -32
- package/src/models/master_workplan_child.js +34 -34
- package/src/models/message.js +38 -38
- package/src/models/module.js +21 -21
- package/src/models/notification.js +44 -44
- package/src/models/paymentTermsMarks.js +19 -19
- package/src/models/penalty.js +76 -76
- package/src/models/pendingCredentials.js +32 -32
- package/src/models/powerMeterCommunicationProtocol.js +17 -17
- package/src/models/powerMeterCustomerAccount.js +78 -78
- package/src/models/powerMeterCustomerBand.js +14 -14
- package/src/models/powerMeterDailyReading.js +30 -30
- package/src/models/powerMeterGateways.js +40 -40
- package/src/models/powerMeterMonthlyReading.js +34 -34
- package/src/models/powerMeterPowerCharges.js +85 -85
- package/src/models/powerMeterSettings.js +159 -159
- package/src/models/powerMeterSingleDayReading.js +32 -32
- package/src/models/powerMeters.js +116 -116
- package/src/models/powerMetersManufacturer.js +14 -14
- package/src/models/power_meter_account.js +81 -81
- package/src/models/power_meter_command_logs.js +30 -30
- package/src/models/power_meter_command_queue.js +33 -33
- package/src/models/power_meter_negative_balance.js +44 -44
- package/src/models/power_prepaid_credits.js +47 -47
- package/src/models/power_prepaid_debits.js +53 -53
- package/src/models/power_prepaid_orders.js +78 -78
- package/src/models/power_sms_notification.js +26 -26
- package/src/models/propertyManagerContract.js +556 -556
- package/src/models/propertyManagerRevenue.js +195 -195
- package/src/models/purchaseOrderInvoice.js +74 -74
- package/src/models/purchase_order.js +213 -213
- package/src/models/purchase_request.js +110 -110
- package/src/models/refresh_token.js +23 -23
- package/src/models/reminder.js +197 -197
- package/src/models/report.js +13 -13
- package/src/models/resident.js +121 -121
- package/src/models/rfq_details.js +131 -131
- package/src/models/rfq_response.js +153 -153
- package/src/models/service_charge_invoice_upload.js +42 -42
- package/src/models/service_charge_payments.js +27 -27
- package/src/models/servicerequest.js +55 -55
- package/src/models/settings.js +62 -62
- package/src/models/short_urls.js +21 -21
- package/src/models/smart_meter_daily_consumption.js +44 -44
- package/src/models/sms_africastalking.js +20 -20
- package/src/models/sms_balance_notification.js +26 -26
- package/src/models/sms_meliora.js +20 -20
- package/src/models/staff.js +36 -36
- package/src/models/stocksandspare.js +161 -161
- package/src/models/suppliers.js +74 -74
- package/src/models/tickets.js +173 -173
- package/src/models/tickets_category.js +72 -72
- package/src/models/unitManagementTemplate.js +44 -44
- package/src/models/unitasset.js +25 -25
- package/src/models/units.js +118 -118
- package/src/models/user.js +186 -186
- package/src/models/valueaddedservices.js +79 -79
- package/src/models/vas_invoices_upload.js +50 -50
- package/src/models/vas_payments.js +24 -24
- package/src/models/vasinvoice.js +192 -192
- package/src/models/vasvendor.js +57 -57
- package/src/models/visitLog.js +95 -95
- package/src/models/visitor.js +67 -67
- package/src/models/waitlist.js +45 -45
- package/src/models/wallet.js +44 -44
- package/src/models/wallet_transactions.js +50 -50
- package/src/models/water_invoice.js +351 -351
- package/src/models/water_meter_Command_Queue.js +33 -33
- package/src/models/water_meter_account.js +82 -82
- package/src/models/water_meter_billing.js +58 -58
- package/src/models/water_meter_communication.js +17 -17
- package/src/models/water_meter_communication_logs.js +39 -39
- package/src/models/water_meter_concentrator.js +70 -70
- package/src/models/water_meter_daily_history.js +32 -32
- package/src/models/water_meter_high_risk.js +36 -36
- package/src/models/water_meter_iot_cards.js +34 -34
- package/src/models/water_meter_manufacturer.js +35 -35
- package/src/models/water_meter_monthly_history.js +36 -36
- package/src/models/water_meter_negative_amounts.js +44 -44
- package/src/models/water_meter_settings.js +276 -276
- package/src/models/water_meter_single_day_history.js +34 -34
- package/src/models/water_meter_size.js +15 -15
- package/src/models/water_meters.js +133 -133
- package/src/models/water_meters_delivery.js +76 -76
- package/src/models/water_prepaid_credit.js +47 -47
- package/src/models/water_prepaid_debit.js +50 -50
- package/src/models/workorder.js +49 -49
- package/.idea/material_theme_project_new.xml +0 -12
- package/.idea/modules.xml +0 -8
- package/.idea/psdb.iml +0 -12
- package/.idea/vcs.xml +0 -6
package/src/models/invoice.js
CHANGED
|
@@ -1,388 +1,388 @@
|
|
|
1
|
-
const mongoose = require("mongoose");
|
|
2
|
-
|
|
3
|
-
const invoiceSchema = new mongoose.Schema(
|
|
4
|
-
{
|
|
5
|
-
invoiceNumber: {
|
|
6
|
-
type: String,
|
|
7
|
-
required: true,
|
|
8
|
-
unique: true,
|
|
9
|
-
},
|
|
10
|
-
accountNumber: {
|
|
11
|
-
type: String,
|
|
12
|
-
required: true
|
|
13
|
-
},
|
|
14
|
-
client: {
|
|
15
|
-
clientId: {
|
|
16
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
17
|
-
ref: "Customer",
|
|
18
|
-
required: true,
|
|
19
|
-
},
|
|
20
|
-
firstName: {
|
|
21
|
-
type: String,
|
|
22
|
-
required: true
|
|
23
|
-
},
|
|
24
|
-
lastName: {
|
|
25
|
-
type: String,
|
|
26
|
-
required: true
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
facility: {
|
|
30
|
-
id: {
|
|
31
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
32
|
-
ref: "Facility",
|
|
33
|
-
required: true,
|
|
34
|
-
},
|
|
35
|
-
name: {
|
|
36
|
-
type: String,
|
|
37
|
-
required: true
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
unit: {
|
|
41
|
-
id: { type: mongoose.Schema.Types.ObjectId, ref: "Unit", required: true },
|
|
42
|
-
name: { type: String, required: true },
|
|
43
|
-
},
|
|
44
|
-
currency: {
|
|
45
|
-
id: {
|
|
46
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
47
|
-
ref: "Currency",
|
|
48
|
-
required: true
|
|
49
|
-
},
|
|
50
|
-
name: {
|
|
51
|
-
type: String,
|
|
52
|
-
required: true
|
|
53
|
-
},
|
|
54
|
-
code: {
|
|
55
|
-
type: String,
|
|
56
|
-
required: true,
|
|
57
|
-
uppercase: true,
|
|
58
|
-
minlength: 3,
|
|
59
|
-
maxlength: 3
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
|
-
items: [
|
|
63
|
-
{
|
|
64
|
-
description: { type: String, required: true },
|
|
65
|
-
quantity: { type: Number, required: true, min: 1 },
|
|
66
|
-
unitPrice: { type: Number, required: true, min: 0 },
|
|
67
|
-
},
|
|
68
|
-
],
|
|
69
|
-
subTotal: {
|
|
70
|
-
type: Number,
|
|
71
|
-
required: true,
|
|
72
|
-
},
|
|
73
|
-
tax: {
|
|
74
|
-
type: Number,
|
|
75
|
-
required: true,
|
|
76
|
-
},
|
|
77
|
-
totalAmount: {
|
|
78
|
-
type: Number,
|
|
79
|
-
required: true,
|
|
80
|
-
},
|
|
81
|
-
amountPaid: {
|
|
82
|
-
type: Number,
|
|
83
|
-
default: 0,
|
|
84
|
-
min: 0,
|
|
85
|
-
},
|
|
86
|
-
// Mark as deprecated, keep for backward compatibility
|
|
87
|
-
overpay: {
|
|
88
|
-
type: Number,
|
|
89
|
-
default: 0,
|
|
90
|
-
min: 0,
|
|
91
|
-
deprecated: true
|
|
92
|
-
},
|
|
93
|
-
issueDate: {
|
|
94
|
-
type: Date,
|
|
95
|
-
required: true,
|
|
96
|
-
},
|
|
97
|
-
dueDate: {
|
|
98
|
-
type: Date,
|
|
99
|
-
required: true,
|
|
100
|
-
},
|
|
101
|
-
status: {
|
|
102
|
-
type: String,
|
|
103
|
-
required: true,
|
|
104
|
-
enum: ["Unpaid", "Pending", "Paid", "Overdue", "Cancelled", "Partially Paid", "Void"],
|
|
105
|
-
},
|
|
106
|
-
penalty: {
|
|
107
|
-
type: Number,
|
|
108
|
-
default: 0,
|
|
109
|
-
},
|
|
110
|
-
whatFor: {
|
|
111
|
-
invoiceType: { type: String, required: true },
|
|
112
|
-
description: { type: String },
|
|
113
|
-
},
|
|
114
|
-
invoiceNote: {
|
|
115
|
-
type: String,
|
|
116
|
-
default: null,
|
|
117
|
-
},
|
|
118
|
-
balanceBroughtForward: {
|
|
119
|
-
type: Number,
|
|
120
|
-
default: 0,
|
|
121
|
-
},
|
|
122
|
-
// NEW FIELD 1: Year-Month for easy filtering
|
|
123
|
-
yearMonth: {
|
|
124
|
-
type: String,
|
|
125
|
-
match: /^\d{4}-\d{2}$/, // Validates format like "2025-08"
|
|
126
|
-
index: true
|
|
127
|
-
},
|
|
128
|
-
// NEW FIELD 2: Simple notification tracking
|
|
129
|
-
notificationsSent: {
|
|
130
|
-
sms: {
|
|
131
|
-
type: Boolean,
|
|
132
|
-
default: false
|
|
133
|
-
},
|
|
134
|
-
email: {
|
|
135
|
-
type: Boolean,
|
|
136
|
-
default: false
|
|
137
|
-
},
|
|
138
|
-
attempts: {
|
|
139
|
-
type: Number,
|
|
140
|
-
default: 0
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
voidMetadata: {
|
|
144
|
-
voidedBy: {
|
|
145
|
-
userId: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
|
|
146
|
-
name: { type: String },
|
|
147
|
-
role: { type: String }
|
|
148
|
-
},
|
|
149
|
-
voidedAt: { type: Date },
|
|
150
|
-
reason: { type: String }
|
|
151
|
-
},
|
|
152
|
-
cancelMetadata: {
|
|
153
|
-
cancelledBy: {
|
|
154
|
-
userId: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
|
|
155
|
-
name: { type: String },
|
|
156
|
-
role: { type: String }
|
|
157
|
-
},
|
|
158
|
-
cancelledAt: { type: Date },
|
|
159
|
-
reason: { type: String }
|
|
160
|
-
},
|
|
161
|
-
lastReminderSent: Date,
|
|
162
|
-
reminderHistory: [
|
|
163
|
-
{
|
|
164
|
-
sentAt: Date,
|
|
165
|
-
reminderId: mongoose.Schema.Types.ObjectId,
|
|
166
|
-
notificationTypes: [String],
|
|
167
|
-
},
|
|
168
|
-
],
|
|
169
|
-
reconciliationHistory: [{
|
|
170
|
-
date: { type: Date, required: true },
|
|
171
|
-
amount: { type: Number, required: true },
|
|
172
|
-
type: {
|
|
173
|
-
type: String,
|
|
174
|
-
enum: ['payment', 'cash', 'cheque', 'bank-transfer', 'mpesa-transfer', 'overpay-transfer', 'balance-deduction', 'overpay-received', 'credit-forward', 'debit-forward'],
|
|
175
|
-
required: true
|
|
176
|
-
},
|
|
177
|
-
sourceInvoice: String,
|
|
178
|
-
destinationInvoice: String,
|
|
179
|
-
paymentReference: String,
|
|
180
|
-
paymentCompletion: String,
|
|
181
|
-
remainingBalance: Number,
|
|
182
|
-
notes: String,
|
|
183
|
-
exchangeRate: {
|
|
184
|
-
type: Number,
|
|
185
|
-
default: 1 // For cross-currency reconciliations
|
|
186
|
-
},
|
|
187
|
-
originalCurrency: {
|
|
188
|
-
code: String, // Original currency code if different from invoice currency
|
|
189
|
-
amount: Number // Amount in original currency
|
|
190
|
-
}
|
|
191
|
-
}],
|
|
192
|
-
paymentDetails: {
|
|
193
|
-
paymentStatus: { type: String, required: true },
|
|
194
|
-
paymentMethod: { type: String },
|
|
195
|
-
paymentDate: { type: Date },
|
|
196
|
-
transactionId: { type: String },
|
|
197
|
-
},
|
|
198
|
-
// New field to track when an invoice has been viewed
|
|
199
|
-
viewStatus: {
|
|
200
|
-
isOpened: { type: Boolean, default: false },
|
|
201
|
-
openedAt: { type: Date, default: null },
|
|
202
|
-
openedBy: {
|
|
203
|
-
facilityId: { type: mongoose.Schema.Types.ObjectId, ref: "Facility" },
|
|
204
|
-
userId: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
|
|
205
|
-
userRole: { type: String }
|
|
206
|
-
},
|
|
207
|
-
viewHistory: [{
|
|
208
|
-
viewedAt: { type: Date, required: true },
|
|
209
|
-
facilityId: { type: mongoose.Schema.Types.ObjectId, ref: "Facility" },
|
|
210
|
-
userId: { type: mongoose.Schema.Types.ObjectId, ref: "User" }
|
|
211
|
-
}]
|
|
212
|
-
},
|
|
213
|
-
invoiceUrl: {
|
|
214
|
-
type: String,
|
|
215
|
-
default: null
|
|
216
|
-
},
|
|
217
|
-
// New fields for double entry accounts
|
|
218
|
-
invoiceDoubleEntryAccount: {
|
|
219
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
220
|
-
ref: "GLAccountDoubleEntries",
|
|
221
|
-
required: false,
|
|
222
|
-
},
|
|
223
|
-
paymentDoubleEntryAccount: {
|
|
224
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
225
|
-
ref: "GLAccountDoubleEntries",
|
|
226
|
-
required: false,
|
|
227
|
-
},
|
|
228
|
-
// GL account details for invoice entries
|
|
229
|
-
accountdebitedData: {
|
|
230
|
-
amount: { type: Number },
|
|
231
|
-
description: { type: String },
|
|
232
|
-
isActive: { type: Boolean, default: true }
|
|
233
|
-
},
|
|
234
|
-
accountcreditedData: {
|
|
235
|
-
amount: { type: Number },
|
|
236
|
-
description: { type: String },
|
|
237
|
-
isActive: { type: Boolean, default: true }
|
|
238
|
-
}
|
|
239
|
-
},
|
|
240
|
-
{
|
|
241
|
-
timestamps: true,
|
|
242
|
-
}
|
|
243
|
-
);
|
|
244
|
-
|
|
245
|
-
// Add indexes for frequently queried fields
|
|
246
|
-
invoiceSchema.index({ accountNumber: 1 });
|
|
247
|
-
invoiceSchema.index({ status: 1 });
|
|
248
|
-
invoiceSchema.index({ 'client.clientId': 1, status: 1 });
|
|
249
|
-
invoiceSchema.index({ 'reconciliationHistory.paymentReference': 1 });
|
|
250
|
-
invoiceSchema.index({ issueDate: -1 });
|
|
251
|
-
invoiceSchema.index({ 'currency.code': 1 }); // Add index for currency code
|
|
252
|
-
invoiceSchema.index({ 'currency.id': 1 }); // Add index for currency ID
|
|
253
|
-
invoiceSchema.index({ 'currency.code': 1, 'client.clientId': 1, status: 1 }); // Compound index for currency-based queries
|
|
254
|
-
invoiceSchema.index({ 'client.clientId': 1, balanceBroughtForward: 1 }); // Add index for finding invoices with credit balances
|
|
255
|
-
invoiceSchema.index({ 'viewStatus.isOpened': 1 }); // Add index for view status
|
|
256
|
-
invoiceSchema.index({ 'viewStatus.openedBy.facilityId': 1 }); // Add index for facility view tracking
|
|
257
|
-
// Add index for double entry accounts
|
|
258
|
-
invoiceSchema.index({ invoiceDoubleEntryAccount: 1 });
|
|
259
|
-
invoiceSchema.index({ paymentDoubleEntryAccount: 1 });
|
|
260
|
-
// NEW INDEXES for new fields
|
|
261
|
-
invoiceSchema.index({ yearMonth: 1 }); // For filtering by year-month
|
|
262
|
-
invoiceSchema.index({ yearMonth: 1, status: 1 }); // Combined index for monthly reports
|
|
263
|
-
invoiceSchema.index({ 'notificationsSent.sms': 1 }); // For finding SMS sent/not sent
|
|
264
|
-
invoiceSchema.index({ 'notificationsSent.email': 1 }); // For finding email sent/not sent
|
|
265
|
-
|
|
266
|
-
// Add virtual field for calculating balance
|
|
267
|
-
invoiceSchema.virtual('calculatedBalance').get(function () {
|
|
268
|
-
const baseBalance = this.totalAmount - (this.amountPaid || 0);
|
|
269
|
-
|
|
270
|
-
// Add positive balanceBroughtForward (customer owes money)
|
|
271
|
-
if (this.balanceBroughtForward > 0) {
|
|
272
|
-
return baseBalance + this.balanceBroughtForward;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Subtract negative balanceBroughtForward (credit)
|
|
276
|
-
return baseBalance;
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
// Add virtual field for credit balance
|
|
280
|
-
invoiceSchema.virtual('creditBalance').get(function () {
|
|
281
|
-
return this.balanceBroughtForward < 0 ? Math.abs(this.balanceBroughtForward) : 0;
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
// Getter for compatible overpay field
|
|
285
|
-
invoiceSchema.virtual('effectiveOverpay').get(function () {
|
|
286
|
-
return this.balanceBroughtForward < 0 ? Math.abs(this.balanceBroughtForward) : 0;
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
// Add virtual populate for invoice double entry account
|
|
290
|
-
invoiceSchema.virtual('invoiceDoubleEntry', {
|
|
291
|
-
ref: 'GLAccountDoubleEntries',
|
|
292
|
-
localField: 'invoiceDoubleEntryAccount',
|
|
293
|
-
foreignField: '_id',
|
|
294
|
-
justOne: true
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
// Add virtual populate for payment double entry account
|
|
298
|
-
invoiceSchema.virtual('paymentDoubleEntry', {
|
|
299
|
-
ref: 'GLAccountDoubleEntries',
|
|
300
|
-
localField: 'paymentDoubleEntryAccount',
|
|
301
|
-
foreignField: '_id',
|
|
302
|
-
justOne: true
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
// Add method for currency conversion if needed
|
|
306
|
-
invoiceSchema.methods.convertAmount = function (amount, fromCurrency, toCurrency, exchangeRate) {
|
|
307
|
-
if (fromCurrency === toCurrency) {
|
|
308
|
-
return amount;
|
|
309
|
-
}
|
|
310
|
-
return amount * exchangeRate;
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
// Add static method to find invoices by currency
|
|
314
|
-
invoiceSchema.statics.findByCurrency = function (currencyCode) {
|
|
315
|
-
return this.find({ 'currency.code': currencyCode.toUpperCase() });
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
// Add static method to find invoices with credit balance
|
|
319
|
-
invoiceSchema.statics.findWithCreditBalance = function (clientId) {
|
|
320
|
-
return this.find({
|
|
321
|
-
'client.clientId': clientId,
|
|
322
|
-
'balanceBroughtForward': { $lt: 0 }
|
|
323
|
-
}).sort({ updatedAt: -1 });
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
// Add static method to calculate totals by currency
|
|
327
|
-
invoiceSchema.statics.calculateTotalsByCurrency = function (query = {}) {
|
|
328
|
-
return this.aggregate([
|
|
329
|
-
{ $match: query },
|
|
330
|
-
{
|
|
331
|
-
$group: {
|
|
332
|
-
_id: '$currency.code',
|
|
333
|
-
totalAmount: { $sum: '$totalAmount' },
|
|
334
|
-
totalPaid: { $sum: '$amountPaid' },
|
|
335
|
-
count: { $sum: 1 }
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
]);
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
// New static method to find all unviewed invoices
|
|
342
|
-
invoiceSchema.statics.findUnviewedInvoices = function (facilityId) {
|
|
343
|
-
return this.find({
|
|
344
|
-
'facility.id': facilityId,
|
|
345
|
-
'viewStatus.isOpened': false
|
|
346
|
-
});
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
// NEW STATIC METHODS for new fields
|
|
350
|
-
// Find invoices by year-month
|
|
351
|
-
invoiceSchema.statics.findByYearMonth = function (yearMonth) {
|
|
352
|
-
return this.find({ yearMonth: yearMonth });
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
// Find invoices where notifications haven't been sent
|
|
356
|
-
invoiceSchema.statics.findPendingNotifications = function (facilityId) {
|
|
357
|
-
return this.find({
|
|
358
|
-
'facility.id': facilityId,
|
|
359
|
-
status: { $in: ['Unpaid', 'Overdue'] },
|
|
360
|
-
$or: [
|
|
361
|
-
{ 'notificationsSent.sms': false },
|
|
362
|
-
{ 'notificationsSent.email': false }
|
|
363
|
-
]
|
|
364
|
-
});
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
// Pre-save middleware to ensure overpay and balanceBroughtForward stay in sync during transition
|
|
368
|
-
invoiceSchema.pre('save', function (next) {
|
|
369
|
-
// If balanceBroughtForward is negative (credit), sync with overpay for backwards compatibility
|
|
370
|
-
if (this.balanceBroughtForward < 0) {
|
|
371
|
-
this.overpay = Math.abs(this.balanceBroughtForward);
|
|
372
|
-
} else {
|
|
373
|
-
this.overpay = 0; // No overpay if there's no negative balance
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// NEW: Auto-generate yearMonth from issueDate if not provided
|
|
377
|
-
if (!this.yearMonth && this.issueDate) {
|
|
378
|
-
const year = this.issueDate.getFullYear();
|
|
379
|
-
const month = String(this.issueDate.getMonth() + 1).padStart(2, '0');
|
|
380
|
-
this.yearMonth = `${year}-${month}`;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
next();
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
const Invoice = mongoose.model('Invoice', invoiceSchema);
|
|
387
|
-
|
|
1
|
+
const mongoose = require("mongoose");
|
|
2
|
+
|
|
3
|
+
const invoiceSchema = new mongoose.Schema(
|
|
4
|
+
{
|
|
5
|
+
invoiceNumber: {
|
|
6
|
+
type: String,
|
|
7
|
+
required: true,
|
|
8
|
+
unique: true,
|
|
9
|
+
},
|
|
10
|
+
accountNumber: {
|
|
11
|
+
type: String,
|
|
12
|
+
required: true
|
|
13
|
+
},
|
|
14
|
+
client: {
|
|
15
|
+
clientId: {
|
|
16
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
17
|
+
ref: "Customer",
|
|
18
|
+
required: true,
|
|
19
|
+
},
|
|
20
|
+
firstName: {
|
|
21
|
+
type: String,
|
|
22
|
+
required: true
|
|
23
|
+
},
|
|
24
|
+
lastName: {
|
|
25
|
+
type: String,
|
|
26
|
+
required: true
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
facility: {
|
|
30
|
+
id: {
|
|
31
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
32
|
+
ref: "Facility",
|
|
33
|
+
required: true,
|
|
34
|
+
},
|
|
35
|
+
name: {
|
|
36
|
+
type: String,
|
|
37
|
+
required: true
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
unit: {
|
|
41
|
+
id: { type: mongoose.Schema.Types.ObjectId, ref: "Unit", required: true },
|
|
42
|
+
name: { type: String, required: true },
|
|
43
|
+
},
|
|
44
|
+
currency: {
|
|
45
|
+
id: {
|
|
46
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
47
|
+
ref: "Currency",
|
|
48
|
+
required: true
|
|
49
|
+
},
|
|
50
|
+
name: {
|
|
51
|
+
type: String,
|
|
52
|
+
required: true
|
|
53
|
+
},
|
|
54
|
+
code: {
|
|
55
|
+
type: String,
|
|
56
|
+
required: true,
|
|
57
|
+
uppercase: true,
|
|
58
|
+
minlength: 3,
|
|
59
|
+
maxlength: 3
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
items: [
|
|
63
|
+
{
|
|
64
|
+
description: { type: String, required: true },
|
|
65
|
+
quantity: { type: Number, required: true, min: 1 },
|
|
66
|
+
unitPrice: { type: Number, required: true, min: 0 },
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
subTotal: {
|
|
70
|
+
type: Number,
|
|
71
|
+
required: true,
|
|
72
|
+
},
|
|
73
|
+
tax: {
|
|
74
|
+
type: Number,
|
|
75
|
+
required: true,
|
|
76
|
+
},
|
|
77
|
+
totalAmount: {
|
|
78
|
+
type: Number,
|
|
79
|
+
required: true,
|
|
80
|
+
},
|
|
81
|
+
amountPaid: {
|
|
82
|
+
type: Number,
|
|
83
|
+
default: 0,
|
|
84
|
+
min: 0,
|
|
85
|
+
},
|
|
86
|
+
// Mark as deprecated, keep for backward compatibility
|
|
87
|
+
overpay: {
|
|
88
|
+
type: Number,
|
|
89
|
+
default: 0,
|
|
90
|
+
min: 0,
|
|
91
|
+
deprecated: true
|
|
92
|
+
},
|
|
93
|
+
issueDate: {
|
|
94
|
+
type: Date,
|
|
95
|
+
required: true,
|
|
96
|
+
},
|
|
97
|
+
dueDate: {
|
|
98
|
+
type: Date,
|
|
99
|
+
required: true,
|
|
100
|
+
},
|
|
101
|
+
status: {
|
|
102
|
+
type: String,
|
|
103
|
+
required: true,
|
|
104
|
+
enum: ["Unpaid", "Pending", "Paid", "Overdue", "Cancelled", "Partially Paid", "Void"],
|
|
105
|
+
},
|
|
106
|
+
penalty: {
|
|
107
|
+
type: Number,
|
|
108
|
+
default: 0,
|
|
109
|
+
},
|
|
110
|
+
whatFor: {
|
|
111
|
+
invoiceType: { type: String, required: true },
|
|
112
|
+
description: { type: String },
|
|
113
|
+
},
|
|
114
|
+
invoiceNote: {
|
|
115
|
+
type: String,
|
|
116
|
+
default: null,
|
|
117
|
+
},
|
|
118
|
+
balanceBroughtForward: {
|
|
119
|
+
type: Number,
|
|
120
|
+
default: 0,
|
|
121
|
+
},
|
|
122
|
+
// NEW FIELD 1: Year-Month for easy filtering
|
|
123
|
+
yearMonth: {
|
|
124
|
+
type: String,
|
|
125
|
+
match: /^\d{4}-\d{2}$/, // Validates format like "2025-08"
|
|
126
|
+
index: true
|
|
127
|
+
},
|
|
128
|
+
// NEW FIELD 2: Simple notification tracking
|
|
129
|
+
notificationsSent: {
|
|
130
|
+
sms: {
|
|
131
|
+
type: Boolean,
|
|
132
|
+
default: false
|
|
133
|
+
},
|
|
134
|
+
email: {
|
|
135
|
+
type: Boolean,
|
|
136
|
+
default: false
|
|
137
|
+
},
|
|
138
|
+
attempts: {
|
|
139
|
+
type: Number,
|
|
140
|
+
default: 0
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
voidMetadata: {
|
|
144
|
+
voidedBy: {
|
|
145
|
+
userId: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
|
|
146
|
+
name: { type: String },
|
|
147
|
+
role: { type: String }
|
|
148
|
+
},
|
|
149
|
+
voidedAt: { type: Date },
|
|
150
|
+
reason: { type: String }
|
|
151
|
+
},
|
|
152
|
+
cancelMetadata: {
|
|
153
|
+
cancelledBy: {
|
|
154
|
+
userId: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
|
|
155
|
+
name: { type: String },
|
|
156
|
+
role: { type: String }
|
|
157
|
+
},
|
|
158
|
+
cancelledAt: { type: Date },
|
|
159
|
+
reason: { type: String }
|
|
160
|
+
},
|
|
161
|
+
lastReminderSent: Date,
|
|
162
|
+
reminderHistory: [
|
|
163
|
+
{
|
|
164
|
+
sentAt: Date,
|
|
165
|
+
reminderId: mongoose.Schema.Types.ObjectId,
|
|
166
|
+
notificationTypes: [String],
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
reconciliationHistory: [{
|
|
170
|
+
date: { type: Date, required: true },
|
|
171
|
+
amount: { type: Number, required: true },
|
|
172
|
+
type: {
|
|
173
|
+
type: String,
|
|
174
|
+
enum: ['payment', 'cash', 'cheque', 'bank-transfer', 'mpesa-transfer', 'overpay-transfer', 'balance-deduction', 'overpay-received', 'credit-forward', 'debit-forward'],
|
|
175
|
+
required: true
|
|
176
|
+
},
|
|
177
|
+
sourceInvoice: String,
|
|
178
|
+
destinationInvoice: String,
|
|
179
|
+
paymentReference: String,
|
|
180
|
+
paymentCompletion: String,
|
|
181
|
+
remainingBalance: Number,
|
|
182
|
+
notes: String,
|
|
183
|
+
exchangeRate: {
|
|
184
|
+
type: Number,
|
|
185
|
+
default: 1 // For cross-currency reconciliations
|
|
186
|
+
},
|
|
187
|
+
originalCurrency: {
|
|
188
|
+
code: String, // Original currency code if different from invoice currency
|
|
189
|
+
amount: Number // Amount in original currency
|
|
190
|
+
}
|
|
191
|
+
}],
|
|
192
|
+
paymentDetails: {
|
|
193
|
+
paymentStatus: { type: String, required: true },
|
|
194
|
+
paymentMethod: { type: String },
|
|
195
|
+
paymentDate: { type: Date },
|
|
196
|
+
transactionId: { type: String },
|
|
197
|
+
},
|
|
198
|
+
// New field to track when an invoice has been viewed
|
|
199
|
+
viewStatus: {
|
|
200
|
+
isOpened: { type: Boolean, default: false },
|
|
201
|
+
openedAt: { type: Date, default: null },
|
|
202
|
+
openedBy: {
|
|
203
|
+
facilityId: { type: mongoose.Schema.Types.ObjectId, ref: "Facility" },
|
|
204
|
+
userId: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
|
|
205
|
+
userRole: { type: String }
|
|
206
|
+
},
|
|
207
|
+
viewHistory: [{
|
|
208
|
+
viewedAt: { type: Date, required: true },
|
|
209
|
+
facilityId: { type: mongoose.Schema.Types.ObjectId, ref: "Facility" },
|
|
210
|
+
userId: { type: mongoose.Schema.Types.ObjectId, ref: "User" }
|
|
211
|
+
}]
|
|
212
|
+
},
|
|
213
|
+
invoiceUrl: {
|
|
214
|
+
type: String,
|
|
215
|
+
default: null
|
|
216
|
+
},
|
|
217
|
+
// New fields for double entry accounts
|
|
218
|
+
invoiceDoubleEntryAccount: {
|
|
219
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
220
|
+
ref: "GLAccountDoubleEntries",
|
|
221
|
+
required: false,
|
|
222
|
+
},
|
|
223
|
+
paymentDoubleEntryAccount: {
|
|
224
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
225
|
+
ref: "GLAccountDoubleEntries",
|
|
226
|
+
required: false,
|
|
227
|
+
},
|
|
228
|
+
// GL account details for invoice entries
|
|
229
|
+
accountdebitedData: {
|
|
230
|
+
amount: { type: Number },
|
|
231
|
+
description: { type: String },
|
|
232
|
+
isActive: { type: Boolean, default: true }
|
|
233
|
+
},
|
|
234
|
+
accountcreditedData: {
|
|
235
|
+
amount: { type: Number },
|
|
236
|
+
description: { type: String },
|
|
237
|
+
isActive: { type: Boolean, default: true }
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
timestamps: true,
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// Add indexes for frequently queried fields
|
|
246
|
+
invoiceSchema.index({ accountNumber: 1 });
|
|
247
|
+
invoiceSchema.index({ status: 1 });
|
|
248
|
+
invoiceSchema.index({ 'client.clientId': 1, status: 1 });
|
|
249
|
+
invoiceSchema.index({ 'reconciliationHistory.paymentReference': 1 });
|
|
250
|
+
invoiceSchema.index({ issueDate: -1 });
|
|
251
|
+
invoiceSchema.index({ 'currency.code': 1 }); // Add index for currency code
|
|
252
|
+
invoiceSchema.index({ 'currency.id': 1 }); // Add index for currency ID
|
|
253
|
+
invoiceSchema.index({ 'currency.code': 1, 'client.clientId': 1, status: 1 }); // Compound index for currency-based queries
|
|
254
|
+
invoiceSchema.index({ 'client.clientId': 1, balanceBroughtForward: 1 }); // Add index for finding invoices with credit balances
|
|
255
|
+
invoiceSchema.index({ 'viewStatus.isOpened': 1 }); // Add index for view status
|
|
256
|
+
invoiceSchema.index({ 'viewStatus.openedBy.facilityId': 1 }); // Add index for facility view tracking
|
|
257
|
+
// Add index for double entry accounts
|
|
258
|
+
invoiceSchema.index({ invoiceDoubleEntryAccount: 1 });
|
|
259
|
+
invoiceSchema.index({ paymentDoubleEntryAccount: 1 });
|
|
260
|
+
// NEW INDEXES for new fields
|
|
261
|
+
invoiceSchema.index({ yearMonth: 1 }); // For filtering by year-month
|
|
262
|
+
invoiceSchema.index({ yearMonth: 1, status: 1 }); // Combined index for monthly reports
|
|
263
|
+
invoiceSchema.index({ 'notificationsSent.sms': 1 }); // For finding SMS sent/not sent
|
|
264
|
+
invoiceSchema.index({ 'notificationsSent.email': 1 }); // For finding email sent/not sent
|
|
265
|
+
|
|
266
|
+
// Add virtual field for calculating balance
|
|
267
|
+
invoiceSchema.virtual('calculatedBalance').get(function () {
|
|
268
|
+
const baseBalance = this.totalAmount - (this.amountPaid || 0);
|
|
269
|
+
|
|
270
|
+
// Add positive balanceBroughtForward (customer owes money)
|
|
271
|
+
if (this.balanceBroughtForward > 0) {
|
|
272
|
+
return baseBalance + this.balanceBroughtForward;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Subtract negative balanceBroughtForward (credit)
|
|
276
|
+
return baseBalance;
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Add virtual field for credit balance
|
|
280
|
+
invoiceSchema.virtual('creditBalance').get(function () {
|
|
281
|
+
return this.balanceBroughtForward < 0 ? Math.abs(this.balanceBroughtForward) : 0;
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Getter for compatible overpay field
|
|
285
|
+
invoiceSchema.virtual('effectiveOverpay').get(function () {
|
|
286
|
+
return this.balanceBroughtForward < 0 ? Math.abs(this.balanceBroughtForward) : 0;
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Add virtual populate for invoice double entry account
|
|
290
|
+
invoiceSchema.virtual('invoiceDoubleEntry', {
|
|
291
|
+
ref: 'GLAccountDoubleEntries',
|
|
292
|
+
localField: 'invoiceDoubleEntryAccount',
|
|
293
|
+
foreignField: '_id',
|
|
294
|
+
justOne: true
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Add virtual populate for payment double entry account
|
|
298
|
+
invoiceSchema.virtual('paymentDoubleEntry', {
|
|
299
|
+
ref: 'GLAccountDoubleEntries',
|
|
300
|
+
localField: 'paymentDoubleEntryAccount',
|
|
301
|
+
foreignField: '_id',
|
|
302
|
+
justOne: true
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Add method for currency conversion if needed
|
|
306
|
+
invoiceSchema.methods.convertAmount = function (amount, fromCurrency, toCurrency, exchangeRate) {
|
|
307
|
+
if (fromCurrency === toCurrency) {
|
|
308
|
+
return amount;
|
|
309
|
+
}
|
|
310
|
+
return amount * exchangeRate;
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// Add static method to find invoices by currency
|
|
314
|
+
invoiceSchema.statics.findByCurrency = function (currencyCode) {
|
|
315
|
+
return this.find({ 'currency.code': currencyCode.toUpperCase() });
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// Add static method to find invoices with credit balance
|
|
319
|
+
invoiceSchema.statics.findWithCreditBalance = function (clientId) {
|
|
320
|
+
return this.find({
|
|
321
|
+
'client.clientId': clientId,
|
|
322
|
+
'balanceBroughtForward': { $lt: 0 }
|
|
323
|
+
}).sort({ updatedAt: -1 });
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// Add static method to calculate totals by currency
|
|
327
|
+
invoiceSchema.statics.calculateTotalsByCurrency = function (query = {}) {
|
|
328
|
+
return this.aggregate([
|
|
329
|
+
{ $match: query },
|
|
330
|
+
{
|
|
331
|
+
$group: {
|
|
332
|
+
_id: '$currency.code',
|
|
333
|
+
totalAmount: { $sum: '$totalAmount' },
|
|
334
|
+
totalPaid: { $sum: '$amountPaid' },
|
|
335
|
+
count: { $sum: 1 }
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
]);
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// New static method to find all unviewed invoices
|
|
342
|
+
invoiceSchema.statics.findUnviewedInvoices = function (facilityId) {
|
|
343
|
+
return this.find({
|
|
344
|
+
'facility.id': facilityId,
|
|
345
|
+
'viewStatus.isOpened': false
|
|
346
|
+
});
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// NEW STATIC METHODS for new fields
|
|
350
|
+
// Find invoices by year-month
|
|
351
|
+
invoiceSchema.statics.findByYearMonth = function (yearMonth) {
|
|
352
|
+
return this.find({ yearMonth: yearMonth });
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
// Find invoices where notifications haven't been sent
|
|
356
|
+
invoiceSchema.statics.findPendingNotifications = function (facilityId) {
|
|
357
|
+
return this.find({
|
|
358
|
+
'facility.id': facilityId,
|
|
359
|
+
status: { $in: ['Unpaid', 'Overdue'] },
|
|
360
|
+
$or: [
|
|
361
|
+
{ 'notificationsSent.sms': false },
|
|
362
|
+
{ 'notificationsSent.email': false }
|
|
363
|
+
]
|
|
364
|
+
});
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
// Pre-save middleware to ensure overpay and balanceBroughtForward stay in sync during transition
|
|
368
|
+
invoiceSchema.pre('save', function (next) {
|
|
369
|
+
// If balanceBroughtForward is negative (credit), sync with overpay for backwards compatibility
|
|
370
|
+
if (this.balanceBroughtForward < 0) {
|
|
371
|
+
this.overpay = Math.abs(this.balanceBroughtForward);
|
|
372
|
+
} else {
|
|
373
|
+
this.overpay = 0; // No overpay if there's no negative balance
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// NEW: Auto-generate yearMonth from issueDate if not provided
|
|
377
|
+
if (!this.yearMonth && this.issueDate) {
|
|
378
|
+
const year = this.issueDate.getFullYear();
|
|
379
|
+
const month = String(this.issueDate.getMonth() + 1).padStart(2, '0');
|
|
380
|
+
this.yearMonth = `${year}-${month}`;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
next();
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const Invoice = mongoose.model('Invoice', invoiceSchema);
|
|
387
|
+
|
|
388
388
|
module.exports = Invoice;
|