@classytic/payroll 1.0.2 → 2.0.0

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 (68) hide show
  1. package/README.md +168 -489
  2. package/dist/core/index.d.ts +480 -0
  3. package/dist/core/index.js +971 -0
  4. package/dist/core/index.js.map +1 -0
  5. package/dist/index-CTjHlCzz.d.ts +721 -0
  6. package/dist/index.d.ts +967 -0
  7. package/dist/index.js +4352 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/payroll.d.ts +233 -0
  10. package/dist/payroll.js +2103 -0
  11. package/dist/payroll.js.map +1 -0
  12. package/dist/plugin-D9mOr3_d.d.ts +333 -0
  13. package/dist/schemas/index.d.ts +2869 -0
  14. package/dist/schemas/index.js +440 -0
  15. package/dist/schemas/index.js.map +1 -0
  16. package/dist/services/index.d.ts +3 -0
  17. package/dist/services/index.js +1696 -0
  18. package/dist/services/index.js.map +1 -0
  19. package/dist/types-BSYyX2KJ.d.ts +671 -0
  20. package/dist/utils/index.d.ts +873 -0
  21. package/dist/utils/index.js +1046 -0
  22. package/dist/utils/index.js.map +1 -0
  23. package/package.json +54 -37
  24. package/dist/types/config.d.ts +0 -162
  25. package/dist/types/core/compensation.manager.d.ts +0 -54
  26. package/dist/types/core/employment.manager.d.ts +0 -49
  27. package/dist/types/core/payroll.manager.d.ts +0 -60
  28. package/dist/types/enums.d.ts +0 -117
  29. package/dist/types/factories/compensation.factory.d.ts +0 -196
  30. package/dist/types/factories/employee.factory.d.ts +0 -149
  31. package/dist/types/factories/payroll.factory.d.ts +0 -319
  32. package/dist/types/hrm.orchestrator.d.ts +0 -47
  33. package/dist/types/index.d.ts +0 -20
  34. package/dist/types/init.d.ts +0 -30
  35. package/dist/types/models/payroll-record.model.d.ts +0 -3
  36. package/dist/types/plugins/employee.plugin.d.ts +0 -2
  37. package/dist/types/schemas/employment.schema.d.ts +0 -959
  38. package/dist/types/services/compensation.service.d.ts +0 -94
  39. package/dist/types/services/employee.service.d.ts +0 -28
  40. package/dist/types/services/payroll.service.d.ts +0 -30
  41. package/dist/types/utils/calculation.utils.d.ts +0 -26
  42. package/dist/types/utils/date.utils.d.ts +0 -35
  43. package/dist/types/utils/logger.d.ts +0 -12
  44. package/dist/types/utils/query-builders.d.ts +0 -83
  45. package/dist/types/utils/validation.utils.d.ts +0 -33
  46. package/payroll.d.ts +0 -241
  47. package/src/config.js +0 -177
  48. package/src/core/compensation.manager.js +0 -242
  49. package/src/core/employment.manager.js +0 -224
  50. package/src/core/payroll.manager.js +0 -499
  51. package/src/enums.js +0 -141
  52. package/src/factories/compensation.factory.js +0 -198
  53. package/src/factories/employee.factory.js +0 -173
  54. package/src/factories/payroll.factory.js +0 -413
  55. package/src/hrm.orchestrator.js +0 -139
  56. package/src/index.js +0 -172
  57. package/src/init.js +0 -62
  58. package/src/models/payroll-record.model.js +0 -126
  59. package/src/plugins/employee.plugin.js +0 -164
  60. package/src/schemas/employment.schema.js +0 -126
  61. package/src/services/compensation.service.js +0 -231
  62. package/src/services/employee.service.js +0 -162
  63. package/src/services/payroll.service.js +0 -213
  64. package/src/utils/calculation.utils.js +0 -91
  65. package/src/utils/date.utils.js +0 -120
  66. package/src/utils/logger.js +0 -36
  67. package/src/utils/query-builders.js +0 -185
  68. package/src/utils/validation.utils.js +0 -122
