payservedb 8.4.5 → 8.4.7
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 +2 -0
- package/package.json +1 -1
- package/src/models/customer.js +189 -189
- package/src/models/facility.js +44 -36
- package/src/models/invoice.js +192 -100
- package/src/models/invoicing_schedule.js +6 -1
- package/src/models/zohoAccount.js +453 -0
- package/src/models/zohoItem.js +504 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
const mongoose = require("mongoose");
|
|
2
|
+
|
|
3
|
+
const ZohoAccountSchema = new mongoose.Schema(
|
|
4
|
+
{
|
|
5
|
+
facilityId: {
|
|
6
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
7
|
+
ref: "Facility",
|
|
8
|
+
required: [true, "Facility ID is required"],
|
|
9
|
+
index: true,
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
// Account Identification
|
|
13
|
+
accountName: {
|
|
14
|
+
type: String,
|
|
15
|
+
required: [true, "Account name is required"],
|
|
16
|
+
trim: true,
|
|
17
|
+
maxlength: [255, "Account name cannot exceed 255 characters"],
|
|
18
|
+
index: true,
|
|
19
|
+
},
|
|
20
|
+
accountCode: {
|
|
21
|
+
type: String,
|
|
22
|
+
trim: true,
|
|
23
|
+
sparse: true,
|
|
24
|
+
default: null,
|
|
25
|
+
index: true,
|
|
26
|
+
},
|
|
27
|
+
description: {
|
|
28
|
+
type: String,
|
|
29
|
+
trim: true,
|
|
30
|
+
maxlength: [1000, "Description cannot exceed 1000 characters"],
|
|
31
|
+
default: null,
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
// Account Classification
|
|
35
|
+
accountType: {
|
|
36
|
+
type: String,
|
|
37
|
+
required: [true, "Account type is required"],
|
|
38
|
+
enum: ["income", "expense", "asset", "liability", "equity"],
|
|
39
|
+
index: true,
|
|
40
|
+
},
|
|
41
|
+
accountSubType: {
|
|
42
|
+
type: String,
|
|
43
|
+
trim: true,
|
|
44
|
+
default: null,
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// Account Hierarchy
|
|
48
|
+
parentAccountId: {
|
|
49
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
50
|
+
ref: "ZohoAccount",
|
|
51
|
+
default: null,
|
|
52
|
+
index: true,
|
|
53
|
+
},
|
|
54
|
+
zohoParentAccountId: {
|
|
55
|
+
type: String,
|
|
56
|
+
trim: true,
|
|
57
|
+
default: null,
|
|
58
|
+
},
|
|
59
|
+
isSubAccount: {
|
|
60
|
+
type: Boolean,
|
|
61
|
+
default: false,
|
|
62
|
+
index: true,
|
|
63
|
+
},
|
|
64
|
+
depth: {
|
|
65
|
+
type: Number,
|
|
66
|
+
default: 0,
|
|
67
|
+
min: [0, "Depth cannot be negative"],
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
// Zoho Sync Fields
|
|
71
|
+
zohoAccountId: {
|
|
72
|
+
type: String,
|
|
73
|
+
trim: true,
|
|
74
|
+
unique: true,
|
|
75
|
+
sparse: true,
|
|
76
|
+
default: null,
|
|
77
|
+
index: true,
|
|
78
|
+
},
|
|
79
|
+
zohoCurrencyId: {
|
|
80
|
+
type: String,
|
|
81
|
+
trim: true,
|
|
82
|
+
default: null,
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// Financial Settings
|
|
86
|
+
currencyCode: {
|
|
87
|
+
type: String,
|
|
88
|
+
trim: true,
|
|
89
|
+
uppercase: true,
|
|
90
|
+
default: "KES",
|
|
91
|
+
maxlength: [3, "Currency code must be 3 characters"],
|
|
92
|
+
},
|
|
93
|
+
openingBalance: {
|
|
94
|
+
type: Number,
|
|
95
|
+
default: 0,
|
|
96
|
+
},
|
|
97
|
+
openingBalanceDate: {
|
|
98
|
+
type: Date,
|
|
99
|
+
default: null,
|
|
100
|
+
},
|
|
101
|
+
currentBalance: {
|
|
102
|
+
type: Number,
|
|
103
|
+
default: 0,
|
|
104
|
+
},
|
|
105
|
+
lastBalanceUpdate: {
|
|
106
|
+
type: Date,
|
|
107
|
+
default: null,
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
// Account Settings
|
|
111
|
+
isActive: {
|
|
112
|
+
type: Boolean,
|
|
113
|
+
default: true,
|
|
114
|
+
index: true,
|
|
115
|
+
},
|
|
116
|
+
isSystemAccount: {
|
|
117
|
+
type: Boolean,
|
|
118
|
+
default: false,
|
|
119
|
+
},
|
|
120
|
+
canBeDeleted: {
|
|
121
|
+
type: Boolean,
|
|
122
|
+
default: true,
|
|
123
|
+
},
|
|
124
|
+
showInPortal: {
|
|
125
|
+
type: Boolean,
|
|
126
|
+
default: false,
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
// Usage Tracking
|
|
130
|
+
usageCount: {
|
|
131
|
+
type: Number,
|
|
132
|
+
default: 0,
|
|
133
|
+
min: [0, "Usage count cannot be negative"],
|
|
134
|
+
},
|
|
135
|
+
lastUsedAt: {
|
|
136
|
+
type: Date,
|
|
137
|
+
default: null,
|
|
138
|
+
},
|
|
139
|
+
linkedItemsCount: {
|
|
140
|
+
type: Number,
|
|
141
|
+
default: 0,
|
|
142
|
+
min: [0, "Linked items count cannot be negative"],
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
// Sync Status
|
|
146
|
+
isSynced: {
|
|
147
|
+
type: Boolean,
|
|
148
|
+
default: false,
|
|
149
|
+
index: true,
|
|
150
|
+
},
|
|
151
|
+
lastSyncedAt: {
|
|
152
|
+
type: Date,
|
|
153
|
+
default: null,
|
|
154
|
+
},
|
|
155
|
+
syncStatus: {
|
|
156
|
+
type: String,
|
|
157
|
+
enum: ["pending", "syncing", "synced", "failed"],
|
|
158
|
+
default: "pending",
|
|
159
|
+
index: true,
|
|
160
|
+
},
|
|
161
|
+
syncError: {
|
|
162
|
+
type: String,
|
|
163
|
+
trim: true,
|
|
164
|
+
default: null,
|
|
165
|
+
},
|
|
166
|
+
lastSyncAttemptAt: {
|
|
167
|
+
type: Date,
|
|
168
|
+
default: null,
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
// Audit Fields
|
|
172
|
+
createdBy: {
|
|
173
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
174
|
+
ref: "User",
|
|
175
|
+
default: null,
|
|
176
|
+
},
|
|
177
|
+
updatedBy: {
|
|
178
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
179
|
+
ref: "User",
|
|
180
|
+
default: null,
|
|
181
|
+
},
|
|
182
|
+
lastModifiedBy: {
|
|
183
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
184
|
+
ref: "User",
|
|
185
|
+
default: null,
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
// Metadata
|
|
189
|
+
notes: {
|
|
190
|
+
type: String,
|
|
191
|
+
trim: true,
|
|
192
|
+
maxlength: [1000, "Notes cannot exceed 1000 characters"],
|
|
193
|
+
default: null,
|
|
194
|
+
},
|
|
195
|
+
metadata: {
|
|
196
|
+
type: mongoose.Schema.Types.Mixed,
|
|
197
|
+
default: {},
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
// Zoho Specific Fields
|
|
201
|
+
zohoAccountNumber: {
|
|
202
|
+
type: String,
|
|
203
|
+
trim: true,
|
|
204
|
+
default: null,
|
|
205
|
+
},
|
|
206
|
+
zohoDepth: {
|
|
207
|
+
type: Number,
|
|
208
|
+
default: null,
|
|
209
|
+
},
|
|
210
|
+
zohoHasAttachment: {
|
|
211
|
+
type: Boolean,
|
|
212
|
+
default: false,
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
timestamps: true,
|
|
217
|
+
}
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// Compound Indexes
|
|
221
|
+
ZohoAccountSchema.index({ facilityId: 1, accountName: 1 });
|
|
222
|
+
ZohoAccountSchema.index({ facilityId: 1, accountType: 1 });
|
|
223
|
+
ZohoAccountSchema.index({ facilityId: 1, isActive: 1 });
|
|
224
|
+
ZohoAccountSchema.index({ facilityId: 1, isSynced: 1 });
|
|
225
|
+
ZohoAccountSchema.index({ facilityId: 1, accountType: 1, isActive: 1 });
|
|
226
|
+
ZohoAccountSchema.index({ zohoAccountId: 1, facilityId: 1 });
|
|
227
|
+
|
|
228
|
+
// Text index for searching
|
|
229
|
+
ZohoAccountSchema.index({
|
|
230
|
+
accountName: "text",
|
|
231
|
+
description: "text",
|
|
232
|
+
accountCode: "text",
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Virtual for full account path (for sub-accounts)
|
|
236
|
+
ZohoAccountSchema.virtual("fullPath").get(function () {
|
|
237
|
+
if (this.parentAccountId) {
|
|
238
|
+
return `${this.parentAccountId.accountName} > ${this.accountName}`;
|
|
239
|
+
}
|
|
240
|
+
return this.accountName;
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Virtual for sync status indicator
|
|
244
|
+
ZohoAccountSchema.virtual("needsSync").get(function () {
|
|
245
|
+
if (!this.zohoAccountId) return true;
|
|
246
|
+
if (!this.isSynced) return true;
|
|
247
|
+
return false;
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Method to mark as synced
|
|
251
|
+
ZohoAccountSchema.methods.markAsSynced = function (zohoAccountId, zohoData = {}) {
|
|
252
|
+
this.zohoAccountId = zohoAccountId;
|
|
253
|
+
this.isSynced = true;
|
|
254
|
+
this.lastSyncedAt = new Date();
|
|
255
|
+
this.syncStatus = "synced";
|
|
256
|
+
this.syncError = null;
|
|
257
|
+
|
|
258
|
+
// Update Zoho-specific fields if provided
|
|
259
|
+
if (zohoData.currencyId) this.zohoCurrencyId = zohoData.currencyId;
|
|
260
|
+
if (zohoData.parentAccountId) this.zohoParentAccountId = zohoData.parentAccountId;
|
|
261
|
+
if (zohoData.accountNumber) this.zohoAccountNumber = zohoData.accountNumber;
|
|
262
|
+
if (zohoData.depth !== undefined) this.zohoDepth = zohoData.depth;
|
|
263
|
+
|
|
264
|
+
return this.save();
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// Method to mark sync as failed
|
|
268
|
+
ZohoAccountSchema.methods.markSyncFailed = function (errorMessage) {
|
|
269
|
+
this.syncStatus = "failed";
|
|
270
|
+
this.syncError = errorMessage;
|
|
271
|
+
this.lastSyncAttemptAt = new Date();
|
|
272
|
+
return this.save();
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// Method to increment usage count
|
|
276
|
+
ZohoAccountSchema.methods.incrementUsage = function () {
|
|
277
|
+
this.usageCount += 1;
|
|
278
|
+
this.lastUsedAt = new Date();
|
|
279
|
+
return this.save();
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// Method to update balance
|
|
283
|
+
ZohoAccountSchema.methods.updateBalance = function (newBalance) {
|
|
284
|
+
this.currentBalance = newBalance;
|
|
285
|
+
this.lastBalanceUpdate = new Date();
|
|
286
|
+
return this.save();
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// Method to activate account
|
|
290
|
+
ZohoAccountSchema.methods.activate = function () {
|
|
291
|
+
this.isActive = true;
|
|
292
|
+
return this.save();
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// Method to deactivate account
|
|
296
|
+
ZohoAccountSchema.methods.deactivate = function () {
|
|
297
|
+
this.isActive = false;
|
|
298
|
+
return this.save();
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// Static method to find accounts by type
|
|
302
|
+
ZohoAccountSchema.statics.findByType = function (facilityId, accountType) {
|
|
303
|
+
return this.find({
|
|
304
|
+
facilityId,
|
|
305
|
+
accountType,
|
|
306
|
+
isActive: true,
|
|
307
|
+
}).sort({ accountName: 1 });
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// Static method to find income accounts (for items)
|
|
311
|
+
ZohoAccountSchema.statics.findIncomeAccounts = function (facilityId) {
|
|
312
|
+
return this.find({
|
|
313
|
+
facilityId,
|
|
314
|
+
accountType: "income",
|
|
315
|
+
isActive: true,
|
|
316
|
+
}).sort({ accountName: 1 });
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// Static method to find expense accounts
|
|
320
|
+
ZohoAccountSchema.statics.findExpenseAccounts = function (facilityId) {
|
|
321
|
+
return this.find({
|
|
322
|
+
facilityId,
|
|
323
|
+
accountType: "expense",
|
|
324
|
+
isActive: true,
|
|
325
|
+
}).sort({ accountName: 1 });
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// Static method to find unsynced accounts
|
|
329
|
+
ZohoAccountSchema.statics.findUnsynced = function (facilityId) {
|
|
330
|
+
return this.find({
|
|
331
|
+
facilityId,
|
|
332
|
+
$or: [{ isSynced: false }, { zohoAccountId: null }],
|
|
333
|
+
isActive: true,
|
|
334
|
+
}).sort({ createdAt: 1 });
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// Static method to search accounts
|
|
338
|
+
ZohoAccountSchema.statics.searchAccounts = function (facilityId, searchText) {
|
|
339
|
+
return this.find({
|
|
340
|
+
facilityId,
|
|
341
|
+
isActive: true,
|
|
342
|
+
$text: { $search: searchText },
|
|
343
|
+
}).sort({ score: { $meta: "textScore" } });
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// Pre-save hook to update timestamps
|
|
347
|
+
ZohoAccountSchema.pre("save", function (next) {
|
|
348
|
+
if (this.isModified() && !this.isNew) {
|
|
349
|
+
this.updatedAt = new Date();
|
|
350
|
+
}
|
|
351
|
+
next();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Pre-save validation
|
|
355
|
+
ZohoAccountSchema.pre("save", function (next) {
|
|
356
|
+
// Validate sub-account
|
|
357
|
+
if (this.parentAccountId && !this.isSubAccount) {
|
|
358
|
+
this.isSubAccount = true;
|
|
359
|
+
}
|
|
360
|
+
if (!this.parentAccountId && this.isSubAccount) {
|
|
361
|
+
this.isSubAccount = false;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// System accounts cannot be deleted
|
|
365
|
+
if (this.isSystemAccount) {
|
|
366
|
+
this.canBeDeleted = false;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
next();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// Instance method to convert to Zoho format
|
|
373
|
+
ZohoAccountSchema.methods.toZohoFormat = function () {
|
|
374
|
+
const zohoAccount = {
|
|
375
|
+
account_name: this.accountName,
|
|
376
|
+
account_type: this.accountType,
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
// Add optional fields
|
|
380
|
+
if (this.accountCode) {
|
|
381
|
+
zohoAccount.account_code = this.accountCode;
|
|
382
|
+
}
|
|
383
|
+
if (this.description) {
|
|
384
|
+
zohoAccount.description = this.description;
|
|
385
|
+
}
|
|
386
|
+
if (this.zohoParentAccountId) {
|
|
387
|
+
zohoAccount.parent_account_id = this.zohoParentAccountId;
|
|
388
|
+
}
|
|
389
|
+
if (this.currencyCode) {
|
|
390
|
+
zohoAccount.currency_code = this.currencyCode;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return zohoAccount;
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
// Instance method to update from Zoho data
|
|
397
|
+
ZohoAccountSchema.methods.updateFromZoho = function (zohoData) {
|
|
398
|
+
if (zohoData.account_id) this.zohoAccountId = zohoData.account_id;
|
|
399
|
+
if (zohoData.account_name) this.accountName = zohoData.account_name;
|
|
400
|
+
if (zohoData.account_code) this.accountCode = zohoData.account_code;
|
|
401
|
+
if (zohoData.account_type) this.accountType = zohoData.account_type;
|
|
402
|
+
if (zohoData.account_number) this.zohoAccountNumber = zohoData.account_number;
|
|
403
|
+
if (zohoData.description) this.description = zohoData.description;
|
|
404
|
+
if (zohoData.is_active !== undefined) this.isActive = zohoData.is_active;
|
|
405
|
+
if (zohoData.currency_id) this.zohoCurrencyId = zohoData.currency_id;
|
|
406
|
+
if (zohoData.currency_code) this.currencyCode = zohoData.currency_code;
|
|
407
|
+
if (zohoData.parent_account_id) this.zohoParentAccountId = zohoData.parent_account_id;
|
|
408
|
+
if (zohoData.depth !== undefined) {
|
|
409
|
+
this.depth = zohoData.depth;
|
|
410
|
+
this.zohoDepth = zohoData.depth;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
this.isSynced = true;
|
|
414
|
+
this.lastSyncedAt = new Date();
|
|
415
|
+
this.syncStatus = "synced";
|
|
416
|
+
|
|
417
|
+
return this.save();
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// Static method to build account hierarchy
|
|
421
|
+
ZohoAccountSchema.statics.getAccountTree = async function (facilityId) {
|
|
422
|
+
const accounts = await this.find({ facilityId, isActive: true })
|
|
423
|
+
.populate("parentAccountId")
|
|
424
|
+
.sort({ accountType: 1, accountName: 1 });
|
|
425
|
+
|
|
426
|
+
// Build tree structure
|
|
427
|
+
const accountMap = {};
|
|
428
|
+
const tree = [];
|
|
429
|
+
|
|
430
|
+
accounts.forEach((account) => {
|
|
431
|
+
accountMap[account._id] = {
|
|
432
|
+
...account.toObject(),
|
|
433
|
+
children: [],
|
|
434
|
+
};
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
accounts.forEach((account) => {
|
|
438
|
+
if (account.parentAccountId) {
|
|
439
|
+
const parent = accountMap[account.parentAccountId];
|
|
440
|
+
if (parent) {
|
|
441
|
+
parent.children.push(accountMap[account._id]);
|
|
442
|
+
}
|
|
443
|
+
} else {
|
|
444
|
+
tree.push(accountMap[account._id]);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
return tree;
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const ZohoAccount = mongoose.model("ZohoAccount", ZohoAccountSchema);
|
|
452
|
+
|
|
453
|
+
module.exports = ZohoAccount;
|