payservedb 8.5.8 → 8.5.9
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/index.js +1 -0
- package/package.json +1 -1
- package/src/models/combined_invoice.js +391 -0
- package/src/models/user.js +5 -2
package/index.js
CHANGED
|
@@ -94,6 +94,7 @@ const models = {
|
|
|
94
94
|
LevyType: require("./src/models/levytype"),
|
|
95
95
|
LevyContract: require("./src/models/levycontract"),
|
|
96
96
|
Invoice: require("./src/models/invoice"),
|
|
97
|
+
CombinedInvoice: require("./src/models/combined_invoice"),
|
|
97
98
|
InvoiceGeneration: require("./src/models/invoice_generation_approval"),
|
|
98
99
|
ShortUrl: require("./src/models/short_urls"),
|
|
99
100
|
InvoicingSchedule: require("./src/models/invoicing_schedule"),
|
package/package.json
CHANGED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
const mongoose = require("mongoose");
|
|
2
|
+
|
|
3
|
+
const combinedInvoiceSchema = new mongoose.Schema(
|
|
4
|
+
{
|
|
5
|
+
// Customer Information
|
|
6
|
+
customer: {
|
|
7
|
+
customerId: {
|
|
8
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
9
|
+
ref: "Customer",
|
|
10
|
+
required: true,
|
|
11
|
+
},
|
|
12
|
+
firstName: {
|
|
13
|
+
type: String,
|
|
14
|
+
required: true,
|
|
15
|
+
},
|
|
16
|
+
lastName: {
|
|
17
|
+
type: String,
|
|
18
|
+
required: true,
|
|
19
|
+
},
|
|
20
|
+
accountNumber: {
|
|
21
|
+
type: String,
|
|
22
|
+
required: true,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
// Unit Information
|
|
27
|
+
unit: {
|
|
28
|
+
id: {
|
|
29
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
30
|
+
ref: "Unit",
|
|
31
|
+
required: true
|
|
32
|
+
},
|
|
33
|
+
name: {
|
|
34
|
+
type: String,
|
|
35
|
+
required: true
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
// Facility Information
|
|
40
|
+
facility: {
|
|
41
|
+
id: {
|
|
42
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
43
|
+
ref: "Facility",
|
|
44
|
+
required: true,
|
|
45
|
+
},
|
|
46
|
+
name: {
|
|
47
|
+
type: String,
|
|
48
|
+
required: true,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
// Currency Information
|
|
53
|
+
currency: {
|
|
54
|
+
id: {
|
|
55
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
56
|
+
ref: "Currency",
|
|
57
|
+
required: true,
|
|
58
|
+
},
|
|
59
|
+
name: {
|
|
60
|
+
type: String,
|
|
61
|
+
required: true,
|
|
62
|
+
},
|
|
63
|
+
code: {
|
|
64
|
+
type: String,
|
|
65
|
+
required: true,
|
|
66
|
+
uppercase: true,
|
|
67
|
+
minlength: 3,
|
|
68
|
+
maxlength: 3,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
// Billing Period (Year-Month format)
|
|
73
|
+
period: {
|
|
74
|
+
type: String,
|
|
75
|
+
required: true,
|
|
76
|
+
match: /^\d{4}-\d{2}$/, // Format: "2025-01"
|
|
77
|
+
index: true,
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// Issue Date
|
|
81
|
+
issueDate: {
|
|
82
|
+
type: Date,
|
|
83
|
+
required: true,
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// Due Date (can be based on the latest invoice due date)
|
|
87
|
+
dueDate: {
|
|
88
|
+
type: Date,
|
|
89
|
+
required: true,
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
// Overall Status
|
|
93
|
+
status: {
|
|
94
|
+
type: String,
|
|
95
|
+
required: true,
|
|
96
|
+
enum: [
|
|
97
|
+
"Unpaid",
|
|
98
|
+
"Pending",
|
|
99
|
+
"Paid",
|
|
100
|
+
"Overdue",
|
|
101
|
+
"Cancelled",
|
|
102
|
+
"Partially Paid",
|
|
103
|
+
"Void",
|
|
104
|
+
],
|
|
105
|
+
default: "Unpaid",
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
// Total Amount (sum of all individual invoices)
|
|
109
|
+
totalAmount: {
|
|
110
|
+
type: Number,
|
|
111
|
+
required: true,
|
|
112
|
+
default: 0,
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
// Total Tax (sum of all individual invoice taxes)
|
|
116
|
+
totalTax: {
|
|
117
|
+
type: Number,
|
|
118
|
+
required: true,
|
|
119
|
+
default: 0,
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
// Total Amount Paid
|
|
123
|
+
amountPaid: {
|
|
124
|
+
type: Number,
|
|
125
|
+
default: 0,
|
|
126
|
+
min: 0,
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
// Total Balance Brought Forward
|
|
130
|
+
totalBalanceBroughtForward: {
|
|
131
|
+
type: Number,
|
|
132
|
+
default: 0,
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
// Array of Individual Invoices
|
|
136
|
+
invoices: [
|
|
137
|
+
{
|
|
138
|
+
invoiceId: {
|
|
139
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
140
|
+
ref: "Invoice",
|
|
141
|
+
required: true,
|
|
142
|
+
},
|
|
143
|
+
invoiceNumber: {
|
|
144
|
+
type: String,
|
|
145
|
+
required: true,
|
|
146
|
+
},
|
|
147
|
+
type: {
|
|
148
|
+
type: String,
|
|
149
|
+
required: true,
|
|
150
|
+
enum: ["contract", "lease", "water", "power", "other"],
|
|
151
|
+
},
|
|
152
|
+
description: {
|
|
153
|
+
type: String,
|
|
154
|
+
required: true,
|
|
155
|
+
},
|
|
156
|
+
period: {
|
|
157
|
+
type: String,
|
|
158
|
+
required: true,
|
|
159
|
+
match: /^\d{4}-\d{2}$/,
|
|
160
|
+
},
|
|
161
|
+
amount: {
|
|
162
|
+
type: Number,
|
|
163
|
+
required: true,
|
|
164
|
+
min: 0,
|
|
165
|
+
},
|
|
166
|
+
tax: {
|
|
167
|
+
type: Number,
|
|
168
|
+
required: true,
|
|
169
|
+
default: 0,
|
|
170
|
+
},
|
|
171
|
+
balanceBroughtForward: {
|
|
172
|
+
type: Number,
|
|
173
|
+
default: 0,
|
|
174
|
+
},
|
|
175
|
+
totalAmount: {
|
|
176
|
+
type: Number,
|
|
177
|
+
required: true,
|
|
178
|
+
},
|
|
179
|
+
amountPaid: {
|
|
180
|
+
type: Number,
|
|
181
|
+
default: 0,
|
|
182
|
+
},
|
|
183
|
+
status: {
|
|
184
|
+
type: String,
|
|
185
|
+
required: true,
|
|
186
|
+
enum: [
|
|
187
|
+
"Unpaid",
|
|
188
|
+
"Pending",
|
|
189
|
+
"Paid",
|
|
190
|
+
"Overdue",
|
|
191
|
+
"Cancelled",
|
|
192
|
+
"Partially Paid",
|
|
193
|
+
"Void",
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
issueDate: {
|
|
197
|
+
type: Date,
|
|
198
|
+
required: true,
|
|
199
|
+
},
|
|
200
|
+
dueDate: {
|
|
201
|
+
type: Date,
|
|
202
|
+
required: true,
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
|
|
207
|
+
// Notification tracking for the combined invoice
|
|
208
|
+
notificationsSent: {
|
|
209
|
+
sms: {
|
|
210
|
+
type: Boolean,
|
|
211
|
+
default: false,
|
|
212
|
+
},
|
|
213
|
+
email: {
|
|
214
|
+
type: Boolean,
|
|
215
|
+
default: false,
|
|
216
|
+
},
|
|
217
|
+
attempts: {
|
|
218
|
+
type: Number,
|
|
219
|
+
default: 0,
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
// Last reminder sent
|
|
224
|
+
lastReminderSent: Date,
|
|
225
|
+
|
|
226
|
+
// Combined invoice notes
|
|
227
|
+
invoiceNote: {
|
|
228
|
+
type: String,
|
|
229
|
+
default: null,
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
// View status tracking
|
|
233
|
+
viewStatus: {
|
|
234
|
+
isOpened: { type: Boolean, default: false },
|
|
235
|
+
openedAt: { type: Date, default: null },
|
|
236
|
+
openedBy: {
|
|
237
|
+
facilityId: { type: mongoose.Schema.Types.ObjectId, ref: "Facility" },
|
|
238
|
+
userId: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
|
|
239
|
+
userRole: { type: String },
|
|
240
|
+
},
|
|
241
|
+
viewHistory: [
|
|
242
|
+
{
|
|
243
|
+
viewedAt: { type: Date, required: true },
|
|
244
|
+
facilityId: { type: mongoose.Schema.Types.ObjectId, ref: "Facility" },
|
|
245
|
+
userId: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
// Combined invoice URL (if generated)
|
|
251
|
+
invoiceUrl: {
|
|
252
|
+
type: String,
|
|
253
|
+
default: null,
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
timestamps: true,
|
|
258
|
+
}
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
// Indexes for efficient querying
|
|
262
|
+
combinedInvoiceSchema.index({ "customer.customerId": 1, period: 1 });
|
|
263
|
+
combinedInvoiceSchema.index({ "unit.id": 1, period: 1 });
|
|
264
|
+
combinedInvoiceSchema.index({ "facility.id": 1, period: 1 });
|
|
265
|
+
combinedInvoiceSchema.index({ status: 1 });
|
|
266
|
+
combinedInvoiceSchema.index({ period: 1 });
|
|
267
|
+
combinedInvoiceSchema.index({ issueDate: -1 });
|
|
268
|
+
combinedInvoiceSchema.index({ "customer.customerId": 1, status: 1 });
|
|
269
|
+
combinedInvoiceSchema.index({ "invoices.invoiceId": 1 });
|
|
270
|
+
combinedInvoiceSchema.index({ "currency.code": 1 });
|
|
271
|
+
|
|
272
|
+
// Compound index for unique combined invoice per customer/unit/facility/period
|
|
273
|
+
combinedInvoiceSchema.index(
|
|
274
|
+
{
|
|
275
|
+
"customer.customerId": 1,
|
|
276
|
+
"unit.id": 1,
|
|
277
|
+
"facility.id": 1,
|
|
278
|
+
period: 1
|
|
279
|
+
},
|
|
280
|
+
{ unique: true }
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
// Virtual field for calculating total balance
|
|
284
|
+
combinedInvoiceSchema.virtual("calculatedBalance").get(function () {
|
|
285
|
+
return this.totalAmount - (this.amountPaid || 0) + (this.totalBalanceBroughtForward || 0);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Virtual field for invoice count
|
|
289
|
+
combinedInvoiceSchema.virtual("invoiceCount").get(function () {
|
|
290
|
+
return this.invoices ? this.invoices.length : 0;
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Static method to find combined invoice by period and customer
|
|
294
|
+
combinedInvoiceSchema.statics.findByCustomerAndPeriod = function (customerId, period) {
|
|
295
|
+
return this.findOne({
|
|
296
|
+
"customer.customerId": customerId,
|
|
297
|
+
period: period,
|
|
298
|
+
});
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// Static method to find combined invoices by facility and period
|
|
302
|
+
combinedInvoiceSchema.statics.findByFacilityAndPeriod = function (facilityId, period) {
|
|
303
|
+
return this.find({
|
|
304
|
+
"facility.id": facilityId,
|
|
305
|
+
period: period,
|
|
306
|
+
});
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// Static method to find all combined invoices for a customer
|
|
310
|
+
combinedInvoiceSchema.statics.findByCustomer = function (customerId) {
|
|
311
|
+
return this.find({
|
|
312
|
+
"customer.customerId": customerId,
|
|
313
|
+
}).sort({ period: -1 });
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// Static method to find pending notifications
|
|
317
|
+
combinedInvoiceSchema.statics.findPendingNotifications = function (facilityId) {
|
|
318
|
+
return this.find({
|
|
319
|
+
"facility.id": facilityId,
|
|
320
|
+
status: { $in: ["Unpaid", "Overdue"] },
|
|
321
|
+
$or: [
|
|
322
|
+
{ "notificationsSent.sms": false },
|
|
323
|
+
{ "notificationsSent.email": false },
|
|
324
|
+
],
|
|
325
|
+
});
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// Method to update status based on individual invoices
|
|
329
|
+
combinedInvoiceSchema.methods.updateOverallStatus = function () {
|
|
330
|
+
if (!this.invoices || this.invoices.length === 0) {
|
|
331
|
+
this.status = "Unpaid";
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const statuses = this.invoices.map(inv => inv.status);
|
|
336
|
+
|
|
337
|
+
// If all paid, mark as paid
|
|
338
|
+
if (statuses.every(s => s === "Paid")) {
|
|
339
|
+
this.status = "Paid";
|
|
340
|
+
}
|
|
341
|
+
// If any overdue, mark as overdue
|
|
342
|
+
else if (statuses.some(s => s === "Overdue")) {
|
|
343
|
+
this.status = "Overdue";
|
|
344
|
+
}
|
|
345
|
+
// If any partially paid, mark as partially paid
|
|
346
|
+
else if (statuses.some(s => s === "Partially Paid")) {
|
|
347
|
+
this.status = "Partially Paid";
|
|
348
|
+
}
|
|
349
|
+
// If all cancelled, mark as cancelled
|
|
350
|
+
else if (statuses.every(s => s === "Cancelled")) {
|
|
351
|
+
this.status = "Cancelled";
|
|
352
|
+
}
|
|
353
|
+
// If all void, mark as void
|
|
354
|
+
else if (statuses.every(s => s === "Void")) {
|
|
355
|
+
this.status = "Void";
|
|
356
|
+
}
|
|
357
|
+
// Otherwise unpaid
|
|
358
|
+
else {
|
|
359
|
+
this.status = "Unpaid";
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// Method to recalculate totals from individual invoices
|
|
364
|
+
combinedInvoiceSchema.methods.recalculateTotals = function () {
|
|
365
|
+
if (!this.invoices || this.invoices.length === 0) {
|
|
366
|
+
this.totalAmount = 0;
|
|
367
|
+
this.totalTax = 0;
|
|
368
|
+
this.amountPaid = 0;
|
|
369
|
+
this.totalBalanceBroughtForward = 0;
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
this.totalAmount = this.invoices.reduce((sum, inv) => sum + (inv.totalAmount || 0), 0);
|
|
374
|
+
this.totalTax = this.invoices.reduce((sum, inv) => sum + (inv.tax || 0), 0);
|
|
375
|
+
this.amountPaid = this.invoices.reduce((sum, inv) => sum + (inv.amountPaid || 0), 0);
|
|
376
|
+
this.totalBalanceBroughtForward = this.invoices.reduce(
|
|
377
|
+
(sum, inv) => sum + (inv.balanceBroughtForward || 0),
|
|
378
|
+
0
|
|
379
|
+
);
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
// Pre-save middleware to auto-calculate totals and status
|
|
383
|
+
combinedInvoiceSchema.pre("save", function (next) {
|
|
384
|
+
this.recalculateTotals();
|
|
385
|
+
this.updateOverallStatus();
|
|
386
|
+
next();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const CombinedInvoice = mongoose.model("CombinedInvoice", combinedInvoiceSchema);
|
|
390
|
+
|
|
391
|
+
module.exports = CombinedInvoice;
|
package/src/models/user.js
CHANGED
|
@@ -22,7 +22,7 @@ const userSchema = new mongoose.Schema({
|
|
|
22
22
|
type: String,
|
|
23
23
|
required: false
|
|
24
24
|
},
|
|
25
|
-
|
|
25
|
+
type: {
|
|
26
26
|
type: String,
|
|
27
27
|
required: [true, 'Type is required'],
|
|
28
28
|
enum: ['Company', 'Project Manager', 'Universal', 'Core', 'Resident', 'Landlord', 'Supplier', 'Customer_Support'],
|
|
@@ -126,7 +126,6 @@ const userSchema = new mongoose.Schema({
|
|
|
126
126
|
}
|
|
127
127
|
},
|
|
128
128
|
kyc: {
|
|
129
|
-
|
|
130
129
|
Id: {
|
|
131
130
|
type: String
|
|
132
131
|
}
|
|
@@ -135,6 +134,10 @@ const userSchema = new mongoose.Schema({
|
|
|
135
134
|
type: Boolean,
|
|
136
135
|
required: false
|
|
137
136
|
},
|
|
137
|
+
combineInvoices: {
|
|
138
|
+
type: Boolean,
|
|
139
|
+
default: false
|
|
140
|
+
},
|
|
138
141
|
companies: [{
|
|
139
142
|
type: mongoose.Schema.Types.ObjectId,
|
|
140
143
|
ref: 'Company',
|