@@ -0,0 +1,1696 @@
1
+ import { Types } from 'mongoose';
2
+
3
+ // src/utils/date.ts
4
+ function addMonths(date, months) {
5
+ const result = new Date(date);
6
+ result.setMonth(result.getMonth() + months);
7
+ return result;
8
+ }
9
+ function startOfMonth(date) {
10
+ const result = new Date(date);
11
+ result.setDate(1);
12
+ result.setHours(0, 0, 0, 0);
13
+ return result;
14
+ }
15
+ function endOfMonth(date) {
16
+ const result = new Date(date);
17
+ result.setMonth(result.getMonth() + 1, 0);
18
+ result.setHours(23, 59, 59, 999);
19
+ return result;
20
+ }
21
+ function getPayPeriod(month, year) {
22
+ const startDate = new Date(year, month - 1, 1);
23
+ return {
24
+ month,
25
+ year,
26
+ startDate: startOfMonth(startDate),
27
+ endDate: endOfMonth(startDate)
28
+ };
29
+ }
30
+ function getCurrentPeriod(date = /* @__PURE__ */ new Date()) {
31
+ const d = new Date(date);
32
+ return {
33
+ year: d.getFullYear(),
34
+ month: d.getMonth() + 1
35
+ };
36
+ }
37
+ function calculateProbationEnd(hireDate, probationMonths) {
38
+ if (!probationMonths || probationMonths <= 0) return null;
39
+ return addMonths(hireDate, probationMonths);
40
+ }
41
+
42
+ // src/config.ts
43
+ var HRM_CONFIG = {
44
+ payroll: {
45
+ defaultCurrency: "BDT"},
46
+ employment: {
47
+ defaultProbationMonths: 3}};
48
+ var ORG_ROLES = {
49
+ OWNER: {
50
+ key: "owner",
51
+ label: "Owner",
52
+ description: "Full organization access (set by Organization model)"
53
+ },
54
+ MANAGER: {
55
+ key: "manager",
56
+ label: "Manager",
57
+ description: "Management and administrative features"
58
+ },
59
+ TRAINER: {
60
+ key: "trainer",
61
+ label: "Trainer",
62
+ description: "Training and coaching features"
63
+ },
64
+ STAFF: {
65
+ key: "staff",
66
+ label: "Staff",
67
+ description: "General staff access to basic features"
68
+ },
69
+ INTERN: {
70
+ key: "intern",
71
+ label: "Intern",
72
+ description: "Limited access for interns"
73
+ },
74
+ CONSULTANT: {
75
+ key: "consultant",
76
+ label: "Consultant",
77
+ description: "Project-based consultant access"
78
+ }
79
+ };
80
+ Object.values(ORG_ROLES).map((role) => role.key);
81
+
82
+ // src/factories/employee.factory.ts
83
+ var EmployeeFactory = class {
84
+ /**
85
+ * Create employee data object
86
+ */
87
+ static create(params) {
88
+ const { userId, organizationId, employment, compensation, bankDetails } = params;
89
+ const hireDate = employment.hireDate || /* @__PURE__ */ new Date();
90
+ return {
91
+ userId,
92
+ organizationId,
93
+ employeeId: employment.employeeId || `EMP-${Date.now()}-${Math.random().toString(36).substr(2, 6).toUpperCase()}`,
94
+ employmentType: employment.type || "full_time",
95
+ status: "active",
96
+ department: employment.department,
97
+ position: employment.position,
98
+ hireDate,
99
+ probationEndDate: calculateProbationEnd(
100
+ hireDate,
101
+ employment.probationMonths ?? HRM_CONFIG.employment.defaultProbationMonths
102
+ ),
103
+ compensation: this.createCompensation(compensation),
104
+ workSchedule: employment.workSchedule || this.defaultWorkSchedule(),
105
+ bankDetails: bankDetails || {},
106
+ payrollStats: {
107
+ totalPaid: 0,
108
+ paymentsThisYear: 0,
109
+ averageMonthly: 0
110
+ }
111
+ };
112
+ }
113
+ /**
114
+ * Create compensation object
115
+ */
116
+ static createCompensation(params) {
117
+ return {
118
+ baseAmount: params.baseAmount,
119
+ frequency: params.frequency || "monthly",
120
+ currency: params.currency || HRM_CONFIG.payroll.defaultCurrency,
121
+ allowances: (params.allowances || []).map((a) => ({
122
+ type: a.type || "other",
123
+ name: a.name || a.type || "other",
124
+ amount: a.amount || 0,
125
+ taxable: a.taxable,
126
+ recurring: a.recurring,
127
+ effectiveFrom: a.effectiveFrom,
128
+ effectiveTo: a.effectiveTo
129
+ })),
130
+ deductions: (params.deductions || []).map((d) => ({
131
+ type: d.type || "other",
132
+ name: d.name || d.type || "other",
133
+ amount: d.amount || 0,
134
+ auto: d.auto,
135
+ recurring: d.recurring,
136
+ description: d.description,
137
+ effectiveFrom: d.effectiveFrom,
138
+ effectiveTo: d.effectiveTo
139
+ })),
140
+ grossSalary: 0,
141
+ netSalary: 0,
142
+ effectiveFrom: /* @__PURE__ */ new Date(),
143
+ lastModified: /* @__PURE__ */ new Date()
144
+ };
145
+ }
146
+ /**
147
+ * Create allowance object
148
+ */
149
+ static createAllowance(params) {
150
+ return {
151
+ type: params.type,
152
+ name: params.name || params.type,
153
+ amount: params.amount,
154
+ isPercentage: params.isPercentage ?? false,
155
+ taxable: params.taxable ?? true,
156
+ recurring: params.recurring ?? true,
157
+ effectiveFrom: /* @__PURE__ */ new Date()
158
+ };
159
+ }
160
+ /**
161
+ * Create deduction object
162
+ */
163
+ static createDeduction(params) {
164
+ return {
165
+ type: params.type,
166
+ name: params.name || params.type,
167
+ amount: params.amount,
168
+ isPercentage: params.isPercentage ?? false,
169
+ auto: params.auto ?? false,
170
+ recurring: params.recurring ?? true,
171
+ description: params.description,
172
+ effectiveFrom: /* @__PURE__ */ new Date()
173
+ };
174
+ }
175
+ /**
176
+ * Default work schedule
177
+ */
178
+ static defaultWorkSchedule() {
179
+ return {
180
+ hoursPerWeek: 40,
181
+ hoursPerDay: 8,
182
+ workingDays: [1, 2, 3, 4, 5],
183
+ // Mon-Fri
184
+ shiftStart: "09:00",
185
+ shiftEnd: "17:00"
186
+ };
187
+ }
188
+ /**
189
+ * Create termination data
190
+ */
191
+ static createTermination(params) {
192
+ return {
193
+ terminatedAt: params.date || /* @__PURE__ */ new Date(),
194
+ terminationReason: params.reason,
195
+ terminationNotes: params.notes,
196
+ terminatedBy: {
197
+ userId: params.context?.userId,
198
+ name: params.context?.userName,
199
+ role: params.context?.userRole
200
+ }
201
+ };
202
+ }
203
+ };
204
+ function toObjectId(id) {
205
+ if (id instanceof Types.ObjectId) return id;
206
+ return new Types.ObjectId(id);
207
+ }
208
+ var QueryBuilder = class {
209
+ query;
210
+ constructor(initialQuery = {}) {
211
+ this.query = { ...initialQuery };
212
+ }
213
+ /**
214
+ * Add where condition
215
+ */
216
+ where(field, value) {
217
+ this.query[field] = value;
218
+ return this;
219
+ }
220
+ /**
221
+ * Add $in condition
222
+ */
223
+ whereIn(field, values) {
224
+ this.query[field] = { $in: values };
225
+ return this;
226
+ }
227
+ /**
228
+ * Add $nin condition
229
+ */
230
+ whereNotIn(field, values) {
231
+ this.query[field] = { $nin: values };
232
+ return this;
233
+ }
234
+ /**
235
+ * Add $gte condition
236
+ */
237
+ whereGte(field, value) {
238
+ const existing = this.query[field] || {};
239
+ this.query[field] = { ...existing, $gte: value };
240
+ return this;
241
+ }
242
+ /**
243
+ * Add $lte condition
244
+ */
245
+ whereLte(field, value) {
246
+ const existing = this.query[field] || {};
247
+ this.query[field] = { ...existing, $lte: value };
248
+ return this;
249
+ }
250
+ /**
251
+ * Add $gt condition
252
+ */
253
+ whereGt(field, value) {
254
+ const existing = this.query[field] || {};
255
+ this.query[field] = { ...existing, $gt: value };
256
+ return this;
257
+ }
258
+ /**
259
+ * Add $lt condition
260
+ */
261
+ whereLt(field, value) {
262
+ const existing = this.query[field] || {};
263
+ this.query[field] = { ...existing, $lt: value };
264
+ return this;
265
+ }
266
+ /**
267
+ * Add between condition
268
+ */
269
+ whereBetween(field, start, end) {
270
+ this.query[field] = { $gte: start, $lte: end };
271
+ return this;
272
+ }
273
+ /**
274
+ * Add $exists condition
275
+ */
276
+ whereExists(field) {
277
+ this.query[field] = { $exists: true };
278
+ return this;
279
+ }
280
+ /**
281
+ * Add $exists: false condition
282
+ */
283
+ whereNotExists(field) {
284
+ this.query[field] = { $exists: false };
285
+ return this;
286
+ }
287
+ /**
288
+ * Add $ne condition
289
+ */
290
+ whereNot(field, value) {
291
+ this.query[field] = { $ne: value };
292
+ return this;
293
+ }
294
+ /**
295
+ * Add regex condition
296
+ */
297
+ whereRegex(field, pattern, flags = "i") {
298
+ this.query[field] = { $regex: pattern, $options: flags };
299
+ return this;
300
+ }
301
+ /**
302
+ * Merge another query
303
+ */
304
+ merge(otherQuery) {
305
+ this.query = { ...this.query, ...otherQuery };
306
+ return this;
307
+ }
308
+ /**
309
+ * Build and return the query
310
+ */
311
+ build() {
312
+ return { ...this.query };
313
+ }
314
+ };
315
+ var EmployeeQueryBuilder = class extends QueryBuilder {
316
+ /**
317
+ * Filter by organization
318
+ */
319
+ forOrganization(organizationId) {
320
+ return this.where("organizationId", toObjectId(organizationId));
321
+ }
322
+ /**
323
+ * Filter by user
324
+ */
325
+ forUser(userId) {
326
+ return this.where("userId", toObjectId(userId));
327
+ }
328
+ /**
329
+ * Filter by status(es)
330
+ */
331
+ withStatus(...statuses) {
332
+ if (statuses.length === 1) {
333
+ return this.where("status", statuses[0]);
334
+ }
335
+ return this.whereIn("status", statuses);
336
+ }
337
+ /**
338
+ * Filter active employees
339
+ */
340
+ active() {
341
+ return this.withStatus("active");
342
+ }
343
+ /**
344
+ * Filter employed employees (not terminated)
345
+ */
346
+ employed() {
347
+ return this.whereIn("status", ["active", "on_leave", "suspended"]);
348
+ }
349
+ /**
350
+ * Filter terminated employees
351
+ */
352
+ terminated() {
353
+ return this.withStatus("terminated");
354
+ }
355
+ /**
356
+ * Filter by department
357
+ */
358
+ inDepartment(department) {
359
+ return this.where("department", department);
360
+ }
361
+ /**
362
+ * Filter by position
363
+ */
364
+ inPosition(position) {
365
+ return this.where("position", position);
366
+ }
367
+ /**
368
+ * Filter by employment type
369
+ */
370
+ withEmploymentType(type) {
371
+ return this.where("employmentType", type);
372
+ }
373
+ /**
374
+ * Filter by hire date (after)
375
+ */
376
+ hiredAfter(date) {
377
+ return this.whereGte("hireDate", date);
378
+ }
379
+ /**
380
+ * Filter by hire date (before)
381
+ */
382
+ hiredBefore(date) {
383
+ return this.whereLte("hireDate", date);
384
+ }
385
+ /**
386
+ * Filter by minimum salary
387
+ */
388
+ withMinSalary(amount) {
389
+ return this.whereGte("compensation.netSalary", amount);
390
+ }
391
+ /**
392
+ * Filter by maximum salary
393
+ */
394
+ withMaxSalary(amount) {
395
+ return this.whereLte("compensation.netSalary", amount);
396
+ }
397
+ /**
398
+ * Filter by salary range
399
+ */
400
+ withSalaryRange(min, max) {
401
+ return this.whereBetween("compensation.netSalary", min, max);
402
+ }
403
+ };
404
+ var PayrollQueryBuilder = class extends QueryBuilder {
405
+ /**
406
+ * Filter by organization
407
+ */
408
+ forOrganization(organizationId) {
409
+ return this.where("organizationId", toObjectId(organizationId));
410
+ }
411
+ /**
412
+ * Filter by employee
413
+ */
414
+ forEmployee(employeeId) {
415
+ return this.where("employeeId", toObjectId(employeeId));
416
+ }
417
+ /**
418
+ * Filter by period
419
+ */
420
+ forPeriod(month, year) {
421
+ if (month !== void 0) {
422
+ this.where("period.month", month);
423
+ }
424
+ if (year !== void 0) {
425
+ this.where("period.year", year);
426
+ }
427
+ return this;
428
+ }
429
+ /**
430
+ * Filter by status(es)
431
+ */
432
+ withStatus(...statuses) {
433
+ if (statuses.length === 1) {
434
+ return this.where("status", statuses[0]);
435
+ }
436
+ return this.whereIn("status", statuses);
437
+ }
438
+ /**
439
+ * Filter paid records
440
+ */
441
+ paid() {
442
+ return this.withStatus("paid");
443
+ }
444
+ /**
445
+ * Filter pending records
446
+ */
447
+ pending() {
448
+ return this.whereIn("status", ["pending", "processing"]);
449
+ }
450
+ /**
451
+ * Filter by date range
452
+ */
453
+ inDateRange(start, end) {
454
+ return this.whereBetween("period.payDate", start, end);
455
+ }
456
+ /**
457
+ * Filter exported records
458
+ */
459
+ exported() {
460
+ return this.where("exported", true);
461
+ }
462
+ /**
463
+ * Filter not exported records
464
+ */
465
+ notExported() {
466
+ return this.where("exported", false);
467
+ }
468
+ };
469
+ function employee() {
470
+ return new EmployeeQueryBuilder();
471
+ }
472
+ function payroll() {
473
+ return new PayrollQueryBuilder();
474
+ }
475
+
476
+ // src/utils/validation.ts
477
+ function isActive(employee2) {
478
+ return employee2?.status === "active";
479
+ }
480
+ function isOnLeave(employee2) {
481
+ return employee2?.status === "on_leave";
482
+ }
483
+ function isSuspended(employee2) {
484
+ return employee2?.status === "suspended";
485
+ }
486
+ function isEmployed(employee2) {
487
+ return isActive(employee2) || isOnLeave(employee2) || isSuspended(employee2);
488
+ }
489
+ function canReceiveSalary(employee2) {
490
+ return (isActive(employee2) || isOnLeave(employee2)) && (employee2.compensation?.baseAmount ?? 0) > 0;
491
+ }
492
+
493
+ // src/utils/logger.ts
494
+ var createConsoleLogger = () => ({
495
+ info: (message, meta) => {
496
+ if (meta) {
497
+ console.log(`[Payroll] INFO: ${message}`, meta);
498
+ } else {
499
+ console.log(`[Payroll] INFO: ${message}`);
500
+ }
501
+ },
502
+ error: (message, meta) => {
503
+ if (meta) {
504
+ console.error(`[Payroll] ERROR: ${message}`, meta);
505
+ } else {
506
+ console.error(`[Payroll] ERROR: ${message}`);
507
+ }
508
+ },
509
+ warn: (message, meta) => {
510
+ if (meta) {
511
+ console.warn(`[Payroll] WARN: ${message}`, meta);
512
+ } else {
513
+ console.warn(`[Payroll] WARN: ${message}`);
514
+ }
515
+ },
516
+ debug: (message, meta) => {
517
+ if (process.env.NODE_ENV !== "production") {
518
+ if (meta) {
519
+ console.log(`[Payroll] DEBUG: ${message}`, meta);
520
+ } else {
521
+ console.log(`[Payroll] DEBUG: ${message}`);
522
+ }
523
+ }
524
+ }
525
+ });
526
+ var currentLogger = createConsoleLogger();
527
+ var logger = {
528
+ info: (message, meta) => {
529
+ currentLogger.info(message, meta);
530
+ },
531
+ error: (message, meta) => {
532
+ currentLogger.error(message, meta);
533
+ },
534
+ warn: (message, meta) => {
535
+ currentLogger.warn(message, meta);
536
+ },
537
+ debug: (message, meta) => {
538
+ currentLogger.debug(message, meta);
539
+ }
540
+ };
541
+
542
+ // src/services/employee.service.ts
543
+ var EmployeeService = class {
544
+ constructor(EmployeeModel) {
545
+ this.EmployeeModel = EmployeeModel;
546
+ }
547
+ /**
548
+ * Find employee by ID
549
+ */
550
+ async findById(employeeId, options = {}) {
551
+ let query = this.EmployeeModel.findById(toObjectId(employeeId));
552
+ if (options.session) {
553
+ query = query.session(options.session);
554
+ }
555
+ if (options.populate) {
556
+ query = query.populate("userId", "name email phone");
557
+ }
558
+ return query.exec();
559
+ }
560
+ /**
561
+ * Find employee by user and organization
562
+ */
563
+ async findByUserId(userId, organizationId, options = {}) {
564
+ const query = employee().forUser(userId).forOrganization(organizationId).build();
565
+ let mongooseQuery = this.EmployeeModel.findOne(query);
566
+ if (options.session) {
567
+ mongooseQuery = mongooseQuery.session(options.session);
568
+ }
569
+ return mongooseQuery.exec();
570
+ }
571
+ /**
572
+ * Find active employees in organization
573
+ */
574
+ async findActive(organizationId, options = {}) {
575
+ const query = employee().forOrganization(organizationId).active().build();
576
+ let mongooseQuery = this.EmployeeModel.find(query, options.projection);
577
+ if (options.session) {
578
+ mongooseQuery = mongooseQuery.session(options.session);
579
+ }
580
+ return mongooseQuery.exec();
581
+ }
582
+ /**
583
+ * Find employed employees (not terminated)
584
+ */
585
+ async findEmployed(organizationId, options = {}) {
586
+ const query = employee().forOrganization(organizationId).employed().build();
587
+ let mongooseQuery = this.EmployeeModel.find(query, options.projection);
588
+ if (options.session) {
589
+ mongooseQuery = mongooseQuery.session(options.session);
590
+ }
591
+ return mongooseQuery.exec();
592
+ }
593
+ /**
594
+ * Find employees by department
595
+ */
596
+ async findByDepartment(organizationId, department, options = {}) {
597
+ const query = employee().forOrganization(organizationId).inDepartment(department).active().build();
598
+ let mongooseQuery = this.EmployeeModel.find(query);
599
+ if (options.session) {
600
+ mongooseQuery = mongooseQuery.session(options.session);
601
+ }
602
+ return mongooseQuery.exec();
603
+ }
604
+ /**
605
+ * Find employees eligible for payroll
606
+ */
607
+ async findEligibleForPayroll(organizationId, options = {}) {
608
+ const query = employee().forOrganization(organizationId).employed().build();
609
+ let mongooseQuery = this.EmployeeModel.find(query);
610
+ if (options.session) {
611
+ mongooseQuery = mongooseQuery.session(options.session);
612
+ }
613
+ const employees = await mongooseQuery.exec();
614
+ return employees.filter((emp) => canReceiveSalary(emp));
615
+ }
616
+ /**
617
+ * Create new employee
618
+ */
619
+ async create(params, options = {}) {
620
+ const employeeData = EmployeeFactory.create(params);
621
+ const [employee2] = await this.EmployeeModel.create([employeeData], {
622
+ session: options.session
623
+ });
624
+ logger.info("Employee created", {
625
+ employeeId: employee2.employeeId,
626
+ organizationId: employee2.organizationId.toString()
627
+ });
628
+ return employee2;
629
+ }
630
+ /**
631
+ * Update employee status
632
+ */
633
+ async updateStatus(employeeId, status, context = {}, options = {}) {
634
+ const employee2 = await this.findById(employeeId, options);
635
+ if (!employee2) {
636
+ throw new Error("Employee not found");
637
+ }
638
+ employee2.status = status;
639
+ await employee2.save({ session: options.session });
640
+ logger.info("Employee status updated", {
641
+ employeeId: employee2.employeeId,
642
+ newStatus: status
643
+ });
644
+ return employee2;
645
+ }
646
+ /**
647
+ * Update employee compensation
648
+ *
649
+ * NOTE: This merges the compensation fields rather than replacing the entire object.
650
+ * To update allowances/deductions, use addAllowance/removeAllowance methods.
651
+ */
652
+ async updateCompensation(employeeId, compensation, options = {}) {
653
+ const currentEmployee = await this.EmployeeModel.findById(toObjectId(employeeId)).session(options.session || null);
654
+ if (!currentEmployee) {
655
+ throw new Error("Employee not found");
656
+ }
657
+ const updateFields = {
658
+ "compensation.lastModified": /* @__PURE__ */ new Date()
659
+ };
660
+ if (compensation.baseAmount !== void 0) {
661
+ updateFields["compensation.baseAmount"] = compensation.baseAmount;
662
+ }
663
+ if (compensation.currency !== void 0) {
664
+ updateFields["compensation.currency"] = compensation.currency;
665
+ }
666
+ if (compensation.frequency !== void 0) {
667
+ updateFields["compensation.frequency"] = compensation.frequency;
668
+ }
669
+ if (compensation.effectiveFrom !== void 0) {
670
+ updateFields["compensation.effectiveFrom"] = compensation.effectiveFrom;
671
+ }
672
+ const employee2 = await this.EmployeeModel.findByIdAndUpdate(
673
+ toObjectId(employeeId),
674
+ { $set: updateFields },
675
+ { new: true, runValidators: true, session: options.session }
676
+ );
677
+ if (!employee2) {
678
+ throw new Error("Employee not found");
679
+ }
680
+ return employee2;
681
+ }
682
+ /**
683
+ * Get employee statistics for organization
684
+ */
685
+ async getEmployeeStats(organizationId, options = {}) {
686
+ const query = employee().forOrganization(organizationId).build();
687
+ let mongooseQuery = this.EmployeeModel.find(query);
688
+ if (options.session) {
689
+ mongooseQuery = mongooseQuery.session(options.session);
690
+ }
691
+ const employees = await mongooseQuery.exec();
692
+ return {
693
+ total: employees.length,
694
+ active: employees.filter(isActive).length,
695
+ employed: employees.filter(isEmployed).length,
696
+ canReceiveSalary: employees.filter(canReceiveSalary).length,
697
+ byStatus: this.groupByStatus(employees),
698
+ byDepartment: this.groupByDepartment(employees)
699
+ };
700
+ }
701
+ /**
702
+ * Group employees by status
703
+ */
704
+ groupByStatus(employees) {
705
+ return employees.reduce(
706
+ (acc, emp) => {
707
+ acc[emp.status] = (acc[emp.status] || 0) + 1;
708
+ return acc;
709
+ },
710
+ {}
711
+ );
712
+ }
713
+ /**
714
+ * Group employees by department
715
+ */
716
+ groupByDepartment(employees) {
717
+ return employees.reduce(
718
+ (acc, emp) => {
719
+ const dept = emp.department || "unassigned";
720
+ acc[dept] = (acc[dept] || 0) + 1;
721
+ return acc;
722
+ },
723
+ {}
724
+ );
725
+ }
726
+ /**
727
+ * Check if employee is active
728
+ */
729
+ isActive(employee2) {
730
+ return isActive(employee2);
731
+ }
732
+ /**
733
+ * Check if employee is employed
734
+ */
735
+ isEmployed(employee2) {
736
+ return isEmployed(employee2);
737
+ }
738
+ /**
739
+ * Check if employee can receive salary
740
+ */
741
+ canReceiveSalary(employee2) {
742
+ return canReceiveSalary(employee2);
743
+ }
744
+ };
745
+ function createEmployeeService(EmployeeModel) {
746
+ return new EmployeeService(EmployeeModel);
747
+ }
748
+
749
+ // src/utils/calculation.ts
750
+ function sumBy(items, getter) {
751
+ return items.reduce((total, item) => total + getter(item), 0);
752
+ }
753
+ function sumAllowances(allowances) {
754
+ return sumBy(allowances, (a) => a.amount);
755
+ }
756
+ function sumDeductions(deductions) {
757
+ return sumBy(deductions, (d) => d.amount);
758
+ }
759
+ function applyPercentage(amount, percentage) {
760
+ return Math.round(amount * (percentage / 100));
761
+ }
762
+ function calculateGross(baseAmount, allowances) {
763
+ return baseAmount + sumAllowances(allowances);
764
+ }
765
+ function calculateNet(gross, deductions) {
766
+ return Math.max(0, gross - sumDeductions(deductions));
767
+ }
768
+
769
+ // src/factories/payroll.factory.ts
770
+ var PayrollFactory = class {
771
+ /**
772
+ * Create payroll data object
773
+ */
774
+ static create(params) {
775
+ const {
776
+ employeeId,
777
+ organizationId,
778
+ baseAmount,
779
+ allowances = [],
780
+ deductions = [],
781
+ period = {},
782
+ metadata = {}
783
+ } = params;
784
+ const calculatedAllowances = this.calculateAllowances(baseAmount, allowances);
785
+ const calculatedDeductions = this.calculateDeductions(baseAmount, deductions);
786
+ const gross = calculateGross(baseAmount, calculatedAllowances);
787
+ const net = calculateNet(gross, calculatedDeductions);
788
+ return {
789
+ employeeId,
790
+ organizationId,
791
+ period: this.createPeriod(period),
792
+ breakdown: {
793
+ baseAmount,
794
+ allowances: calculatedAllowances,
795
+ deductions: calculatedDeductions,
796
+ grossSalary: gross,
797
+ netSalary: net
798
+ },
799
+ status: "pending",
800
+ processedAt: null,
801
+ paidAt: null,
802
+ metadata: {
803
+ currency: metadata.currency || HRM_CONFIG.payroll.defaultCurrency,
804
+ paymentMethod: metadata.paymentMethod,
805
+ notes: metadata.notes
806
+ }
807
+ };
808
+ }
809
+ /**
810
+ * Create pay period
811
+ */
812
+ static createPeriod(params) {
813
+ const now = /* @__PURE__ */ new Date();
814
+ const month = params.month || now.getMonth() + 1;
815
+ const year = params.year || now.getFullYear();
816
+ const period = getPayPeriod(month, year);
817
+ return {
818
+ ...period,
819
+ payDate: params.payDate || /* @__PURE__ */ new Date()
820
+ };
821
+ }
822
+ /**
823
+ * Calculate allowances from base amount
824
+ */
825
+ static calculateAllowances(baseAmount, allowances) {
826
+ return allowances.map((allowance) => {
827
+ const amount = allowance.isPercentage && allowance.value !== void 0 ? Math.round(baseAmount * allowance.value / 100) : allowance.amount;
828
+ return {
829
+ type: allowance.type,
830
+ amount,
831
+ taxable: allowance.taxable ?? true
832
+ };
833
+ });
834
+ }
835
+ /**
836
+ * Calculate deductions from base amount
837
+ */
838
+ static calculateDeductions(baseAmount, deductions) {
839
+ return deductions.map((deduction) => {
840
+ const amount = deduction.isPercentage && deduction.value !== void 0 ? Math.round(baseAmount * deduction.value / 100) : deduction.amount;
841
+ return {
842
+ type: deduction.type,
843
+ amount,
844
+ description: deduction.description
845
+ };
846
+ });
847
+ }
848
+ /**
849
+ * Create bonus object
850
+ */
851
+ static createBonus(params) {
852
+ return {
853
+ type: params.type,
854
+ amount: params.amount,
855
+ reason: params.reason,
856
+ approvedBy: params.approvedBy,
857
+ approvedAt: /* @__PURE__ */ new Date()
858
+ };
859
+ }
860
+ /**
861
+ * Mark payroll as paid (immutable)
862
+ * Sets both top-level transactionId and metadata for compatibility
863
+ */
864
+ static markAsPaid(payroll2, params = {}) {
865
+ return {
866
+ ...payroll2,
867
+ status: "paid",
868
+ paidAt: params.paidAt || /* @__PURE__ */ new Date(),
869
+ processedAt: payroll2.processedAt || params.paidAt || /* @__PURE__ */ new Date(),
870
+ transactionId: params.transactionId || payroll2.transactionId,
871
+ metadata: {
872
+ ...payroll2.metadata,
873
+ transactionId: params.transactionId,
874
+ paymentMethod: params.paymentMethod || payroll2.metadata?.paymentMethod
875
+ }
876
+ };
877
+ }
878
+ /**
879
+ * Mark payroll as processed (immutable)
880
+ */
881
+ static markAsProcessed(payroll2, params = {}) {
882
+ return {
883
+ ...payroll2,
884
+ status: "processing",
885
+ processedAt: params.processedAt || /* @__PURE__ */ new Date()
886
+ };
887
+ }
888
+ };
889
+ var BatchPayrollFactory = class {
890
+ /**
891
+ * Create payroll records for multiple employees
892
+ */
893
+ static createBatch(employees, params) {
894
+ return employees.map(
895
+ (employee2) => PayrollFactory.create({
896
+ employeeId: employee2._id,
897
+ organizationId: params.organizationId || employee2.organizationId,
898
+ baseAmount: employee2.compensation.baseAmount,
899
+ allowances: employee2.compensation.allowances || [],
900
+ deductions: employee2.compensation.deductions || [],
901
+ period: { month: params.month, year: params.year },
902
+ metadata: { currency: employee2.compensation.currency }
903
+ })
904
+ );
905
+ }
906
+ /**
907
+ * Calculate total payroll amounts
908
+ */
909
+ static calculateTotalPayroll(payrolls) {
910
+ return payrolls.reduce(
911
+ (totals, payroll2) => ({
912
+ count: totals.count + 1,
913
+ totalGross: totals.totalGross + payroll2.breakdown.grossSalary,
914
+ totalNet: totals.totalNet + payroll2.breakdown.netSalary,
915
+ totalAllowances: totals.totalAllowances + sumAllowances(payroll2.breakdown.allowances),
916
+ totalDeductions: totals.totalDeductions + sumDeductions(payroll2.breakdown.deductions)
917
+ }),
918
+ { count: 0, totalGross: 0, totalNet: 0, totalAllowances: 0, totalDeductions: 0 }
919
+ );
920
+ }
921
+ };
922
+
923
+ // src/services/payroll.service.ts
924
+ var PayrollService = class {
925
+ constructor(PayrollModel, employeeService) {
926
+ this.PayrollModel = PayrollModel;
927
+ this.employeeService = employeeService;
928
+ }
929
+ /**
930
+ * Find payroll by ID
931
+ */
932
+ async findById(payrollId, options = {}) {
933
+ let query = this.PayrollModel.findById(toObjectId(payrollId));
934
+ if (options.session) {
935
+ query = query.session(options.session);
936
+ }
937
+ return query.exec();
938
+ }
939
+ /**
940
+ * Find payrolls by employee
941
+ */
942
+ async findByEmployee(employeeId, organizationId, options = {}) {
943
+ const query = payroll().forEmployee(employeeId).forOrganization(organizationId).build();
944
+ let mongooseQuery = this.PayrollModel.find(query).sort({ "period.year": -1, "period.month": -1 }).limit(options.limit || 12);
945
+ if (options.session) {
946
+ mongooseQuery = mongooseQuery.session(options.session);
947
+ }
948
+ return mongooseQuery.exec();
949
+ }
950
+ /**
951
+ * Find payrolls for a period
952
+ */
953
+ async findForPeriod(organizationId, month, year, options = {}) {
954
+ const query = payroll().forOrganization(organizationId).forPeriod(month, year).build();
955
+ let mongooseQuery = this.PayrollModel.find(query);
956
+ if (options.session) {
957
+ mongooseQuery = mongooseQuery.session(options.session);
958
+ }
959
+ return mongooseQuery.exec();
960
+ }
961
+ /**
962
+ * Find pending payrolls
963
+ */
964
+ async findPending(organizationId, month, year, options = {}) {
965
+ const query = payroll().forOrganization(organizationId).forPeriod(month, year).pending().build();
966
+ let mongooseQuery = this.PayrollModel.find(query);
967
+ if (options.session) {
968
+ mongooseQuery = mongooseQuery.session(options.session);
969
+ }
970
+ return mongooseQuery.exec();
971
+ }
972
+ /**
973
+ * Find payroll by employee and period
974
+ */
975
+ async findByEmployeeAndPeriod(employeeId, organizationId, month, year, options = {}) {
976
+ const query = payroll().forEmployee(employeeId).forOrganization(organizationId).forPeriod(month, year).build();
977
+ let mongooseQuery = this.PayrollModel.findOne(query);
978
+ if (options.session) {
979
+ mongooseQuery = mongooseQuery.session(options.session);
980
+ }
981
+ return mongooseQuery.exec();
982
+ }
983
+ /**
984
+ * Create payroll record
985
+ */
986
+ async create(data, options = {}) {
987
+ const [payroll2] = await this.PayrollModel.create([data], {
988
+ session: options.session
989
+ });
990
+ logger.info("Payroll record created", {
991
+ payrollId: payroll2._id.toString(),
992
+ employeeId: payroll2.employeeId.toString()
993
+ });
994
+ return payroll2;
995
+ }
996
+ /**
997
+ * Generate payroll for employee
998
+ */
999
+ async generateForEmployee(employeeId, organizationId, month, year, options = {}) {
1000
+ const employee2 = await this.employeeService.findById(employeeId, options);
1001
+ if (!employee2) {
1002
+ throw new Error("Employee not found");
1003
+ }
1004
+ if (!canReceiveSalary(employee2)) {
1005
+ throw new Error("Employee not eligible for payroll");
1006
+ }
1007
+ const existing = await this.findByEmployeeAndPeriod(
1008
+ employeeId,
1009
+ organizationId,
1010
+ month,
1011
+ year,
1012
+ options
1013
+ );
1014
+ if (existing) {
1015
+ throw new Error("Payroll already exists for this period");
1016
+ }
1017
+ const payrollData = PayrollFactory.create({
1018
+ employeeId,
1019
+ organizationId,
1020
+ baseAmount: employee2.compensation.baseAmount,
1021
+ allowances: employee2.compensation.allowances || [],
1022
+ deductions: employee2.compensation.deductions || [],
1023
+ period: { month, year },
1024
+ metadata: { currency: employee2.compensation.currency }
1025
+ });
1026
+ return this.create(payrollData, options);
1027
+ }
1028
+ /**
1029
+ * Generate batch payroll
1030
+ */
1031
+ async generateBatch(organizationId, month, year, options = {}) {
1032
+ const employees = await this.employeeService.findEligibleForPayroll(
1033
+ organizationId,
1034
+ options
1035
+ );
1036
+ if (employees.length === 0) {
1037
+ return {
1038
+ success: true,
1039
+ generated: 0,
1040
+ skipped: 0,
1041
+ payrolls: [],
1042
+ message: "No eligible employees"
1043
+ };
1044
+ }
1045
+ const existingPayrolls = await this.findForPeriod(
1046
+ organizationId,
1047
+ month,
1048
+ year,
1049
+ options
1050
+ );
1051
+ const existingEmployeeIds = new Set(
1052
+ existingPayrolls.map((p) => p.employeeId.toString())
1053
+ );
1054
+ const eligibleEmployees = employees.filter(
1055
+ (emp) => !existingEmployeeIds.has(emp._id.toString())
1056
+ );
1057
+ if (eligibleEmployees.length === 0) {
1058
+ return {
1059
+ success: true,
1060
+ generated: 0,
1061
+ skipped: employees.length,
1062
+ payrolls: [],
1063
+ message: "Payrolls already exist for all employees"
1064
+ };
1065
+ }
1066
+ const payrollsData = BatchPayrollFactory.createBatch(eligibleEmployees, {
1067
+ month,
1068
+ year,
1069
+ organizationId
1070
+ });
1071
+ const created = await this.PayrollModel.insertMany(payrollsData, {
1072
+ session: options.session
1073
+ });
1074
+ logger.info("Batch payroll generated", {
1075
+ organizationId: organizationId.toString(),
1076
+ month,
1077
+ year,
1078
+ count: created.length
1079
+ });
1080
+ return {
1081
+ success: true,
1082
+ generated: created.length,
1083
+ skipped: existingEmployeeIds.size,
1084
+ payrolls: created,
1085
+ message: `Generated ${created.length} payrolls`
1086
+ };
1087
+ }
1088
+ /**
1089
+ * Mark payroll as paid
1090
+ */
1091
+ async markAsPaid(payrollId, paymentDetails = {}, options = {}) {
1092
+ const payroll2 = await this.findById(payrollId, options);
1093
+ if (!payroll2) {
1094
+ throw new Error("Payroll not found");
1095
+ }
1096
+ if (payroll2.status === "paid") {
1097
+ throw new Error("Payroll already paid");
1098
+ }
1099
+ const payrollObj = payroll2.toObject();
1100
+ const updatedData = PayrollFactory.markAsPaid(payrollObj, paymentDetails);
1101
+ const updated = await this.PayrollModel.findByIdAndUpdate(
1102
+ payrollId,
1103
+ updatedData,
1104
+ { new: true, runValidators: true, session: options.session }
1105
+ );
1106
+ if (!updated) {
1107
+ throw new Error("Failed to update payroll");
1108
+ }
1109
+ logger.info("Payroll marked as paid", {
1110
+ payrollId: payrollId.toString()
1111
+ });
1112
+ return updated;
1113
+ }
1114
+ /**
1115
+ * Mark payroll as processed
1116
+ */
1117
+ async markAsProcessed(payrollId, options = {}) {
1118
+ const payroll2 = await this.findById(payrollId, options);
1119
+ if (!payroll2) {
1120
+ throw new Error("Payroll not found");
1121
+ }
1122
+ const payrollObj = payroll2.toObject();
1123
+ const updatedData = PayrollFactory.markAsProcessed(payrollObj);
1124
+ const updated = await this.PayrollModel.findByIdAndUpdate(
1125
+ payrollId,
1126
+ updatedData,
1127
+ { new: true, runValidators: true, session: options.session }
1128
+ );
1129
+ if (!updated) {
1130
+ throw new Error("Failed to update payroll");
1131
+ }
1132
+ return updated;
1133
+ }
1134
+ /**
1135
+ * Calculate period summary
1136
+ */
1137
+ async calculatePeriodSummary(organizationId, month, year, options = {}) {
1138
+ const payrolls = await this.findForPeriod(organizationId, month, year, options);
1139
+ const summary = BatchPayrollFactory.calculateTotalPayroll(payrolls);
1140
+ return {
1141
+ period: { month, year },
1142
+ ...summary,
1143
+ byStatus: this.groupByStatus(payrolls)
1144
+ };
1145
+ }
1146
+ /**
1147
+ * Get employee payroll history
1148
+ */
1149
+ async getEmployeePayrollHistory(employeeId, organizationId, limit = 12, options = {}) {
1150
+ return this.findByEmployee(employeeId, organizationId, { ...options, limit });
1151
+ }
1152
+ /**
1153
+ * Get overview stats
1154
+ */
1155
+ async getOverviewStats(organizationId, options = {}) {
1156
+ const { month, year } = getCurrentPeriod();
1157
+ const result = await this.calculatePeriodSummary(organizationId, month, year, options);
1158
+ return {
1159
+ currentPeriod: result.period,
1160
+ count: result.count,
1161
+ totalGross: result.totalGross,
1162
+ totalNet: result.totalNet,
1163
+ totalAllowances: result.totalAllowances,
1164
+ totalDeductions: result.totalDeductions,
1165
+ byStatus: result.byStatus
1166
+ };
1167
+ }
1168
+ /**
1169
+ * Group payrolls by status
1170
+ */
1171
+ groupByStatus(payrolls) {
1172
+ return payrolls.reduce(
1173
+ (acc, payroll2) => {
1174
+ acc[payroll2.status] = (acc[payroll2.status] || 0) + 1;
1175
+ return acc;
1176
+ },
1177
+ {}
1178
+ );
1179
+ }
1180
+ };
1181
+ function createPayrollService(PayrollModel, employeeService) {
1182
+ return new PayrollService(PayrollModel, employeeService);
1183
+ }
1184
+
1185
+ // src/factories/compensation.factory.ts
1186
+ var CompensationFactory = class {
1187
+ /**
1188
+ * Create compensation object
1189
+ */
1190
+ static create(params) {
1191
+ const {
1192
+ baseAmount,
1193
+ frequency = "monthly",
1194
+ currency = HRM_CONFIG.payroll.defaultCurrency,
1195
+ allowances = [],
1196
+ deductions = [],
1197
+ effectiveFrom = /* @__PURE__ */ new Date()
1198
+ } = params;
1199
+ return {
1200
+ baseAmount,
1201
+ frequency,
1202
+ currency,
1203
+ allowances: allowances.map((a) => this.createAllowance(a, baseAmount)),
1204
+ deductions: deductions.map((d) => this.createDeduction(d, baseAmount)),
1205
+ effectiveFrom,
1206
+ lastModified: /* @__PURE__ */ new Date()
1207
+ };
1208
+ }
1209
+ /**
1210
+ * Create allowance
1211
+ */
1212
+ static createAllowance(params, baseAmount) {
1213
+ const amount = params.isPercentage && baseAmount ? applyPercentage(baseAmount, params.value) : params.value;
1214
+ return {
1215
+ type: params.type,
1216
+ name: params.name || params.type,
1217
+ amount,
1218
+ isPercentage: params.isPercentage ?? false,
1219
+ value: params.isPercentage ? params.value : void 0,
1220
+ taxable: params.taxable ?? true,
1221
+ recurring: true,
1222
+ effectiveFrom: /* @__PURE__ */ new Date()
1223
+ };
1224
+ }
1225
+ /**
1226
+ * Create deduction
1227
+ */
1228
+ static createDeduction(params, baseAmount) {
1229
+ const amount = params.isPercentage && baseAmount ? applyPercentage(baseAmount, params.value) : params.value;
1230
+ return {
1231
+ type: params.type,
1232
+ name: params.name || params.type,
1233
+ amount,
1234
+ isPercentage: params.isPercentage ?? false,
1235
+ value: params.isPercentage ? params.value : void 0,
1236
+ auto: params.auto ?? false,
1237
+ recurring: true,
1238
+ effectiveFrom: /* @__PURE__ */ new Date()
1239
+ };
1240
+ }
1241
+ /**
1242
+ * Update base amount (immutable)
1243
+ */
1244
+ static updateBaseAmount(compensation, newAmount, effectiveFrom = /* @__PURE__ */ new Date()) {
1245
+ return {
1246
+ ...compensation,
1247
+ baseAmount: newAmount,
1248
+ lastModified: effectiveFrom
1249
+ };
1250
+ }
1251
+ /**
1252
+ * Add allowance (immutable)
1253
+ */
1254
+ static addAllowance(compensation, allowance) {
1255
+ return {
1256
+ ...compensation,
1257
+ allowances: [
1258
+ ...compensation.allowances,
1259
+ this.createAllowance(allowance, compensation.baseAmount)
1260
+ ],
1261
+ lastModified: /* @__PURE__ */ new Date()
1262
+ };
1263
+ }
1264
+ /**
1265
+ * Remove allowance (immutable)
1266
+ */
1267
+ static removeAllowance(compensation, allowanceType) {
1268
+ return {
1269
+ ...compensation,
1270
+ allowances: compensation.allowances.filter((a) => a.type !== allowanceType),
1271
+ lastModified: /* @__PURE__ */ new Date()
1272
+ };
1273
+ }
1274
+ /**
1275
+ * Add deduction (immutable)
1276
+ */
1277
+ static addDeduction(compensation, deduction) {
1278
+ return {
1279
+ ...compensation,
1280
+ deductions: [
1281
+ ...compensation.deductions,
1282
+ this.createDeduction(deduction, compensation.baseAmount)
1283
+ ],
1284
+ lastModified: /* @__PURE__ */ new Date()
1285
+ };
1286
+ }
1287
+ /**
1288
+ * Remove deduction (immutable)
1289
+ */
1290
+ static removeDeduction(compensation, deductionType) {
1291
+ return {
1292
+ ...compensation,
1293
+ deductions: compensation.deductions.filter((d) => d.type !== deductionType),
1294
+ lastModified: /* @__PURE__ */ new Date()
1295
+ };
1296
+ }
1297
+ /**
1298
+ * Calculate compensation breakdown
1299
+ */
1300
+ static calculateBreakdown(compensation) {
1301
+ const { baseAmount, allowances, deductions } = compensation;
1302
+ const calculatedAllowances = allowances.map((a) => ({
1303
+ ...a,
1304
+ calculatedAmount: a.isPercentage && a.value !== void 0 ? applyPercentage(baseAmount, a.value) : a.amount
1305
+ }));
1306
+ const calculatedDeductions = deductions.map((d) => ({
1307
+ ...d,
1308
+ calculatedAmount: d.isPercentage && d.value !== void 0 ? applyPercentage(baseAmount, d.value) : d.amount
1309
+ }));
1310
+ const grossAmount = calculateGross(
1311
+ baseAmount,
1312
+ calculatedAllowances.map((a) => ({ amount: a.calculatedAmount }))
1313
+ );
1314
+ const netAmount = calculateNet(
1315
+ grossAmount,
1316
+ calculatedDeductions.map((d) => ({ amount: d.calculatedAmount }))
1317
+ );
1318
+ return {
1319
+ baseAmount,
1320
+ allowances: calculatedAllowances,
1321
+ deductions: calculatedDeductions,
1322
+ grossAmount,
1323
+ netAmount: Math.max(0, netAmount)
1324
+ };
1325
+ }
1326
+ /**
1327
+ * Apply salary increment (immutable)
1328
+ */
1329
+ static applyIncrement(compensation, params) {
1330
+ const newBaseAmount = params.amount ? compensation.baseAmount + params.amount : compensation.baseAmount * (1 + (params.percentage || 0) / 100);
1331
+ return this.updateBaseAmount(
1332
+ compensation,
1333
+ Math.round(newBaseAmount),
1334
+ params.effectiveFrom
1335
+ );
1336
+ }
1337
+ };
1338
+ var CompensationBuilder = class {
1339
+ data = {
1340
+ baseAmount: 0,
1341
+ frequency: "monthly",
1342
+ currency: HRM_CONFIG.payroll.defaultCurrency,
1343
+ allowances: [],
1344
+ deductions: []
1345
+ };
1346
+ /**
1347
+ * Set base amount
1348
+ */
1349
+ withBase(amount, frequency = "monthly", currency = HRM_CONFIG.payroll.defaultCurrency) {
1350
+ this.data.baseAmount = amount;
1351
+ this.data.frequency = frequency;
1352
+ this.data.currency = currency;
1353
+ return this;
1354
+ }
1355
+ /**
1356
+ * Add allowance
1357
+ */
1358
+ addAllowance(type, value, isPercentage = false, name) {
1359
+ this.data.allowances = [
1360
+ ...this.data.allowances || [],
1361
+ { type, value, isPercentage, name }
1362
+ ];
1363
+ return this;
1364
+ }
1365
+ /**
1366
+ * Add deduction
1367
+ */
1368
+ addDeduction(type, value, isPercentage = false, name) {
1369
+ this.data.deductions = [
1370
+ ...this.data.deductions || [],
1371
+ { type, value, isPercentage, name }
1372
+ ];
1373
+ return this;
1374
+ }
1375
+ /**
1376
+ * Set effective date
1377
+ */
1378
+ effectiveFrom(date) {
1379
+ this.data.effectiveFrom = date;
1380
+ return this;
1381
+ }
1382
+ /**
1383
+ * Build compensation
1384
+ */
1385
+ build() {
1386
+ if (!this.data.baseAmount) {
1387
+ throw new Error("baseAmount is required");
1388
+ }
1389
+ return CompensationFactory.create(this.data);
1390
+ }
1391
+ };
1392
+ var CompensationPresets = {
1393
+ /**
1394
+ * Basic compensation (base only)
1395
+ */
1396
+ basic(baseAmount) {
1397
+ return new CompensationBuilder().withBase(baseAmount).build();
1398
+ },
1399
+ /**
1400
+ * With house rent allowance
1401
+ */
1402
+ withHouseRent(baseAmount, rentPercentage = 50) {
1403
+ return new CompensationBuilder().withBase(baseAmount).addAllowance("housing", rentPercentage, true, "House Rent").build();
1404
+ },
1405
+ /**
1406
+ * With medical allowance
1407
+ */
1408
+ withMedical(baseAmount, medicalPercentage = 10) {
1409
+ return new CompensationBuilder().withBase(baseAmount).addAllowance("medical", medicalPercentage, true, "Medical Allowance").build();
1410
+ },
1411
+ /**
1412
+ * Standard package (house rent + medical + transport)
1413
+ */
1414
+ standard(baseAmount) {
1415
+ return new CompensationBuilder().withBase(baseAmount).addAllowance("housing", 50, true, "House Rent").addAllowance("medical", 10, true, "Medical Allowance").addAllowance("transport", 5, true, "Transport Allowance").build();
1416
+ },
1417
+ /**
1418
+ * With provident fund
1419
+ */
1420
+ withProvidentFund(baseAmount, pfPercentage = 10) {
1421
+ return new CompensationBuilder().withBase(baseAmount).addAllowance("housing", 50, true, "House Rent").addAllowance("medical", 10, true, "Medical Allowance").addDeduction("provident_fund", pfPercentage, true, "Provident Fund").build();
1422
+ },
1423
+ /**
1424
+ * Executive package
1425
+ */
1426
+ executive(baseAmount) {
1427
+ return new CompensationBuilder().withBase(baseAmount).addAllowance("housing", 60, true, "House Rent").addAllowance("medical", 15, true, "Medical Allowance").addAllowance("transport", 10, true, "Transport Allowance").addAllowance("mobile", 5, true, "Mobile Allowance").addDeduction("provident_fund", 10, true, "Provident Fund").build();
1428
+ }
1429
+ };
1430
+
1431
+ // src/services/compensation.service.ts
1432
+ var CompensationService = class {
1433
+ constructor(EmployeeModel) {
1434
+ this.EmployeeModel = EmployeeModel;
1435
+ }
1436
+ /**
1437
+ * Get employee compensation
1438
+ */
1439
+ async getEmployeeCompensation(employeeId, options = {}) {
1440
+ let query = this.EmployeeModel.findById(toObjectId(employeeId));
1441
+ if (options.session) {
1442
+ query = query.session(options.session);
1443
+ }
1444
+ const employee2 = await query.exec();
1445
+ if (!employee2) {
1446
+ throw new Error("Employee not found");
1447
+ }
1448
+ return employee2.compensation;
1449
+ }
1450
+ /**
1451
+ * Calculate compensation breakdown
1452
+ */
1453
+ async calculateBreakdown(employeeId, options = {}) {
1454
+ const compensation = await this.getEmployeeCompensation(employeeId, options);
1455
+ return CompensationFactory.calculateBreakdown(compensation);
1456
+ }
1457
+ /**
1458
+ * Update base amount
1459
+ */
1460
+ async updateBaseAmount(employeeId, newAmount, effectiveFrom = /* @__PURE__ */ new Date(), options = {}) {
1461
+ const employee2 = await this.findEmployee(employeeId, options);
1462
+ const updatedCompensation = CompensationFactory.updateBaseAmount(
1463
+ employee2.compensation,
1464
+ newAmount,
1465
+ effectiveFrom
1466
+ );
1467
+ employee2.compensation = updatedCompensation;
1468
+ await employee2.save({ session: options.session });
1469
+ logger.info("Compensation base amount updated", {
1470
+ employeeId: employee2.employeeId,
1471
+ newAmount
1472
+ });
1473
+ return this.calculateBreakdown(employeeId, options);
1474
+ }
1475
+ /**
1476
+ * Apply salary increment
1477
+ */
1478
+ async applyIncrement(employeeId, params, options = {}) {
1479
+ const employee2 = await this.findEmployee(employeeId, options);
1480
+ const previousAmount = employee2.compensation.baseAmount;
1481
+ const updatedCompensation = CompensationFactory.applyIncrement(
1482
+ employee2.compensation,
1483
+ params
1484
+ );
1485
+ employee2.compensation = updatedCompensation;
1486
+ await employee2.save({ session: options.session });
1487
+ logger.info("Salary increment applied", {
1488
+ employeeId: employee2.employeeId,
1489
+ previousAmount,
1490
+ newAmount: updatedCompensation.baseAmount,
1491
+ percentage: params.percentage
1492
+ });
1493
+ return this.calculateBreakdown(employeeId, options);
1494
+ }
1495
+ /**
1496
+ * Add allowance
1497
+ */
1498
+ async addAllowance(employeeId, allowance, options = {}) {
1499
+ const employee2 = await this.findEmployee(employeeId, options);
1500
+ const updatedCompensation = CompensationFactory.addAllowance(
1501
+ employee2.compensation,
1502
+ allowance
1503
+ );
1504
+ employee2.compensation = updatedCompensation;
1505
+ await employee2.save({ session: options.session });
1506
+ logger.info("Allowance added", {
1507
+ employeeId: employee2.employeeId,
1508
+ type: allowance.type,
1509
+ value: allowance.value
1510
+ });
1511
+ return this.calculateBreakdown(employeeId, options);
1512
+ }
1513
+ /**
1514
+ * Remove allowance
1515
+ */
1516
+ async removeAllowance(employeeId, allowanceType, options = {}) {
1517
+ const employee2 = await this.findEmployee(employeeId, options);
1518
+ const updatedCompensation = CompensationFactory.removeAllowance(
1519
+ employee2.compensation,
1520
+ allowanceType
1521
+ );
1522
+ employee2.compensation = updatedCompensation;
1523
+ await employee2.save({ session: options.session });
1524
+ logger.info("Allowance removed", {
1525
+ employeeId: employee2.employeeId,
1526
+ type: allowanceType
1527
+ });
1528
+ return this.calculateBreakdown(employeeId, options);
1529
+ }
1530
+ /**
1531
+ * Add deduction
1532
+ */
1533
+ async addDeduction(employeeId, deduction, options = {}) {
1534
+ const employee2 = await this.findEmployee(employeeId, options);
1535
+ const updatedCompensation = CompensationFactory.addDeduction(
1536
+ employee2.compensation,
1537
+ deduction
1538
+ );
1539
+ employee2.compensation = updatedCompensation;
1540
+ await employee2.save({ session: options.session });
1541
+ logger.info("Deduction added", {
1542
+ employeeId: employee2.employeeId,
1543
+ type: deduction.type,
1544
+ value: deduction.value
1545
+ });
1546
+ return this.calculateBreakdown(employeeId, options);
1547
+ }
1548
+ /**
1549
+ * Remove deduction
1550
+ */
1551
+ async removeDeduction(employeeId, deductionType, options = {}) {
1552
+ const employee2 = await this.findEmployee(employeeId, options);
1553
+ const updatedCompensation = CompensationFactory.removeDeduction(
1554
+ employee2.compensation,
1555
+ deductionType
1556
+ );
1557
+ employee2.compensation = updatedCompensation;
1558
+ await employee2.save({ session: options.session });
1559
+ logger.info("Deduction removed", {
1560
+ employeeId: employee2.employeeId,
1561
+ type: deductionType
1562
+ });
1563
+ return this.calculateBreakdown(employeeId, options);
1564
+ }
1565
+ /**
1566
+ * Set standard compensation
1567
+ */
1568
+ async setStandardCompensation(employeeId, baseAmount, options = {}) {
1569
+ const employee2 = await this.findEmployee(employeeId, options);
1570
+ employee2.compensation = CompensationPresets.standard(baseAmount);
1571
+ await employee2.save({ session: options.session });
1572
+ logger.info("Standard compensation set", {
1573
+ employeeId: employee2.employeeId,
1574
+ baseAmount
1575
+ });
1576
+ return this.calculateBreakdown(employeeId, options);
1577
+ }
1578
+ /**
1579
+ * Compare compensation between two employees
1580
+ */
1581
+ async compareCompensation(employeeId1, employeeId2, options = {}) {
1582
+ const breakdown1 = await this.calculateBreakdown(employeeId1, options);
1583
+ const breakdown2 = await this.calculateBreakdown(employeeId2, options);
1584
+ return {
1585
+ employee1: breakdown1,
1586
+ employee2: breakdown2,
1587
+ difference: {
1588
+ base: breakdown2.baseAmount - breakdown1.baseAmount,
1589
+ gross: breakdown2.grossAmount - breakdown1.grossAmount,
1590
+ net: breakdown2.netAmount - breakdown1.netAmount
1591
+ },
1592
+ ratio: {
1593
+ base: breakdown1.baseAmount > 0 ? breakdown2.baseAmount / breakdown1.baseAmount : 0,
1594
+ gross: breakdown1.grossAmount > 0 ? breakdown2.grossAmount / breakdown1.grossAmount : 0,
1595
+ net: breakdown1.netAmount > 0 ? breakdown2.netAmount / breakdown1.netAmount : 0
1596
+ }
1597
+ };
1598
+ }
1599
+ /**
1600
+ * Get department compensation stats
1601
+ */
1602
+ async getDepartmentCompensationStats(organizationId, department, options = {}) {
1603
+ let query = this.EmployeeModel.find({
1604
+ organizationId: toObjectId(organizationId),
1605
+ department,
1606
+ status: { $in: ["active", "on_leave"] }
1607
+ });
1608
+ if (options.session) {
1609
+ query = query.session(options.session);
1610
+ }
1611
+ const employees = await query.exec();
1612
+ const breakdowns = employees.map(
1613
+ (emp) => CompensationFactory.calculateBreakdown(emp.compensation)
1614
+ );
1615
+ const totals = breakdowns.reduce(
1616
+ (acc, breakdown) => ({
1617
+ totalBase: acc.totalBase + breakdown.baseAmount,
1618
+ totalGross: acc.totalGross + breakdown.grossAmount,
1619
+ totalNet: acc.totalNet + breakdown.netAmount
1620
+ }),
1621
+ { totalBase: 0, totalGross: 0, totalNet: 0 }
1622
+ );
1623
+ const count = employees.length || 1;
1624
+ return {
1625
+ department,
1626
+ employeeCount: employees.length,
1627
+ ...totals,
1628
+ averageBase: Math.round(totals.totalBase / count),
1629
+ averageGross: Math.round(totals.totalGross / count),
1630
+ averageNet: Math.round(totals.totalNet / count)
1631
+ };
1632
+ }
1633
+ /**
1634
+ * Get organization compensation stats
1635
+ */
1636
+ async getOrganizationCompensationStats(organizationId, options = {}) {
1637
+ let query = this.EmployeeModel.find({
1638
+ organizationId: toObjectId(organizationId),
1639
+ status: { $in: ["active", "on_leave"] }
1640
+ });
1641
+ if (options.session) {
1642
+ query = query.session(options.session);
1643
+ }
1644
+ const employees = await query.exec();
1645
+ const breakdowns = employees.map(
1646
+ (emp) => CompensationFactory.calculateBreakdown(emp.compensation)
1647
+ );
1648
+ const totals = breakdowns.reduce(
1649
+ (acc, breakdown) => ({
1650
+ totalBase: acc.totalBase + breakdown.baseAmount,
1651
+ totalGross: acc.totalGross + breakdown.grossAmount,
1652
+ totalNet: acc.totalNet + breakdown.netAmount
1653
+ }),
1654
+ { totalBase: 0, totalGross: 0, totalNet: 0 }
1655
+ );
1656
+ const byDepartment = {};
1657
+ employees.forEach((emp, i) => {
1658
+ const dept = emp.department || "unassigned";
1659
+ if (!byDepartment[dept]) {
1660
+ byDepartment[dept] = { count: 0, totalNet: 0 };
1661
+ }
1662
+ byDepartment[dept].count++;
1663
+ byDepartment[dept].totalNet += breakdowns[i].netAmount;
1664
+ });
1665
+ const count = employees.length || 1;
1666
+ return {
1667
+ employeeCount: employees.length,
1668
+ ...totals,
1669
+ averageBase: Math.round(totals.totalBase / count),
1670
+ averageGross: Math.round(totals.totalGross / count),
1671
+ averageNet: Math.round(totals.totalNet / count),
1672
+ byDepartment
1673
+ };
1674
+ }
1675
+ /**
1676
+ * Find employee helper
1677
+ */
1678
+ async findEmployee(employeeId, options = {}) {
1679
+ let query = this.EmployeeModel.findById(toObjectId(employeeId));
1680
+ if (options.session) {
1681
+ query = query.session(options.session);
1682
+ }
1683
+ const employee2 = await query.exec();
1684
+ if (!employee2) {
1685
+ throw new Error("Employee not found");
1686
+ }
1687
+ return employee2;
1688
+ }
1689
+ };
1690
+ function createCompensationService(EmployeeModel) {
1691
+ return new CompensationService(EmployeeModel);
1692
+ }
1693
+
1694
+ export { CompensationService, EmployeeService, PayrollService, createCompensationService, createEmployeeService, createPayrollService };
1695
+ //# sourceMappingURL=index.js.map
1696
+ //# sourceMappingURL=index.js.map