payservedb 8.9.1 → 8.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +1 -0
- package/package.json +1 -1
- package/src/models/leaseagreement.js +6 -0
- package/src/models/levycontract.js +6 -4
- package/src/models/power_invoice.js +471 -0
package/index.js
CHANGED
|
@@ -196,6 +196,7 @@ const models = {
|
|
|
196
196
|
FacilityDepartment: require("./src/models/facility_departements"),
|
|
197
197
|
EmailSmsQueue: require("./src/models/email_sms_queue"),
|
|
198
198
|
PurchaseOrderInvoice: require("./src/models/purchaseOrderInvoice"),
|
|
199
|
+
PowerInvoice: require("./src/models/power_invoice"),
|
|
199
200
|
PowerMeterCustomerBand: require("./src/models/powerMeterCustomerBand"),
|
|
200
201
|
PowerMeterCommunicationProtocol: require("./src/models/powerMeterCommunicationProtocol"),
|
|
201
202
|
PowerMeterDailyReading: require("./src/models/powerMeterDailyReading"),
|
package/package.json
CHANGED
|
@@ -105,6 +105,12 @@ const leaseAgreementSchema = new mongoose.Schema({
|
|
|
105
105
|
securityDeposit: { type: Number, required: true },
|
|
106
106
|
balanceBroughtForward: { type: Number, required: true, default: 0 },
|
|
107
107
|
taxEnabled: { type: Boolean, default: false },
|
|
108
|
+
enabledTaxes: [
|
|
109
|
+
{
|
|
110
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
111
|
+
ref: 'CountryTaxRate'
|
|
112
|
+
}
|
|
113
|
+
],
|
|
108
114
|
penaltyId: {
|
|
109
115
|
type: mongoose.Schema.Types.ObjectId,
|
|
110
116
|
ref: 'Penalty'
|
|
@@ -49,10 +49,12 @@ const LevyContractSchema = new Schema(
|
|
|
49
49
|
type: Boolean,
|
|
50
50
|
default: true
|
|
51
51
|
},
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
enabledTaxes: [
|
|
53
|
+
{
|
|
54
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
55
|
+
ref: 'CountryTaxRate'
|
|
56
|
+
}
|
|
57
|
+
],
|
|
56
58
|
paymentFrequency: {
|
|
57
59
|
type: String,
|
|
58
60
|
enum: ['Daily', 'Weekly', 'Bi-Weekly', 'Monthly', 'Quarterly', 'Semi-Annually', 'Annually'],
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
|
|
3
|
+
const powerInvoiceSchema = new mongoose.Schema(
|
|
4
|
+
{
|
|
5
|
+
// ── Basic Identifiers ──────────────────────────────────────────────────────
|
|
6
|
+
invoiceNumber: {
|
|
7
|
+
type: String,
|
|
8
|
+
required: true,
|
|
9
|
+
unique: true,
|
|
10
|
+
},
|
|
11
|
+
accountNumber: {
|
|
12
|
+
type: String,
|
|
13
|
+
required: true,
|
|
14
|
+
},
|
|
15
|
+
unitName: {
|
|
16
|
+
type: String,
|
|
17
|
+
required: true,
|
|
18
|
+
},
|
|
19
|
+
yearMonth: {
|
|
20
|
+
type: String,
|
|
21
|
+
required: true,
|
|
22
|
+
// Format: "YYYY-MM" e.g. "2025-03"
|
|
23
|
+
},
|
|
24
|
+
facilityId: {
|
|
25
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
26
|
+
required: true,
|
|
27
|
+
},
|
|
28
|
+
customerId: {
|
|
29
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
30
|
+
required: true,
|
|
31
|
+
},
|
|
32
|
+
billingType: {
|
|
33
|
+
type: String,
|
|
34
|
+
enum: ['postpaid'],
|
|
35
|
+
default: 'postpaid',
|
|
36
|
+
},
|
|
37
|
+
balanceBroughtForward: {
|
|
38
|
+
type: Number,
|
|
39
|
+
default: 0,
|
|
40
|
+
// Positive = arrears carried forward; Negative = credit from overpayment
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
// ── Payment Methods ────────────────────────────────────────────────────────
|
|
44
|
+
paymentMethods: {
|
|
45
|
+
mobilePayment: {
|
|
46
|
+
status: {
|
|
47
|
+
type: Boolean,
|
|
48
|
+
default: false,
|
|
49
|
+
},
|
|
50
|
+
paymentId: {
|
|
51
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
52
|
+
ref: 'FacilityPaymentDetails',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
bankPayment: {
|
|
56
|
+
status: {
|
|
57
|
+
type: Boolean,
|
|
58
|
+
default: false,
|
|
59
|
+
},
|
|
60
|
+
paymentId: {
|
|
61
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
62
|
+
ref: 'BankDetails',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
cashPayment: {
|
|
66
|
+
status: {
|
|
67
|
+
type: Boolean,
|
|
68
|
+
default: false,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
paymentMethod: {
|
|
73
|
+
type: String,
|
|
74
|
+
enum: ['mobile', 'bank', 'cash', 'mixed'],
|
|
75
|
+
default: null,
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
// ── Biller Address ─────────────────────────────────────────────────────────
|
|
79
|
+
billerAddress: {
|
|
80
|
+
name: {
|
|
81
|
+
type: String,
|
|
82
|
+
required: [true, 'Biller name is required'],
|
|
83
|
+
trim: true,
|
|
84
|
+
minlength: [1, 'Biller name must be at least 1 character long'],
|
|
85
|
+
},
|
|
86
|
+
email: {
|
|
87
|
+
type: String,
|
|
88
|
+
trim: true,
|
|
89
|
+
lowercase: true,
|
|
90
|
+
validate: {
|
|
91
|
+
validator: function (v) {
|
|
92
|
+
return !v || /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(v);
|
|
93
|
+
},
|
|
94
|
+
message: 'Please enter a valid email address',
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
phone: {
|
|
98
|
+
type: String,
|
|
99
|
+
required: [true, 'Biller phone is required'],
|
|
100
|
+
trim: true,
|
|
101
|
+
},
|
|
102
|
+
address: {
|
|
103
|
+
type: String,
|
|
104
|
+
required: [true, 'Biller address is required'],
|
|
105
|
+
trim: true,
|
|
106
|
+
},
|
|
107
|
+
city: {
|
|
108
|
+
type: String,
|
|
109
|
+
trim: true,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
// ── Dates ──────────────────────────────────────────────────────────────────
|
|
114
|
+
dateIssued: {
|
|
115
|
+
type: Date,
|
|
116
|
+
default: Date.now,
|
|
117
|
+
},
|
|
118
|
+
dueDate: {
|
|
119
|
+
type: Date,
|
|
120
|
+
required: true,
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
// ── Customer Info (denormalised snapshot) ──────────────────────────────────
|
|
124
|
+
CustomerInfo: {
|
|
125
|
+
fullName: {
|
|
126
|
+
type: String,
|
|
127
|
+
required: true,
|
|
128
|
+
trim: true,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
// ── Meter & Consumption ────────────────────────────────────────────────────
|
|
133
|
+
meterNumber: {
|
|
134
|
+
type: String,
|
|
135
|
+
required: true,
|
|
136
|
+
},
|
|
137
|
+
tariff: {
|
|
138
|
+
type: String,
|
|
139
|
+
required: true,
|
|
140
|
+
// e.g. "Domestic Lifeline", "Domestic Ordinary 1", "Domestic Ordinary 2",
|
|
141
|
+
// "Commercial Low Voltage", "Industrial Low Voltage"
|
|
142
|
+
},
|
|
143
|
+
meterReadings: {
|
|
144
|
+
previousReading: {
|
|
145
|
+
type: Number,
|
|
146
|
+
required: true,
|
|
147
|
+
},
|
|
148
|
+
currentReading: {
|
|
149
|
+
type: Number,
|
|
150
|
+
required: true,
|
|
151
|
+
},
|
|
152
|
+
usage: {
|
|
153
|
+
type: Number,
|
|
154
|
+
required: true,
|
|
155
|
+
// kWh consumed = currentReading - previousReading
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
consumptionPeriod: {
|
|
159
|
+
startDate: {
|
|
160
|
+
type: Date,
|
|
161
|
+
required: true,
|
|
162
|
+
},
|
|
163
|
+
endDate: {
|
|
164
|
+
type: Date,
|
|
165
|
+
required: true,
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
// ── Power Charges ──────────────────────────────────────────────────────────
|
|
170
|
+
// All monetary values are in the invoice currency (default KES).
|
|
171
|
+
powerCharges: {
|
|
172
|
+
tariff: {
|
|
173
|
+
type: String,
|
|
174
|
+
// Mirror of the top-level tariff field; stored here for invoice snapshot
|
|
175
|
+
},
|
|
176
|
+
yearMonth: {
|
|
177
|
+
type: String,
|
|
178
|
+
// Mirror of the top-level yearMonth; stored for historical rate lookups
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
// Core energy charge based on units consumed and applicable tariff block
|
|
182
|
+
energyCharge: {
|
|
183
|
+
type: Number,
|
|
184
|
+
required: true,
|
|
185
|
+
default: 0,
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
// Fuel Cost Charge (FCC) – passed through from EPRA-approved fuel levy
|
|
189
|
+
fuelCostCharge: {
|
|
190
|
+
type: Number,
|
|
191
|
+
default: 0,
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
// Foreign Exchange Fluctuation Adjustment
|
|
195
|
+
forexAdjustment: {
|
|
196
|
+
type: Number,
|
|
197
|
+
default: 0,
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
// Inflation Adjustment Charge
|
|
201
|
+
inflationAdjustment: {
|
|
202
|
+
type: Number,
|
|
203
|
+
default: 0,
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
// Water Resource Management Levy (WRML)
|
|
207
|
+
waterResourceManagementLevy: {
|
|
208
|
+
type: Number,
|
|
209
|
+
default: 0,
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
// Energy Regulatory Levy (ERL)
|
|
213
|
+
energyRegulatoryLevy: {
|
|
214
|
+
type: Number,
|
|
215
|
+
default: 0,
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
// Rural Electrification & Renewable Energy Levy (REL)
|
|
219
|
+
ruralElectrificationLevy: {
|
|
220
|
+
type: Number,
|
|
221
|
+
default: 0,
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
// Power Factor Surcharge (applied to commercial/industrial customers only)
|
|
225
|
+
powerFactorSurcharge: {
|
|
226
|
+
type: Number,
|
|
227
|
+
default: 0,
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
// Value Added Tax amount
|
|
231
|
+
valueAddedTax: {
|
|
232
|
+
type: Number,
|
|
233
|
+
default: 0,
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
// VAT rate applied (%) – captured at time of billing; typically 16
|
|
237
|
+
vatPercentage: {
|
|
238
|
+
type: Number,
|
|
239
|
+
default: 16,
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
// Rural Electrification Programme levy percentage (%) – typically 5
|
|
243
|
+
repPercentage: {
|
|
244
|
+
type: Number,
|
|
245
|
+
default: 5,
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
// Grand total = sum of all charges above
|
|
249
|
+
totalCharge: {
|
|
250
|
+
type: Number,
|
|
251
|
+
required: true,
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
// ── Payment & Reconciliation ───────────────────────────────────────────────
|
|
256
|
+
amountPaid: {
|
|
257
|
+
type: Number,
|
|
258
|
+
default: 0,
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
// ── Invoice Note & Status ──────────────────────────────────────────────────
|
|
262
|
+
invoiceNote: {
|
|
263
|
+
type: String,
|
|
264
|
+
default: 'Payment is due within 10 days',
|
|
265
|
+
},
|
|
266
|
+
status: {
|
|
267
|
+
type: String,
|
|
268
|
+
required: true,
|
|
269
|
+
enum: ['Pending', 'Paid', 'Partially Paid', 'Cancelled', 'Overdue', 'Unpaid'],
|
|
270
|
+
default: 'Unpaid',
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
// ── View / Read Status
|
|
274
|
+
viewStatus: {
|
|
275
|
+
isOpened: {
|
|
276
|
+
type: Boolean,
|
|
277
|
+
default: false,
|
|
278
|
+
},
|
|
279
|
+
openedAt: {
|
|
280
|
+
type: Date,
|
|
281
|
+
default: null,
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
// ── Currency
|
|
286
|
+
currency: {
|
|
287
|
+
id: {
|
|
288
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
289
|
+
},
|
|
290
|
+
name: {
|
|
291
|
+
type: String,
|
|
292
|
+
default: 'Kenyan Shilling',
|
|
293
|
+
},
|
|
294
|
+
code: {
|
|
295
|
+
type: String,
|
|
296
|
+
default: 'KES',
|
|
297
|
+
},
|
|
298
|
+
symbol: {
|
|
299
|
+
type: String,
|
|
300
|
+
default: 'KSh',
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
// ── Notification Tracking
|
|
305
|
+
notificationsSent: {
|
|
306
|
+
type: {
|
|
307
|
+
sms: {
|
|
308
|
+
type: Boolean,
|
|
309
|
+
default: false,
|
|
310
|
+
},
|
|
311
|
+
email: {
|
|
312
|
+
type: Boolean,
|
|
313
|
+
default: false,
|
|
314
|
+
},
|
|
315
|
+
sentAt: {
|
|
316
|
+
type: Date,
|
|
317
|
+
default: null,
|
|
318
|
+
},
|
|
319
|
+
lastAttempt: {
|
|
320
|
+
type: Date,
|
|
321
|
+
default: null,
|
|
322
|
+
},
|
|
323
|
+
attempts: {
|
|
324
|
+
type: Number,
|
|
325
|
+
default: 0,
|
|
326
|
+
},
|
|
327
|
+
smsDetails: {
|
|
328
|
+
type: {
|
|
329
|
+
success: Boolean,
|
|
330
|
+
error: String,
|
|
331
|
+
sentAt: Date,
|
|
332
|
+
phoneNumber: String,
|
|
333
|
+
},
|
|
334
|
+
default: null,
|
|
335
|
+
},
|
|
336
|
+
emailDetails: {
|
|
337
|
+
type: {
|
|
338
|
+
success: Boolean,
|
|
339
|
+
error: String,
|
|
340
|
+
sentAt: Date,
|
|
341
|
+
emailAddress: String,
|
|
342
|
+
},
|
|
343
|
+
default: null,
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
default: {
|
|
347
|
+
sms: false,
|
|
348
|
+
email: false,
|
|
349
|
+
sentAt: null,
|
|
350
|
+
lastAttempt: null,
|
|
351
|
+
attempts: 0,
|
|
352
|
+
smsDetails: null,
|
|
353
|
+
emailDetails: null,
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
// Reconciliation History
|
|
358
|
+
reconciliationHistory: [
|
|
359
|
+
{
|
|
360
|
+
date: {
|
|
361
|
+
type: Date,
|
|
362
|
+
default: Date.now,
|
|
363
|
+
},
|
|
364
|
+
amount: {
|
|
365
|
+
type: Number,
|
|
366
|
+
required: true,
|
|
367
|
+
},
|
|
368
|
+
type: {
|
|
369
|
+
type: String,
|
|
370
|
+
default: 'Manual',
|
|
371
|
+
// e.g. 'Manual', 'M-Pesa', 'Bank Transfer', 'Cash'
|
|
372
|
+
},
|
|
373
|
+
paymentReference: {
|
|
374
|
+
type: String,
|
|
375
|
+
},
|
|
376
|
+
paymentCompletion: {
|
|
377
|
+
type: String,
|
|
378
|
+
default: 'Completed',
|
|
379
|
+
},
|
|
380
|
+
sourceInvoice: {
|
|
381
|
+
type: String,
|
|
382
|
+
},
|
|
383
|
+
destinationInvoice: {
|
|
384
|
+
type: String,
|
|
385
|
+
},
|
|
386
|
+
notes: {
|
|
387
|
+
type: String,
|
|
388
|
+
},
|
|
389
|
+
remainingBalance: {
|
|
390
|
+
type: Number,
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
timestamps: true,
|
|
397
|
+
}
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
// Pre-save middleware
|
|
401
|
+
|
|
402
|
+
powerInvoiceSchema.pre('save', function (next) {
|
|
403
|
+
// 1. Auto-mark overdue
|
|
404
|
+
if (this.isModified('dueDate') || this.isNew) {
|
|
405
|
+
const today = new Date();
|
|
406
|
+
if (this.dueDate < today && this.status === 'Pending') {
|
|
407
|
+
this.status = 'Overdue';
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// 2. Sync powerCharges snapshot fields with top-level fields
|
|
412
|
+
if (this.isNew || this.isModified('tariff')) {
|
|
413
|
+
if (this.powerCharges && !this.powerCharges.tariff) {
|
|
414
|
+
this.powerCharges.tariff = this.tariff;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (this.isNew || this.isModified('yearMonth')) {
|
|
418
|
+
if (this.powerCharges && !this.powerCharges.yearMonth) {
|
|
419
|
+
this.powerCharges.yearMonth = this.yearMonth;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// 3. Update payment method flags from reconciliation history
|
|
424
|
+
if (this.isModified('reconciliationHistory') || this.isNew) {
|
|
425
|
+
if (this.reconciliationHistory && this.reconciliationHistory.length > 0) {
|
|
426
|
+
const latestPayment = this.reconciliationHistory[this.reconciliationHistory.length - 1];
|
|
427
|
+
const paymentType = latestPayment.type?.toLowerCase();
|
|
428
|
+
|
|
429
|
+
if (!this.paymentMethods) {
|
|
430
|
+
this.paymentMethods = {
|
|
431
|
+
mobilePayment: { status: false },
|
|
432
|
+
bankPayment: { status: false },
|
|
433
|
+
cashPayment: { status: false },
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (
|
|
438
|
+
paymentType &&
|
|
439
|
+
(paymentType.includes('mobile') ||
|
|
440
|
+
paymentType.includes('mpesa') ||
|
|
441
|
+
paymentType.includes('m-pesa'))
|
|
442
|
+
) {
|
|
443
|
+
this.paymentMethods.mobilePayment.status = true;
|
|
444
|
+
this.paymentMethod = 'mobile';
|
|
445
|
+
} else if (paymentType && paymentType.includes('bank')) {
|
|
446
|
+
this.paymentMethods.bankPayment.status = true;
|
|
447
|
+
this.paymentMethod = 'bank';
|
|
448
|
+
} else if (paymentType && paymentType.includes('cash')) {
|
|
449
|
+
this.paymentMethods.cashPayment.status = true;
|
|
450
|
+
this.paymentMethod = 'cash';
|
|
451
|
+
} else {
|
|
452
|
+
// Default: treat unspecified / manual payments as cash
|
|
453
|
+
this.paymentMethods.cashPayment.status = true;
|
|
454
|
+
this.paymentMethod = 'cash';
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
next();
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Indexes
|
|
463
|
+
powerInvoiceSchema.index({ facilityId: 1, yearMonth: -1 });
|
|
464
|
+
powerInvoiceSchema.index({ customerId: 1 });
|
|
465
|
+
powerInvoiceSchema.index({ status: 1 });
|
|
466
|
+
powerInvoiceSchema.index({ meterNumber: 1 });
|
|
467
|
+
powerInvoiceSchema.index({ accountNumber: 1 });
|
|
468
|
+
|
|
469
|
+
const PowerInvoice = mongoose.model('PowerInvoice', powerInvoiceSchema);
|
|
470
|
+
|
|
471
|
+
module.exports = PowerInvoice;
|