@classytic/payroll 1.0.2 → 2.3.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.

Potentially problematic release.


This version of @classytic/payroll might be problematic. Click here for more details.

Files changed (78) hide show
  1. package/README.md +2599 -574
  2. package/dist/calculators/index.d.ts +433 -0
  3. package/dist/calculators/index.js +283 -0
  4. package/dist/calculators/index.js.map +1 -0
  5. package/dist/core/index.d.ts +314 -0
  6. package/dist/core/index.js +1166 -0
  7. package/dist/core/index.js.map +1 -0
  8. package/dist/employee-identity-DXhgOgXE.d.ts +473 -0
  9. package/dist/employee.factory-BlZqhiCk.d.ts +189 -0
  10. package/dist/idempotency-Cw2CWicb.d.ts +52 -0
  11. package/dist/index.d.ts +902 -0
  12. package/dist/index.js +9108 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/jurisdiction/index.d.ts +660 -0
  15. package/dist/jurisdiction/index.js +533 -0
  16. package/dist/jurisdiction/index.js.map +1 -0
  17. package/dist/payroll.d.ts +429 -0
  18. package/dist/payroll.js +5192 -0
  19. package/dist/payroll.js.map +1 -0
  20. package/dist/schemas/index.d.ts +3262 -0
  21. package/dist/schemas/index.js +780 -0
  22. package/dist/schemas/index.js.map +1 -0
  23. package/dist/services/index.d.ts +582 -0
  24. package/dist/services/index.js +2172 -0
  25. package/dist/services/index.js.map +1 -0
  26. package/dist/shift-compliance/index.d.ts +1171 -0
  27. package/dist/shift-compliance/index.js +1479 -0
  28. package/dist/shift-compliance/index.js.map +1 -0
  29. package/dist/types-BN3K_Uhr.d.ts +1842 -0
  30. package/dist/utils/index.d.ts +893 -0
  31. package/dist/utils/index.js +1515 -0
  32. package/dist/utils/index.js.map +1 -0
  33. package/package.json +72 -37
  34. package/dist/types/config.d.ts +0 -162
  35. package/dist/types/core/compensation.manager.d.ts +0 -54
  36. package/dist/types/core/employment.manager.d.ts +0 -49
  37. package/dist/types/core/payroll.manager.d.ts +0 -60
  38. package/dist/types/enums.d.ts +0 -117
  39. package/dist/types/factories/compensation.factory.d.ts +0 -196
  40. package/dist/types/factories/employee.factory.d.ts +0 -149
  41. package/dist/types/factories/payroll.factory.d.ts +0 -319
  42. package/dist/types/hrm.orchestrator.d.ts +0 -47
  43. package/dist/types/index.d.ts +0 -20
  44. package/dist/types/init.d.ts +0 -30
  45. package/dist/types/models/payroll-record.model.d.ts +0 -3
  46. package/dist/types/plugins/employee.plugin.d.ts +0 -2
  47. package/dist/types/schemas/employment.schema.d.ts +0 -959
  48. package/dist/types/services/compensation.service.d.ts +0 -94
  49. package/dist/types/services/employee.service.d.ts +0 -28
  50. package/dist/types/services/payroll.service.d.ts +0 -30
  51. package/dist/types/utils/calculation.utils.d.ts +0 -26
  52. package/dist/types/utils/date.utils.d.ts +0 -35
  53. package/dist/types/utils/logger.d.ts +0 -12
  54. package/dist/types/utils/query-builders.d.ts +0 -83
  55. package/dist/types/utils/validation.utils.d.ts +0 -33
  56. package/payroll.d.ts +0 -241
  57. package/src/config.js +0 -177
  58. package/src/core/compensation.manager.js +0 -242
  59. package/src/core/employment.manager.js +0 -224
  60. package/src/core/payroll.manager.js +0 -499
  61. package/src/enums.js +0 -141
  62. package/src/factories/compensation.factory.js +0 -198
  63. package/src/factories/employee.factory.js +0 -173
  64. package/src/factories/payroll.factory.js +0 -413
  65. package/src/hrm.orchestrator.js +0 -139
  66. package/src/index.js +0 -172
  67. package/src/init.js +0 -62
  68. package/src/models/payroll-record.model.js +0 -126
  69. package/src/plugins/employee.plugin.js +0 -164
  70. package/src/schemas/employment.schema.js +0 -126
  71. package/src/services/compensation.service.js +0 -231
  72. package/src/services/employee.service.js +0 -162
  73. package/src/services/payroll.service.js +0 -213
  74. package/src/utils/calculation.utils.js +0 -91
  75. package/src/utils/date.utils.js +0 -120
  76. package/src/utils/logger.js +0 -36
  77. package/src/utils/query-builders.js +0 -185
  78. package/src/utils/validation.utils.js +0 -122
@@ -0,0 +1,2172 @@
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
+ dataRetention: {
45
+ payrollRecordsTTL: 63072e3,
46
+ // 2 years in seconds
47
+ exportWarningDays: 30,
48
+ archiveBeforeDeletion: true
49
+ },
50
+ payroll: {
51
+ defaultCurrency: "BDT",
52
+ allowProRating: true,
53
+ attendanceIntegration: true,
54
+ autoDeductions: true,
55
+ overtimeEnabled: false,
56
+ overtimeMultiplier: 1.5
57
+ },
58
+ salary: {
59
+ minimumWage: 0,
60
+ maximumAllowances: 10,
61
+ maximumDeductions: 10,
62
+ defaultFrequency: "monthly"
63
+ },
64
+ employment: {
65
+ defaultProbationMonths: 3,
66
+ maxProbationMonths: 6,
67
+ allowReHiring: true,
68
+ trackEmploymentHistory: true
69
+ },
70
+ validation: {
71
+ requireBankDetails: false,
72
+ requireUserId: false,
73
+ // Modern: Allow guest employees by default
74
+ identityMode: "employeeId",
75
+ // Modern: Use human-readable IDs as primary
76
+ identityFallbacks: ["email", "userId"]
77
+ // Smart fallback chain
78
+ }
79
+ };
80
+ var ORG_ROLES = {
81
+ OWNER: {
82
+ key: "owner",
83
+ label: "Owner",
84
+ description: "Full organization access (set by Organization model)"
85
+ },
86
+ MANAGER: {
87
+ key: "manager",
88
+ label: "Manager",
89
+ description: "Management and administrative features"
90
+ },
91
+ TRAINER: {
92
+ key: "trainer",
93
+ label: "Trainer",
94
+ description: "Training and coaching features"
95
+ },
96
+ STAFF: {
97
+ key: "staff",
98
+ label: "Staff",
99
+ description: "General staff access to basic features"
100
+ },
101
+ INTERN: {
102
+ key: "intern",
103
+ label: "Intern",
104
+ description: "Limited access for interns"
105
+ },
106
+ CONSULTANT: {
107
+ key: "consultant",
108
+ label: "Consultant",
109
+ description: "Project-based consultant access"
110
+ }
111
+ };
112
+ Object.values(ORG_ROLES).map((role) => role.key);
113
+
114
+ // src/factories/employee.factory.ts
115
+ function normalizeEmail(email) {
116
+ if (!email || typeof email !== "string") return void 0;
117
+ const trimmed = email.trim();
118
+ return trimmed ? trimmed.toLowerCase() : void 0;
119
+ }
120
+ var EmployeeFactory = class {
121
+ /**
122
+ * Create employee data object
123
+ */
124
+ static create(params, config = HRM_CONFIG) {
125
+ const { userId, organizationId, employment, compensation, bankDetails } = params;
126
+ const hireDate = employment.hireDate || /* @__PURE__ */ new Date();
127
+ const normalizedEmail = normalizeEmail(employment.email);
128
+ return {
129
+ ...userId ? { userId } : {},
130
+ // Only include userId if present
131
+ ...normalizedEmail ? { email: normalizedEmail } : {},
132
+ // Include normalized email for guest employees
133
+ organizationId,
134
+ employeeId: employment.employeeId || `EMP-${Date.now()}-${Math.random().toString(36).substr(2, 6).toUpperCase()}`,
135
+ employmentType: employment.type || "full_time",
136
+ status: "active",
137
+ department: employment.department,
138
+ position: employment.position,
139
+ hireDate,
140
+ probationEndDate: calculateProbationEnd(
141
+ hireDate,
142
+ employment.probationMonths ?? config.employment.defaultProbationMonths
143
+ ),
144
+ compensation: this.createCompensation(compensation, config),
145
+ workSchedule: employment.workSchedule || this.defaultWorkSchedule(),
146
+ bankDetails: bankDetails || {},
147
+ payrollStats: {
148
+ totalPaid: 0,
149
+ paymentsThisYear: 0,
150
+ averageMonthly: 0
151
+ }
152
+ };
153
+ }
154
+ /**
155
+ * Create compensation object
156
+ */
157
+ static createCompensation(params, config = HRM_CONFIG) {
158
+ return {
159
+ baseAmount: params.baseAmount,
160
+ frequency: params.frequency || "monthly",
161
+ currency: params.currency || config.payroll.defaultCurrency,
162
+ allowances: (params.allowances || []).map((a) => ({
163
+ type: a.type || "other",
164
+ name: a.name || a.type || "other",
165
+ amount: a.amount || 0,
166
+ taxable: a.taxable,
167
+ recurring: a.recurring,
168
+ effectiveFrom: a.effectiveFrom,
169
+ effectiveTo: a.effectiveTo
170
+ })),
171
+ deductions: (params.deductions || []).map((d) => ({
172
+ type: d.type || "other",
173
+ name: d.name || d.type || "other",
174
+ amount: d.amount || 0,
175
+ auto: d.auto,
176
+ recurring: d.recurring,
177
+ description: d.description,
178
+ effectiveFrom: d.effectiveFrom,
179
+ effectiveTo: d.effectiveTo
180
+ })),
181
+ grossSalary: 0,
182
+ netSalary: 0,
183
+ effectiveFrom: /* @__PURE__ */ new Date(),
184
+ lastModified: /* @__PURE__ */ new Date()
185
+ };
186
+ }
187
+ /**
188
+ * Create allowance object
189
+ */
190
+ static createAllowance(params) {
191
+ return {
192
+ type: params.type,
193
+ name: params.name || params.type,
194
+ amount: params.amount,
195
+ isPercentage: params.isPercentage ?? false,
196
+ taxable: params.taxable ?? true,
197
+ recurring: params.recurring ?? true,
198
+ effectiveFrom: /* @__PURE__ */ new Date()
199
+ };
200
+ }
201
+ /**
202
+ * Create deduction object
203
+ */
204
+ static createDeduction(params) {
205
+ return {
206
+ type: params.type,
207
+ name: params.name || params.type,
208
+ amount: params.amount,
209
+ isPercentage: params.isPercentage ?? false,
210
+ auto: params.auto ?? false,
211
+ recurring: params.recurring ?? true,
212
+ description: params.description,
213
+ effectiveFrom: /* @__PURE__ */ new Date()
214
+ };
215
+ }
216
+ /**
217
+ * Default work schedule
218
+ */
219
+ static defaultWorkSchedule() {
220
+ return {
221
+ hoursPerWeek: 40,
222
+ hoursPerDay: 8,
223
+ workingDays: [1, 2, 3, 4, 5],
224
+ // Mon-Fri
225
+ shiftStart: "09:00",
226
+ shiftEnd: "17:00"
227
+ };
228
+ }
229
+ /**
230
+ * Create termination data
231
+ */
232
+ static createTermination(params) {
233
+ return {
234
+ terminatedAt: params.date || /* @__PURE__ */ new Date(),
235
+ terminationReason: params.reason,
236
+ terminationNotes: params.notes,
237
+ terminatedBy: {
238
+ userId: params.context?.userId,
239
+ name: params.context?.userName,
240
+ role: params.context?.userRole
241
+ }
242
+ };
243
+ }
244
+ };
245
+ function toObjectId(id) {
246
+ if (id instanceof Types.ObjectId) return id;
247
+ return new Types.ObjectId(id);
248
+ }
249
+ var QueryBuilder = class {
250
+ query;
251
+ constructor(initialQuery = {}) {
252
+ this.query = { ...initialQuery };
253
+ }
254
+ /**
255
+ * Add where condition
256
+ */
257
+ where(field, value) {
258
+ this.query[field] = value;
259
+ return this;
260
+ }
261
+ /**
262
+ * Add $in condition
263
+ */
264
+ whereIn(field, values) {
265
+ this.query[field] = { $in: values };
266
+ return this;
267
+ }
268
+ /**
269
+ * Add $nin condition
270
+ */
271
+ whereNotIn(field, values) {
272
+ this.query[field] = { $nin: values };
273
+ return this;
274
+ }
275
+ /**
276
+ * Add $gte condition
277
+ */
278
+ whereGte(field, value) {
279
+ const existing = this.query[field] || {};
280
+ this.query[field] = { ...existing, $gte: value };
281
+ return this;
282
+ }
283
+ /**
284
+ * Add $lte condition
285
+ */
286
+ whereLte(field, value) {
287
+ const existing = this.query[field] || {};
288
+ this.query[field] = { ...existing, $lte: value };
289
+ return this;
290
+ }
291
+ /**
292
+ * Add $gt condition
293
+ */
294
+ whereGt(field, value) {
295
+ const existing = this.query[field] || {};
296
+ this.query[field] = { ...existing, $gt: value };
297
+ return this;
298
+ }
299
+ /**
300
+ * Add $lt condition
301
+ */
302
+ whereLt(field, value) {
303
+ const existing = this.query[field] || {};
304
+ this.query[field] = { ...existing, $lt: value };
305
+ return this;
306
+ }
307
+ /**
308
+ * Add between condition
309
+ */
310
+ whereBetween(field, start, end) {
311
+ this.query[field] = { $gte: start, $lte: end };
312
+ return this;
313
+ }
314
+ /**
315
+ * Add $exists condition
316
+ */
317
+ whereExists(field) {
318
+ this.query[field] = { $exists: true };
319
+ return this;
320
+ }
321
+ /**
322
+ * Add $exists: false condition
323
+ */
324
+ whereNotExists(field) {
325
+ this.query[field] = { $exists: false };
326
+ return this;
327
+ }
328
+ /**
329
+ * Add $ne condition
330
+ */
331
+ whereNot(field, value) {
332
+ this.query[field] = { $ne: value };
333
+ return this;
334
+ }
335
+ /**
336
+ * Add regex condition
337
+ */
338
+ whereRegex(field, pattern, flags = "i") {
339
+ this.query[field] = { $regex: pattern, $options: flags };
340
+ return this;
341
+ }
342
+ /**
343
+ * Merge another query
344
+ */
345
+ merge(otherQuery) {
346
+ this.query = { ...this.query, ...otherQuery };
347
+ return this;
348
+ }
349
+ /**
350
+ * Build and return the query
351
+ */
352
+ build() {
353
+ return { ...this.query };
354
+ }
355
+ };
356
+ var EmployeeQueryBuilder = class extends QueryBuilder {
357
+ /**
358
+ * Filter by organization
359
+ */
360
+ forOrganization(organizationId) {
361
+ return this.where("organizationId", toObjectId(organizationId));
362
+ }
363
+ /**
364
+ * Filter by user
365
+ */
366
+ forUser(userId) {
367
+ return this.where("userId", toObjectId(userId));
368
+ }
369
+ /**
370
+ * Filter by employeeId (human-readable ID)
371
+ */
372
+ forEmployeeId(employeeId) {
373
+ return this.where("employeeId", employeeId);
374
+ }
375
+ /**
376
+ * Filter by email (for guest employees)
377
+ */
378
+ forEmail(email) {
379
+ return this.where("email", email.toLowerCase().trim());
380
+ }
381
+ /**
382
+ * Filter guest employees (no userId)
383
+ */
384
+ guestEmployees() {
385
+ return this.where("userId", null);
386
+ }
387
+ /**
388
+ * Filter user-linked employees (has userId)
389
+ */
390
+ userLinkedEmployees() {
391
+ return this.where("userId", { $ne: null });
392
+ }
393
+ /**
394
+ * Filter by status(es)
395
+ */
396
+ withStatus(...statuses) {
397
+ if (statuses.length === 1) {
398
+ return this.where("status", statuses[0]);
399
+ }
400
+ return this.whereIn("status", statuses);
401
+ }
402
+ /**
403
+ * Filter active employees
404
+ */
405
+ active() {
406
+ return this.withStatus("active");
407
+ }
408
+ /**
409
+ * Filter employed employees (not terminated)
410
+ */
411
+ employed() {
412
+ return this.whereIn("status", ["active", "on_leave", "suspended"]);
413
+ }
414
+ /**
415
+ * Filter terminated employees
416
+ */
417
+ terminated() {
418
+ return this.withStatus("terminated");
419
+ }
420
+ /**
421
+ * Filter by department
422
+ */
423
+ inDepartment(department) {
424
+ return this.where("department", department);
425
+ }
426
+ /**
427
+ * Filter by position
428
+ */
429
+ inPosition(position) {
430
+ return this.where("position", position);
431
+ }
432
+ /**
433
+ * Filter by employment type
434
+ */
435
+ withEmploymentType(type) {
436
+ return this.where("employmentType", type);
437
+ }
438
+ /**
439
+ * Filter by hire date (after)
440
+ */
441
+ hiredAfter(date) {
442
+ return this.whereGte("hireDate", date);
443
+ }
444
+ /**
445
+ * Filter by hire date (before)
446
+ */
447
+ hiredBefore(date) {
448
+ return this.whereLte("hireDate", date);
449
+ }
450
+ /**
451
+ * Filter by minimum salary
452
+ */
453
+ withMinSalary(amount) {
454
+ return this.whereGte("compensation.netSalary", amount);
455
+ }
456
+ /**
457
+ * Filter by maximum salary
458
+ */
459
+ withMaxSalary(amount) {
460
+ return this.whereLte("compensation.netSalary", amount);
461
+ }
462
+ /**
463
+ * Filter by salary range
464
+ */
465
+ withSalaryRange(min2, max2) {
466
+ return this.whereBetween("compensation.netSalary", min2, max2);
467
+ }
468
+ };
469
+ var PayrollQueryBuilder = class extends QueryBuilder {
470
+ /**
471
+ * Filter by organization
472
+ */
473
+ forOrganization(organizationId) {
474
+ return this.where("organizationId", toObjectId(organizationId));
475
+ }
476
+ /**
477
+ * Filter by employee
478
+ *
479
+ * Note: PayrollRecord.employeeId is always ObjectId _id
480
+ * If passing a string business ID, resolve to _id first
481
+ */
482
+ forEmployee(employeeId) {
483
+ return this.where("employeeId", toObjectId(employeeId));
484
+ }
485
+ /**
486
+ * Filter by period
487
+ */
488
+ forPeriod(month, year) {
489
+ if (month !== void 0) {
490
+ this.where("period.month", month);
491
+ }
492
+ if (year !== void 0) {
493
+ this.where("period.year", year);
494
+ }
495
+ return this;
496
+ }
497
+ /**
498
+ * Filter by status(es)
499
+ */
500
+ withStatus(...statuses) {
501
+ if (statuses.length === 1) {
502
+ return this.where("status", statuses[0]);
503
+ }
504
+ return this.whereIn("status", statuses);
505
+ }
506
+ /**
507
+ * Filter paid records
508
+ */
509
+ paid() {
510
+ return this.withStatus("paid");
511
+ }
512
+ /**
513
+ * Filter pending records
514
+ */
515
+ pending() {
516
+ return this.whereIn("status", ["pending", "processing"]);
517
+ }
518
+ /**
519
+ * Filter by date range
520
+ */
521
+ inDateRange(start, end) {
522
+ return this.whereBetween("period.payDate", start, end);
523
+ }
524
+ /**
525
+ * Filter exported records
526
+ */
527
+ exported() {
528
+ return this.where("exported", true);
529
+ }
530
+ /**
531
+ * Filter not exported records
532
+ */
533
+ notExported() {
534
+ return this.where("exported", false);
535
+ }
536
+ };
537
+ function employee() {
538
+ return new EmployeeQueryBuilder();
539
+ }
540
+ function payroll() {
541
+ return new PayrollQueryBuilder();
542
+ }
543
+ var TAX_TYPE = {
544
+ INCOME_TAX: "income_tax",
545
+ SOCIAL_SECURITY: "social_security",
546
+ HEALTH_INSURANCE: "health_insurance",
547
+ PENSION: "pension",
548
+ EMPLOYMENT_INSURANCE: "employment_insurance",
549
+ LOCAL_TAX: "local_tax",
550
+ OTHER: "other"
551
+ };
552
+ var TAX_STATUS = {
553
+ PENDING: "pending"};
554
+
555
+ // src/utils/validation.ts
556
+ function isActive(employee2) {
557
+ return employee2?.status === "active";
558
+ }
559
+ function isOnLeave(employee2) {
560
+ return employee2?.status === "on_leave";
561
+ }
562
+ function isSuspended(employee2) {
563
+ return employee2?.status === "suspended";
564
+ }
565
+ function isEmployed(employee2) {
566
+ return isActive(employee2) || isOnLeave(employee2) || isSuspended(employee2);
567
+ }
568
+ function canReceiveSalary(employee2) {
569
+ return (isActive(employee2) || isOnLeave(employee2)) && (employee2.compensation?.baseAmount ?? 0) > 0;
570
+ }
571
+
572
+ // src/utils/logger.ts
573
+ var createConsoleLogger = () => ({
574
+ info: (message, meta) => {
575
+ if (meta) {
576
+ console.log(`[Payroll] INFO: ${message}`, meta);
577
+ } else {
578
+ console.log(`[Payroll] INFO: ${message}`);
579
+ }
580
+ },
581
+ error: (message, meta) => {
582
+ if (meta) {
583
+ console.error(`[Payroll] ERROR: ${message}`, meta);
584
+ } else {
585
+ console.error(`[Payroll] ERROR: ${message}`);
586
+ }
587
+ },
588
+ warn: (message, meta) => {
589
+ if (meta) {
590
+ console.warn(`[Payroll] WARN: ${message}`, meta);
591
+ } else {
592
+ console.warn(`[Payroll] WARN: ${message}`);
593
+ }
594
+ },
595
+ debug: (message, meta) => {
596
+ if (process.env.NODE_ENV !== "production") {
597
+ if (meta) {
598
+ console.log(`[Payroll] DEBUG: ${message}`, meta);
599
+ } else {
600
+ console.log(`[Payroll] DEBUG: ${message}`);
601
+ }
602
+ }
603
+ }
604
+ });
605
+ var currentLogger = createConsoleLogger();
606
+ var logger = {
607
+ info: (message, meta) => {
608
+ currentLogger.info(message, meta);
609
+ },
610
+ error: (message, meta) => {
611
+ currentLogger.error(message, meta);
612
+ },
613
+ warn: (message, meta) => {
614
+ currentLogger.warn(message, meta);
615
+ },
616
+ debug: (message, meta) => {
617
+ currentLogger.debug(message, meta);
618
+ }
619
+ };
620
+
621
+ // src/services/employee.service.ts
622
+ var EmployeeService = class {
623
+ constructor(EmployeeModel, config) {
624
+ this.EmployeeModel = EmployeeModel;
625
+ this.config = config || HRM_CONFIG;
626
+ }
627
+ config;
628
+ /**
629
+ * Find employee by ID with organization validation
630
+ *
631
+ * ⚠️ SECURITY: Always validates employee belongs to specified organization
632
+ *
633
+ * @throws {Error} If employee not found or doesn't belong to organization
634
+ */
635
+ async findById(employeeId, organizationId, options = {}) {
636
+ const query = {
637
+ _id: toObjectId(employeeId),
638
+ organizationId: toObjectId(organizationId)
639
+ };
640
+ let mongooseQuery = this.EmployeeModel.findOne(query);
641
+ if (options.session) {
642
+ mongooseQuery = mongooseQuery.session(options.session);
643
+ }
644
+ if (options.populate) {
645
+ mongooseQuery = mongooseQuery.populate("userId", "name email phone");
646
+ }
647
+ return mongooseQuery.exec();
648
+ }
649
+ /**
650
+ * Find employee by user and organization
651
+ */
652
+ async findByUserId(userId, organizationId, options = {}) {
653
+ const query = employee().forUser(userId).forOrganization(organizationId).build();
654
+ let mongooseQuery = this.EmployeeModel.findOne(query);
655
+ if (options.session) {
656
+ mongooseQuery = mongooseQuery.session(options.session);
657
+ }
658
+ return mongooseQuery.exec();
659
+ }
660
+ /**
661
+ * Find employee by employeeId (human-readable ID)
662
+ */
663
+ async findByEmployeeId(employeeId, organizationId, options = {}) {
664
+ const query = employee().forEmployeeId(employeeId).forOrganization(organizationId).build();
665
+ let mongooseQuery = this.EmployeeModel.findOne(query);
666
+ if (options.session) {
667
+ mongooseQuery = mongooseQuery.session(options.session);
668
+ }
669
+ return mongooseQuery.exec();
670
+ }
671
+ /**
672
+ * Find employee by email (guest employees)
673
+ */
674
+ async findByEmail(email, organizationId, options = {}) {
675
+ const query = employee().forEmail(email).forOrganization(organizationId).build();
676
+ let mongooseQuery = this.EmployeeModel.findOne(query);
677
+ if (options.session) {
678
+ mongooseQuery = mongooseQuery.session(options.session);
679
+ }
680
+ return mongooseQuery.exec();
681
+ }
682
+ /**
683
+ * Find all guest employees (no userId)
684
+ */
685
+ async findGuestEmployees(organizationId, options = {}) {
686
+ const query = employee().forOrganization(organizationId).guestEmployees().build();
687
+ let mongooseQuery = this.EmployeeModel.find(query);
688
+ if (options.session) {
689
+ mongooseQuery = mongooseQuery.session(options.session);
690
+ }
691
+ return mongooseQuery.exec();
692
+ }
693
+ /**
694
+ * Find active employees in organization
695
+ */
696
+ async findActive(organizationId, options = {}) {
697
+ const query = employee().forOrganization(organizationId).active().build();
698
+ let mongooseQuery = this.EmployeeModel.find(query, options.projection);
699
+ if (options.session) {
700
+ mongooseQuery = mongooseQuery.session(options.session);
701
+ }
702
+ return mongooseQuery.exec();
703
+ }
704
+ /**
705
+ * Find employed employees (not terminated)
706
+ */
707
+ async findEmployed(organizationId, options = {}) {
708
+ const query = employee().forOrganization(organizationId).employed().build();
709
+ let mongooseQuery = this.EmployeeModel.find(query, options.projection);
710
+ if (options.session) {
711
+ mongooseQuery = mongooseQuery.session(options.session);
712
+ }
713
+ return mongooseQuery.exec();
714
+ }
715
+ /**
716
+ * Find employees by department
717
+ */
718
+ async findByDepartment(organizationId, department, options = {}) {
719
+ const query = employee().forOrganization(organizationId).inDepartment(department).active().build();
720
+ let mongooseQuery = this.EmployeeModel.find(query);
721
+ if (options.session) {
722
+ mongooseQuery = mongooseQuery.session(options.session);
723
+ }
724
+ return mongooseQuery.exec();
725
+ }
726
+ /**
727
+ * Find employees eligible for payroll
728
+ */
729
+ async findEligibleForPayroll(organizationId, options = {}) {
730
+ const query = employee().forOrganization(organizationId).employed().build();
731
+ let mongooseQuery = this.EmployeeModel.find(query);
732
+ if (options.session) {
733
+ mongooseQuery = mongooseQuery.session(options.session);
734
+ }
735
+ const employees = await mongooseQuery.exec();
736
+ return employees.filter((emp) => canReceiveSalary(emp));
737
+ }
738
+ /**
739
+ * Create new employee
740
+ */
741
+ async create(params, options = {}) {
742
+ const employeeData = EmployeeFactory.create(params, this.config);
743
+ let employee2;
744
+ if (!params.userId) {
745
+ const dataToInsert = {};
746
+ for (const [key, value] of Object.entries(employeeData)) {
747
+ if (key === "userId" || key === "email") continue;
748
+ dataToInsert[key] = value;
749
+ }
750
+ if (employeeData.email && employeeData.email !== "") {
751
+ dataToInsert.email = employeeData.email;
752
+ }
753
+ const now = /* @__PURE__ */ new Date();
754
+ dataToInsert.createdAt = now;
755
+ dataToInsert.updatedAt = now;
756
+ const insertOptions = options.session ? { session: options.session } : {};
757
+ const result = await this.EmployeeModel.collection.insertOne(
758
+ dataToInsert,
759
+ insertOptions
760
+ );
761
+ employee2 = await this.EmployeeModel.findById(result.insertedId).session(options.session || null).exec();
762
+ } else {
763
+ const [created] = await this.EmployeeModel.create([employeeData], {
764
+ session: options.session
765
+ });
766
+ employee2 = created;
767
+ }
768
+ logger.info("Employee created", {
769
+ employeeId: employee2.employeeId,
770
+ organizationId: employee2.organizationId.toString()
771
+ });
772
+ return employee2;
773
+ }
774
+ /**
775
+ * Update employee status with organization validation
776
+ *
777
+ * ⚠️ SECURITY: Validates employee belongs to organization before update
778
+ */
779
+ async updateStatus(employeeId, organizationId, status, context = {}, options = {}) {
780
+ const employee2 = await this.findById(employeeId, organizationId, options);
781
+ if (!employee2) {
782
+ throw new Error(`Employee not found in organization ${organizationId}`);
783
+ }
784
+ employee2.status = status;
785
+ await employee2.save({ session: options.session });
786
+ logger.info("Employee status updated", {
787
+ employeeId: employee2.employeeId,
788
+ organizationId: organizationId.toString(),
789
+ newStatus: status
790
+ });
791
+ return employee2;
792
+ }
793
+ /**
794
+ * Update employee compensation with organization validation
795
+ *
796
+ * ⚠️ SECURITY: Validates employee belongs to organization before update
797
+ *
798
+ * NOTE: This merges the compensation fields rather than replacing the entire object.
799
+ * To update allowances/deductions, use addAllowance/removeAllowance methods.
800
+ */
801
+ async updateCompensation(employeeId, organizationId, compensation, options = {}) {
802
+ const currentEmployee = await this.findById(employeeId, organizationId, options);
803
+ if (!currentEmployee) {
804
+ throw new Error(`Employee not found in organization ${organizationId}`);
805
+ }
806
+ const updateFields = {
807
+ "compensation.lastModified": /* @__PURE__ */ new Date()
808
+ };
809
+ if (compensation.baseAmount !== void 0) {
810
+ updateFields["compensation.baseAmount"] = compensation.baseAmount;
811
+ }
812
+ if (compensation.currency !== void 0) {
813
+ updateFields["compensation.currency"] = compensation.currency;
814
+ }
815
+ if (compensation.frequency !== void 0) {
816
+ updateFields["compensation.frequency"] = compensation.frequency;
817
+ }
818
+ if (compensation.effectiveFrom !== void 0) {
819
+ updateFields["compensation.effectiveFrom"] = compensation.effectiveFrom;
820
+ }
821
+ const query = {
822
+ _id: toObjectId(employeeId),
823
+ organizationId: toObjectId(organizationId)
824
+ };
825
+ const employee2 = await this.EmployeeModel.findOneAndUpdate(
826
+ query,
827
+ { $set: updateFields },
828
+ { new: true, runValidators: true, session: options.session }
829
+ );
830
+ if (!employee2) {
831
+ throw new Error(`Employee not found in organization ${organizationId}`);
832
+ }
833
+ logger.info("Employee compensation updated", {
834
+ employeeId: employee2.employeeId,
835
+ organizationId: organizationId.toString()
836
+ });
837
+ return employee2;
838
+ }
839
+ /**
840
+ * Get employee statistics for organization
841
+ */
842
+ async getEmployeeStats(organizationId, options = {}) {
843
+ const query = employee().forOrganization(organizationId).build();
844
+ let mongooseQuery = this.EmployeeModel.find(query);
845
+ if (options.session) {
846
+ mongooseQuery = mongooseQuery.session(options.session);
847
+ }
848
+ const employees = await mongooseQuery.exec();
849
+ return {
850
+ total: employees.length,
851
+ active: employees.filter(isActive).length,
852
+ employed: employees.filter(isEmployed).length,
853
+ canReceiveSalary: employees.filter(canReceiveSalary).length,
854
+ byStatus: this.groupByStatus(employees),
855
+ byDepartment: this.groupByDepartment(employees)
856
+ };
857
+ }
858
+ /**
859
+ * Group employees by status
860
+ */
861
+ groupByStatus(employees) {
862
+ return employees.reduce(
863
+ (acc, emp) => {
864
+ acc[emp.status] = (acc[emp.status] || 0) + 1;
865
+ return acc;
866
+ },
867
+ {}
868
+ );
869
+ }
870
+ /**
871
+ * Group employees by department
872
+ */
873
+ groupByDepartment(employees) {
874
+ return employees.reduce(
875
+ (acc, emp) => {
876
+ const dept = emp.department || "unassigned";
877
+ acc[dept] = (acc[dept] || 0) + 1;
878
+ return acc;
879
+ },
880
+ {}
881
+ );
882
+ }
883
+ /**
884
+ * Check if employee is active
885
+ */
886
+ isActive(employee2) {
887
+ return isActive(employee2);
888
+ }
889
+ /**
890
+ * Check if employee is employed
891
+ */
892
+ isEmployed(employee2) {
893
+ return isEmployed(employee2);
894
+ }
895
+ /**
896
+ * Check if employee can receive salary
897
+ */
898
+ canReceiveSalary(employee2) {
899
+ return canReceiveSalary(employee2);
900
+ }
901
+ };
902
+ function createEmployeeService(EmployeeModel, config) {
903
+ return new EmployeeService(EmployeeModel, config);
904
+ }
905
+
906
+ // src/utils/calculation.ts
907
+ function sumBy(items, getter) {
908
+ return items.reduce((total, item) => total + getter(item), 0);
909
+ }
910
+ function sumAllowances(allowances) {
911
+ return sumBy(allowances, (a) => a.amount);
912
+ }
913
+ function sumDeductions(deductions) {
914
+ return sumBy(deductions, (d) => d.amount);
915
+ }
916
+ function applyPercentage(amount, percentage) {
917
+ return Math.round(amount * (percentage / 100));
918
+ }
919
+ function calculateGross(baseAmount, allowances) {
920
+ return baseAmount + sumAllowances(allowances);
921
+ }
922
+ function calculateNet(gross, deductions) {
923
+ return Math.max(0, gross - sumDeductions(deductions));
924
+ }
925
+
926
+ // src/factories/payroll.factory.ts
927
+ var PayrollFactory = class {
928
+ /**
929
+ * Create payroll data object
930
+ */
931
+ static create(params) {
932
+ const {
933
+ employeeId,
934
+ organizationId,
935
+ baseAmount,
936
+ allowances = [],
937
+ deductions = [],
938
+ period = {},
939
+ metadata = {}
940
+ } = params;
941
+ const calculatedAllowances = this.calculateAllowances(baseAmount, allowances);
942
+ const calculatedDeductions = this.calculateDeductions(baseAmount, deductions);
943
+ const gross = calculateGross(baseAmount, calculatedAllowances);
944
+ const net = calculateNet(gross, calculatedDeductions);
945
+ return {
946
+ employeeId,
947
+ organizationId,
948
+ period: this.createPeriod(period),
949
+ breakdown: {
950
+ baseAmount,
951
+ allowances: calculatedAllowances,
952
+ deductions: calculatedDeductions,
953
+ grossSalary: gross,
954
+ netSalary: net
955
+ },
956
+ status: "pending",
957
+ processedAt: null,
958
+ paidAt: null,
959
+ metadata: {
960
+ currency: metadata.currency || HRM_CONFIG.payroll.defaultCurrency,
961
+ paymentMethod: metadata.paymentMethod,
962
+ notes: metadata.notes
963
+ }
964
+ };
965
+ }
966
+ /**
967
+ * Create pay period
968
+ */
969
+ static createPeriod(params) {
970
+ const now = /* @__PURE__ */ new Date();
971
+ const month = params.month || now.getMonth() + 1;
972
+ const year = params.year || now.getFullYear();
973
+ const period = getPayPeriod(month, year);
974
+ return {
975
+ ...period,
976
+ payDate: params.payDate || /* @__PURE__ */ new Date()
977
+ };
978
+ }
979
+ /**
980
+ * Calculate allowances from base amount
981
+ */
982
+ static calculateAllowances(baseAmount, allowances) {
983
+ return allowances.map((allowance) => {
984
+ const amount = allowance.isPercentage && allowance.value !== void 0 ? Math.round(baseAmount * allowance.value / 100) : allowance.amount;
985
+ return {
986
+ type: allowance.type,
987
+ amount,
988
+ taxable: allowance.taxable ?? true
989
+ };
990
+ });
991
+ }
992
+ /**
993
+ * Calculate deductions from base amount
994
+ */
995
+ static calculateDeductions(baseAmount, deductions) {
996
+ return deductions.map((deduction) => {
997
+ const amount = deduction.isPercentage && deduction.value !== void 0 ? Math.round(baseAmount * deduction.value / 100) : deduction.amount;
998
+ return {
999
+ type: deduction.type,
1000
+ amount,
1001
+ description: deduction.description
1002
+ };
1003
+ });
1004
+ }
1005
+ /**
1006
+ * Create bonus object
1007
+ */
1008
+ static createBonus(params) {
1009
+ return {
1010
+ type: params.type,
1011
+ amount: params.amount,
1012
+ reason: params.reason,
1013
+ approvedBy: params.approvedBy,
1014
+ approvedAt: /* @__PURE__ */ new Date()
1015
+ };
1016
+ }
1017
+ /**
1018
+ * Mark payroll as paid (immutable)
1019
+ * Sets both top-level transactionId and metadata for compatibility
1020
+ */
1021
+ static markAsPaid(payroll2, params = {}) {
1022
+ return {
1023
+ ...payroll2,
1024
+ status: "paid",
1025
+ paidAt: params.paidAt || /* @__PURE__ */ new Date(),
1026
+ processedAt: payroll2.processedAt || params.paidAt || /* @__PURE__ */ new Date(),
1027
+ transactionId: params.transactionId || payroll2.transactionId,
1028
+ metadata: {
1029
+ ...payroll2.metadata,
1030
+ transactionId: params.transactionId,
1031
+ paymentMethod: params.paymentMethod || payroll2.metadata?.paymentMethod
1032
+ }
1033
+ };
1034
+ }
1035
+ /**
1036
+ * Mark payroll as processed (immutable)
1037
+ */
1038
+ static markAsProcessed(payroll2, params = {}) {
1039
+ return {
1040
+ ...payroll2,
1041
+ status: "processing",
1042
+ processedAt: params.processedAt || /* @__PURE__ */ new Date()
1043
+ };
1044
+ }
1045
+ };
1046
+ var BatchPayrollFactory = class {
1047
+ /**
1048
+ * Create payroll records for multiple employees
1049
+ */
1050
+ static createBatch(employees, params) {
1051
+ return employees.map(
1052
+ (employee2) => PayrollFactory.create({
1053
+ employeeId: employee2._id,
1054
+ organizationId: params.organizationId || employee2.organizationId,
1055
+ baseAmount: employee2.compensation.baseAmount,
1056
+ allowances: employee2.compensation.allowances || [],
1057
+ deductions: employee2.compensation.deductions || [],
1058
+ period: { month: params.month, year: params.year },
1059
+ metadata: { currency: employee2.compensation.currency }
1060
+ })
1061
+ );
1062
+ }
1063
+ /**
1064
+ * Calculate total payroll amounts
1065
+ */
1066
+ static calculateTotalPayroll(payrolls) {
1067
+ return payrolls.reduce(
1068
+ (totals, payroll2) => ({
1069
+ count: totals.count + 1,
1070
+ totalGross: totals.totalGross + payroll2.breakdown.grossSalary,
1071
+ totalNet: totals.totalNet + payroll2.breakdown.netSalary,
1072
+ totalAllowances: totals.totalAllowances + sumAllowances(payroll2.breakdown.allowances),
1073
+ totalDeductions: totals.totalDeductions + sumDeductions(payroll2.breakdown.deductions)
1074
+ }),
1075
+ { count: 0, totalGross: 0, totalNet: 0, totalAllowances: 0, totalDeductions: 0 }
1076
+ );
1077
+ }
1078
+ };
1079
+
1080
+ // src/services/payroll.service.ts
1081
+ var PayrollService = class {
1082
+ constructor(PayrollModel, employeeService) {
1083
+ this.PayrollModel = PayrollModel;
1084
+ this.employeeService = employeeService;
1085
+ }
1086
+ /**
1087
+ * Find payroll by ID
1088
+ */
1089
+ async findById(payrollId, options = {}) {
1090
+ let query = this.PayrollModel.findById(toObjectId(payrollId));
1091
+ if (options.session) {
1092
+ query = query.session(options.session);
1093
+ }
1094
+ return query.exec();
1095
+ }
1096
+ /**
1097
+ * Find payrolls by employee
1098
+ */
1099
+ async findByEmployee(employeeId, organizationId, options = {}) {
1100
+ const query = payroll().forEmployee(employeeId).forOrganization(organizationId).build();
1101
+ let mongooseQuery = this.PayrollModel.find(query).sort({ "period.year": -1, "period.month": -1 }).limit(options.limit || 12);
1102
+ if (options.session) {
1103
+ mongooseQuery = mongooseQuery.session(options.session);
1104
+ }
1105
+ return mongooseQuery.exec();
1106
+ }
1107
+ /**
1108
+ * Find payrolls for a period
1109
+ */
1110
+ async findForPeriod(organizationId, month, year, options = {}) {
1111
+ const query = payroll().forOrganization(organizationId).forPeriod(month, year).build();
1112
+ let mongooseQuery = this.PayrollModel.find(query);
1113
+ if (options.session) {
1114
+ mongooseQuery = mongooseQuery.session(options.session);
1115
+ }
1116
+ return mongooseQuery.exec();
1117
+ }
1118
+ /**
1119
+ * Find pending payrolls
1120
+ */
1121
+ async findPending(organizationId, month, year, options = {}) {
1122
+ const query = payroll().forOrganization(organizationId).forPeriod(month, year).pending().build();
1123
+ let mongooseQuery = this.PayrollModel.find(query);
1124
+ if (options.session) {
1125
+ mongooseQuery = mongooseQuery.session(options.session);
1126
+ }
1127
+ return mongooseQuery.exec();
1128
+ }
1129
+ /**
1130
+ * Find payroll by employee and period
1131
+ */
1132
+ async findByEmployeeAndPeriod(employeeId, organizationId, month, year, options = {}) {
1133
+ const query = payroll().forEmployee(employeeId).forOrganization(organizationId).forPeriod(month, year).build();
1134
+ let mongooseQuery = this.PayrollModel.findOne(query);
1135
+ if (options.session) {
1136
+ mongooseQuery = mongooseQuery.session(options.session);
1137
+ }
1138
+ return mongooseQuery.exec();
1139
+ }
1140
+ /**
1141
+ * Create payroll record
1142
+ */
1143
+ async create(data, options = {}) {
1144
+ const [payroll2] = await this.PayrollModel.create([data], {
1145
+ session: options.session
1146
+ });
1147
+ logger.info("Payroll record created", {
1148
+ payrollId: payroll2._id.toString(),
1149
+ employeeId: payroll2.employeeId.toString()
1150
+ });
1151
+ return payroll2;
1152
+ }
1153
+ /**
1154
+ * Generate payroll for employee with organization validation
1155
+ *
1156
+ * ⚠️ SECURITY: Validates employee belongs to organization
1157
+ */
1158
+ async generateForEmployee(employeeId, organizationId, month, year, options = {}) {
1159
+ const employee2 = await this.employeeService.findById(employeeId, organizationId, options);
1160
+ if (!employee2) {
1161
+ throw new Error(`Employee not found in organization ${organizationId}`);
1162
+ }
1163
+ if (!canReceiveSalary(employee2)) {
1164
+ throw new Error("Employee not eligible for payroll");
1165
+ }
1166
+ const existing = await this.findByEmployeeAndPeriod(
1167
+ employeeId,
1168
+ organizationId,
1169
+ month,
1170
+ year,
1171
+ options
1172
+ );
1173
+ if (existing) {
1174
+ throw new Error("Payroll already exists for this period");
1175
+ }
1176
+ const payrollData = PayrollFactory.create({
1177
+ employeeId,
1178
+ organizationId,
1179
+ baseAmount: employee2.compensation.baseAmount,
1180
+ allowances: employee2.compensation.allowances || [],
1181
+ deductions: employee2.compensation.deductions || [],
1182
+ period: { month, year },
1183
+ metadata: { currency: employee2.compensation.currency }
1184
+ });
1185
+ return this.create(payrollData, options);
1186
+ }
1187
+ /**
1188
+ * Generate batch payroll
1189
+ */
1190
+ async generateBatch(organizationId, month, year, options = {}) {
1191
+ const employees = await this.employeeService.findEligibleForPayroll(
1192
+ organizationId,
1193
+ options
1194
+ );
1195
+ if (employees.length === 0) {
1196
+ return {
1197
+ success: true,
1198
+ generated: 0,
1199
+ skipped: 0,
1200
+ payrolls: [],
1201
+ message: "No eligible employees"
1202
+ };
1203
+ }
1204
+ const existingPayrolls = await this.findForPeriod(
1205
+ organizationId,
1206
+ month,
1207
+ year,
1208
+ options
1209
+ );
1210
+ const existingEmployeeIds = new Set(
1211
+ existingPayrolls.map((p) => p.employeeId.toString())
1212
+ );
1213
+ const eligibleEmployees = employees.filter(
1214
+ (emp) => !existingEmployeeIds.has(emp._id.toString())
1215
+ );
1216
+ if (eligibleEmployees.length === 0) {
1217
+ return {
1218
+ success: true,
1219
+ generated: 0,
1220
+ skipped: employees.length,
1221
+ payrolls: [],
1222
+ message: "Payrolls already exist for all employees"
1223
+ };
1224
+ }
1225
+ const payrollsData = BatchPayrollFactory.createBatch(eligibleEmployees, {
1226
+ month,
1227
+ year,
1228
+ organizationId
1229
+ });
1230
+ const created = await this.PayrollModel.insertMany(payrollsData, {
1231
+ session: options.session
1232
+ });
1233
+ logger.info("Batch payroll generated", {
1234
+ organizationId: organizationId.toString(),
1235
+ month,
1236
+ year,
1237
+ count: created.length
1238
+ });
1239
+ return {
1240
+ success: true,
1241
+ generated: created.length,
1242
+ skipped: existingEmployeeIds.size,
1243
+ payrolls: created,
1244
+ message: `Generated ${created.length} payrolls`
1245
+ };
1246
+ }
1247
+ /**
1248
+ * Mark payroll as paid with organization validation
1249
+ *
1250
+ * ⚠️ SECURITY: Validates payroll belongs to organization
1251
+ */
1252
+ async markAsPaid(payrollId, organizationId, paymentDetails = {}, options = {}) {
1253
+ const query = {
1254
+ _id: toObjectId(payrollId),
1255
+ organizationId: toObjectId(organizationId)
1256
+ };
1257
+ let payrollFindQuery = this.PayrollModel.findOne(query);
1258
+ if (options.session) {
1259
+ payrollFindQuery = payrollFindQuery.session(options.session);
1260
+ }
1261
+ const payroll2 = await payrollFindQuery;
1262
+ if (!payroll2) {
1263
+ throw new Error(`Payroll not found in organization ${organizationId}`);
1264
+ }
1265
+ if (payroll2.status === "paid") {
1266
+ throw new Error("Payroll already paid");
1267
+ }
1268
+ const payrollObj = payroll2.toObject();
1269
+ const updatedData = PayrollFactory.markAsPaid(payrollObj, paymentDetails);
1270
+ const updated = await this.PayrollModel.findByIdAndUpdate(
1271
+ payrollId,
1272
+ updatedData,
1273
+ { new: true, runValidators: true, session: options.session }
1274
+ );
1275
+ if (!updated) {
1276
+ throw new Error("Failed to update payroll");
1277
+ }
1278
+ logger.info("Payroll marked as paid", {
1279
+ payrollId: payrollId.toString()
1280
+ });
1281
+ return updated;
1282
+ }
1283
+ /**
1284
+ * Mark payroll as processed with organization validation
1285
+ *
1286
+ * ⚠️ SECURITY: Validates payroll belongs to organization
1287
+ */
1288
+ async markAsProcessed(payrollId, organizationId, options = {}) {
1289
+ const query = {
1290
+ _id: toObjectId(payrollId),
1291
+ organizationId: toObjectId(organizationId)
1292
+ };
1293
+ let payrollFindQuery = this.PayrollModel.findOne(query);
1294
+ if (options.session) {
1295
+ payrollFindQuery = payrollFindQuery.session(options.session);
1296
+ }
1297
+ const payroll2 = await payrollFindQuery;
1298
+ if (!payroll2) {
1299
+ throw new Error(`Payroll not found in organization ${organizationId}`);
1300
+ }
1301
+ const payrollObj = payroll2.toObject();
1302
+ const updatedData = PayrollFactory.markAsProcessed(payrollObj);
1303
+ const updated = await this.PayrollModel.findByIdAndUpdate(
1304
+ payrollId,
1305
+ updatedData,
1306
+ { new: true, runValidators: true, session: options.session }
1307
+ );
1308
+ if (!updated) {
1309
+ throw new Error("Failed to update payroll");
1310
+ }
1311
+ return updated;
1312
+ }
1313
+ /**
1314
+ * Calculate period summary
1315
+ */
1316
+ async calculatePeriodSummary(organizationId, month, year, options = {}) {
1317
+ const payrolls = await this.findForPeriod(organizationId, month, year, options);
1318
+ const summary = BatchPayrollFactory.calculateTotalPayroll(payrolls);
1319
+ return {
1320
+ period: { month, year },
1321
+ ...summary,
1322
+ byStatus: this.groupByStatus(payrolls)
1323
+ };
1324
+ }
1325
+ /**
1326
+ * Get employee payroll history
1327
+ */
1328
+ async getEmployeePayrollHistory(employeeId, organizationId, limit = 12, options = {}) {
1329
+ return this.findByEmployee(employeeId, organizationId, { ...options, limit });
1330
+ }
1331
+ /**
1332
+ * Get overview stats
1333
+ */
1334
+ async getOverviewStats(organizationId, options = {}) {
1335
+ const { month, year } = getCurrentPeriod();
1336
+ const result = await this.calculatePeriodSummary(organizationId, month, year, options);
1337
+ return {
1338
+ currentPeriod: result.period,
1339
+ count: result.count,
1340
+ totalGross: result.totalGross,
1341
+ totalNet: result.totalNet,
1342
+ totalAllowances: result.totalAllowances,
1343
+ totalDeductions: result.totalDeductions,
1344
+ byStatus: result.byStatus
1345
+ };
1346
+ }
1347
+ /**
1348
+ * Group payrolls by status
1349
+ */
1350
+ groupByStatus(payrolls) {
1351
+ return payrolls.reduce(
1352
+ (acc, payroll2) => {
1353
+ acc[payroll2.status] = (acc[payroll2.status] || 0) + 1;
1354
+ return acc;
1355
+ },
1356
+ {}
1357
+ );
1358
+ }
1359
+ };
1360
+ function createPayrollService(PayrollModel, employeeService) {
1361
+ return new PayrollService(PayrollModel, employeeService);
1362
+ }
1363
+
1364
+ // src/factories/compensation.factory.ts
1365
+ var CompensationFactory = class {
1366
+ /**
1367
+ * Create compensation object
1368
+ */
1369
+ static create(params) {
1370
+ const {
1371
+ baseAmount,
1372
+ frequency = "monthly",
1373
+ currency = HRM_CONFIG.payroll.defaultCurrency,
1374
+ allowances = [],
1375
+ deductions = [],
1376
+ effectiveFrom = /* @__PURE__ */ new Date()
1377
+ } = params;
1378
+ return {
1379
+ baseAmount,
1380
+ frequency,
1381
+ currency,
1382
+ allowances: allowances.map((a) => this.createAllowance(a, baseAmount)),
1383
+ deductions: deductions.map((d) => this.createDeduction(d, baseAmount)),
1384
+ effectiveFrom,
1385
+ lastModified: /* @__PURE__ */ new Date()
1386
+ };
1387
+ }
1388
+ /**
1389
+ * Create allowance
1390
+ */
1391
+ static createAllowance(params, baseAmount) {
1392
+ const amount = params.isPercentage && baseAmount ? applyPercentage(baseAmount, params.value) : params.value;
1393
+ return {
1394
+ type: params.type,
1395
+ name: params.name || params.type,
1396
+ amount,
1397
+ isPercentage: params.isPercentage ?? false,
1398
+ value: params.isPercentage ? params.value : void 0,
1399
+ taxable: params.taxable ?? true,
1400
+ recurring: true,
1401
+ effectiveFrom: /* @__PURE__ */ new Date()
1402
+ };
1403
+ }
1404
+ /**
1405
+ * Create deduction
1406
+ */
1407
+ static createDeduction(params, baseAmount) {
1408
+ const amount = params.isPercentage && baseAmount ? applyPercentage(baseAmount, params.value) : params.value;
1409
+ return {
1410
+ type: params.type,
1411
+ name: params.name || params.type,
1412
+ amount,
1413
+ isPercentage: params.isPercentage ?? false,
1414
+ value: params.isPercentage ? params.value : void 0,
1415
+ auto: params.auto ?? false,
1416
+ recurring: true,
1417
+ effectiveFrom: /* @__PURE__ */ new Date()
1418
+ };
1419
+ }
1420
+ /**
1421
+ * Update base amount (immutable)
1422
+ */
1423
+ static updateBaseAmount(compensation, newAmount, effectiveFrom = /* @__PURE__ */ new Date()) {
1424
+ return {
1425
+ ...compensation,
1426
+ baseAmount: newAmount,
1427
+ lastModified: effectiveFrom
1428
+ };
1429
+ }
1430
+ /**
1431
+ * Add allowance (immutable)
1432
+ */
1433
+ static addAllowance(compensation, allowance) {
1434
+ return {
1435
+ ...compensation,
1436
+ allowances: [
1437
+ ...compensation.allowances,
1438
+ this.createAllowance(allowance, compensation.baseAmount)
1439
+ ],
1440
+ lastModified: /* @__PURE__ */ new Date()
1441
+ };
1442
+ }
1443
+ /**
1444
+ * Remove allowance (immutable)
1445
+ */
1446
+ static removeAllowance(compensation, allowanceType) {
1447
+ return {
1448
+ ...compensation,
1449
+ allowances: compensation.allowances.filter((a) => a.type !== allowanceType),
1450
+ lastModified: /* @__PURE__ */ new Date()
1451
+ };
1452
+ }
1453
+ /**
1454
+ * Add deduction (immutable)
1455
+ */
1456
+ static addDeduction(compensation, deduction) {
1457
+ return {
1458
+ ...compensation,
1459
+ deductions: [
1460
+ ...compensation.deductions,
1461
+ this.createDeduction(deduction, compensation.baseAmount)
1462
+ ],
1463
+ lastModified: /* @__PURE__ */ new Date()
1464
+ };
1465
+ }
1466
+ /**
1467
+ * Remove deduction (immutable)
1468
+ */
1469
+ static removeDeduction(compensation, deductionType) {
1470
+ return {
1471
+ ...compensation,
1472
+ deductions: compensation.deductions.filter((d) => d.type !== deductionType),
1473
+ lastModified: /* @__PURE__ */ new Date()
1474
+ };
1475
+ }
1476
+ /**
1477
+ * Calculate compensation breakdown
1478
+ */
1479
+ static calculateBreakdown(compensation) {
1480
+ const { baseAmount, allowances, deductions } = compensation;
1481
+ const calculatedAllowances = allowances.map((a) => ({
1482
+ ...a,
1483
+ calculatedAmount: a.isPercentage && a.value !== void 0 ? applyPercentage(baseAmount, a.value) : a.amount
1484
+ }));
1485
+ const calculatedDeductions = deductions.map((d) => ({
1486
+ ...d,
1487
+ calculatedAmount: d.isPercentage && d.value !== void 0 ? applyPercentage(baseAmount, d.value) : d.amount
1488
+ }));
1489
+ const grossAmount = calculateGross(
1490
+ baseAmount,
1491
+ calculatedAllowances.map((a) => ({ amount: a.calculatedAmount }))
1492
+ );
1493
+ const netAmount = calculateNet(
1494
+ grossAmount,
1495
+ calculatedDeductions.map((d) => ({ amount: d.calculatedAmount }))
1496
+ );
1497
+ return {
1498
+ baseAmount,
1499
+ allowances: calculatedAllowances,
1500
+ deductions: calculatedDeductions,
1501
+ grossAmount,
1502
+ netAmount: Math.max(0, netAmount)
1503
+ };
1504
+ }
1505
+ /**
1506
+ * Apply salary increment (immutable)
1507
+ */
1508
+ static applyIncrement(compensation, params) {
1509
+ const newBaseAmount = params.amount ? compensation.baseAmount + params.amount : compensation.baseAmount * (1 + (params.percentage || 0) / 100);
1510
+ return this.updateBaseAmount(
1511
+ compensation,
1512
+ Math.round(newBaseAmount),
1513
+ params.effectiveFrom
1514
+ );
1515
+ }
1516
+ };
1517
+ var CompensationBuilder = class {
1518
+ data = {
1519
+ baseAmount: 0,
1520
+ frequency: "monthly",
1521
+ currency: HRM_CONFIG.payroll.defaultCurrency,
1522
+ allowances: [],
1523
+ deductions: []
1524
+ };
1525
+ /**
1526
+ * Set base amount
1527
+ */
1528
+ withBase(amount, frequency = "monthly", currency = HRM_CONFIG.payroll.defaultCurrency) {
1529
+ this.data.baseAmount = amount;
1530
+ this.data.frequency = frequency;
1531
+ this.data.currency = currency;
1532
+ return this;
1533
+ }
1534
+ /**
1535
+ * Add allowance
1536
+ */
1537
+ addAllowance(type, value, isPercentage = false, name) {
1538
+ this.data.allowances = [
1539
+ ...this.data.allowances || [],
1540
+ { type, value, isPercentage, name }
1541
+ ];
1542
+ return this;
1543
+ }
1544
+ /**
1545
+ * Add deduction
1546
+ */
1547
+ addDeduction(type, value, isPercentage = false, name) {
1548
+ this.data.deductions = [
1549
+ ...this.data.deductions || [],
1550
+ { type, value, isPercentage, name }
1551
+ ];
1552
+ return this;
1553
+ }
1554
+ /**
1555
+ * Set effective date
1556
+ */
1557
+ effectiveFrom(date) {
1558
+ this.data.effectiveFrom = date;
1559
+ return this;
1560
+ }
1561
+ /**
1562
+ * Build compensation
1563
+ */
1564
+ build() {
1565
+ if (!this.data.baseAmount) {
1566
+ throw new Error("baseAmount is required");
1567
+ }
1568
+ return CompensationFactory.create(this.data);
1569
+ }
1570
+ };
1571
+ var CompensationPresets = {
1572
+ /**
1573
+ * Basic compensation (base only)
1574
+ */
1575
+ basic(baseAmount) {
1576
+ return new CompensationBuilder().withBase(baseAmount).build();
1577
+ },
1578
+ /**
1579
+ * With house rent allowance
1580
+ */
1581
+ withHouseRent(baseAmount, rentPercentage = 50) {
1582
+ return new CompensationBuilder().withBase(baseAmount).addAllowance("housing", rentPercentage, true, "House Rent").build();
1583
+ },
1584
+ /**
1585
+ * With medical allowance
1586
+ */
1587
+ withMedical(baseAmount, medicalPercentage = 10) {
1588
+ return new CompensationBuilder().withBase(baseAmount).addAllowance("medical", medicalPercentage, true, "Medical Allowance").build();
1589
+ },
1590
+ /**
1591
+ * Standard package (house rent + medical + transport)
1592
+ */
1593
+ standard(baseAmount) {
1594
+ return new CompensationBuilder().withBase(baseAmount).addAllowance("housing", 50, true, "House Rent").addAllowance("medical", 10, true, "Medical Allowance").addAllowance("transport", 5, true, "Transport Allowance").build();
1595
+ },
1596
+ /**
1597
+ * With provident fund
1598
+ */
1599
+ withProvidentFund(baseAmount, pfPercentage = 10) {
1600
+ 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();
1601
+ },
1602
+ /**
1603
+ * Executive package
1604
+ */
1605
+ executive(baseAmount) {
1606
+ 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();
1607
+ }
1608
+ };
1609
+
1610
+ // src/services/compensation.service.ts
1611
+ var CompensationService = class {
1612
+ constructor(EmployeeModel) {
1613
+ this.EmployeeModel = EmployeeModel;
1614
+ }
1615
+ /**
1616
+ * Get employee compensation with organization validation
1617
+ *
1618
+ * ⚠️ SECURITY: Validates employee belongs to organization
1619
+ */
1620
+ async getEmployeeCompensation(employeeId, organizationId, options = {}) {
1621
+ const employee2 = await this.findEmployee(employeeId, organizationId, options);
1622
+ return employee2.compensation;
1623
+ }
1624
+ /**
1625
+ * Calculate compensation breakdown with organization validation
1626
+ *
1627
+ * ⚠️ SECURITY: Validates employee belongs to organization
1628
+ */
1629
+ async calculateBreakdown(employeeId, organizationId, options = {}) {
1630
+ const compensation = await this.getEmployeeCompensation(employeeId, organizationId, options);
1631
+ return CompensationFactory.calculateBreakdown(compensation);
1632
+ }
1633
+ /**
1634
+ * Update base amount with organization validation
1635
+ *
1636
+ * ⚠️ SECURITY: Validates employee belongs to organization before update
1637
+ */
1638
+ async updateBaseAmount(employeeId, organizationId, newAmount, effectiveFrom = /* @__PURE__ */ new Date(), options = {}) {
1639
+ const employee2 = await this.findEmployee(employeeId, organizationId, options);
1640
+ const updatedCompensation = CompensationFactory.updateBaseAmount(
1641
+ employee2.compensation,
1642
+ newAmount,
1643
+ effectiveFrom
1644
+ );
1645
+ employee2.compensation = updatedCompensation;
1646
+ await employee2.save({ session: options.session });
1647
+ logger.info("Compensation base amount updated", {
1648
+ employeeId: employee2.employeeId,
1649
+ organizationId: organizationId.toString(),
1650
+ newAmount
1651
+ });
1652
+ return this.calculateBreakdown(employeeId, organizationId, options);
1653
+ }
1654
+ /**
1655
+ * Apply salary increment with organization validation
1656
+ *
1657
+ * ⚠️ SECURITY: Validates employee belongs to organization before update
1658
+ */
1659
+ async applyIncrement(employeeId, organizationId, params, options = {}) {
1660
+ const employee2 = await this.findEmployee(employeeId, organizationId, options);
1661
+ const previousAmount = employee2.compensation.baseAmount;
1662
+ const updatedCompensation = CompensationFactory.applyIncrement(
1663
+ employee2.compensation,
1664
+ params
1665
+ );
1666
+ employee2.compensation = updatedCompensation;
1667
+ await employee2.save({ session: options.session });
1668
+ logger.info("Salary increment applied", {
1669
+ employeeId: employee2.employeeId,
1670
+ organizationId: organizationId.toString(),
1671
+ previousAmount,
1672
+ newAmount: updatedCompensation.baseAmount,
1673
+ percentage: params.percentage
1674
+ });
1675
+ return this.calculateBreakdown(employeeId, organizationId, options);
1676
+ }
1677
+ /**
1678
+ * Add allowance with organization validation
1679
+ *
1680
+ * ⚠️ SECURITY: Validates employee belongs to organization before update
1681
+ */
1682
+ async addAllowance(employeeId, organizationId, allowance, options = {}) {
1683
+ const employee2 = await this.findEmployee(employeeId, organizationId, options);
1684
+ const updatedCompensation = CompensationFactory.addAllowance(
1685
+ employee2.compensation,
1686
+ allowance
1687
+ );
1688
+ employee2.compensation = updatedCompensation;
1689
+ await employee2.save({ session: options.session });
1690
+ logger.info("Allowance added", {
1691
+ employeeId: employee2.employeeId,
1692
+ organizationId: organizationId.toString(),
1693
+ type: allowance.type,
1694
+ value: allowance.value
1695
+ });
1696
+ return this.calculateBreakdown(employeeId, organizationId, options);
1697
+ }
1698
+ /**
1699
+ * Remove allowance with organization validation
1700
+ *
1701
+ * ⚠️ SECURITY: Validates employee belongs to organization before update
1702
+ */
1703
+ async removeAllowance(employeeId, organizationId, allowanceType, options = {}) {
1704
+ const employee2 = await this.findEmployee(employeeId, organizationId, options);
1705
+ const updatedCompensation = CompensationFactory.removeAllowance(
1706
+ employee2.compensation,
1707
+ allowanceType
1708
+ );
1709
+ employee2.compensation = updatedCompensation;
1710
+ await employee2.save({ session: options.session });
1711
+ logger.info("Allowance removed", {
1712
+ employeeId: employee2.employeeId,
1713
+ organizationId: organizationId.toString(),
1714
+ type: allowanceType
1715
+ });
1716
+ return this.calculateBreakdown(employeeId, organizationId, options);
1717
+ }
1718
+ /**
1719
+ * Add deduction with organization validation
1720
+ *
1721
+ * ⚠️ SECURITY: Validates employee belongs to organization before update
1722
+ */
1723
+ async addDeduction(employeeId, organizationId, deduction, options = {}) {
1724
+ const employee2 = await this.findEmployee(employeeId, organizationId, options);
1725
+ const updatedCompensation = CompensationFactory.addDeduction(
1726
+ employee2.compensation,
1727
+ deduction
1728
+ );
1729
+ employee2.compensation = updatedCompensation;
1730
+ await employee2.save({ session: options.session });
1731
+ logger.info("Deduction added", {
1732
+ employeeId: employee2.employeeId,
1733
+ organizationId: organizationId.toString(),
1734
+ type: deduction.type,
1735
+ value: deduction.value
1736
+ });
1737
+ return this.calculateBreakdown(employeeId, organizationId, options);
1738
+ }
1739
+ /**
1740
+ * Remove deduction with organization validation
1741
+ *
1742
+ * ⚠️ SECURITY: Validates employee belongs to organization before update
1743
+ */
1744
+ async removeDeduction(employeeId, organizationId, deductionType, options = {}) {
1745
+ const employee2 = await this.findEmployee(employeeId, organizationId, options);
1746
+ const updatedCompensation = CompensationFactory.removeDeduction(
1747
+ employee2.compensation,
1748
+ deductionType
1749
+ );
1750
+ employee2.compensation = updatedCompensation;
1751
+ await employee2.save({ session: options.session });
1752
+ logger.info("Deduction removed", {
1753
+ employeeId: employee2.employeeId,
1754
+ organizationId: organizationId.toString(),
1755
+ type: deductionType
1756
+ });
1757
+ return this.calculateBreakdown(employeeId, organizationId, options);
1758
+ }
1759
+ /**
1760
+ * Set standard compensation with organization validation
1761
+ *
1762
+ * ⚠️ SECURITY: Validates employee belongs to organization before update
1763
+ */
1764
+ async setStandardCompensation(employeeId, organizationId, baseAmount, options = {}) {
1765
+ const employee2 = await this.findEmployee(employeeId, organizationId, options);
1766
+ employee2.compensation = CompensationPresets.standard(baseAmount);
1767
+ await employee2.save({ session: options.session });
1768
+ logger.info("Standard compensation set", {
1769
+ employeeId: employee2.employeeId,
1770
+ organizationId: organizationId.toString(),
1771
+ baseAmount
1772
+ });
1773
+ return this.calculateBreakdown(employeeId, organizationId, options);
1774
+ }
1775
+ /**
1776
+ * Compare compensation between two employees
1777
+ *
1778
+ * ⚠️ SECURITY: Validates both employees belong to organization
1779
+ */
1780
+ async compareCompensation(employeeId1, employeeId2, organizationId, options = {}) {
1781
+ const breakdown1 = await this.calculateBreakdown(employeeId1, organizationId, options);
1782
+ const breakdown2 = await this.calculateBreakdown(employeeId2, organizationId, options);
1783
+ return {
1784
+ employee1: breakdown1,
1785
+ employee2: breakdown2,
1786
+ difference: {
1787
+ base: breakdown2.baseAmount - breakdown1.baseAmount,
1788
+ gross: breakdown2.grossAmount - breakdown1.grossAmount,
1789
+ net: breakdown2.netAmount - breakdown1.netAmount
1790
+ },
1791
+ ratio: {
1792
+ base: breakdown1.baseAmount > 0 ? breakdown2.baseAmount / breakdown1.baseAmount : 0,
1793
+ gross: breakdown1.grossAmount > 0 ? breakdown2.grossAmount / breakdown1.grossAmount : 0,
1794
+ net: breakdown1.netAmount > 0 ? breakdown2.netAmount / breakdown1.netAmount : 0
1795
+ }
1796
+ };
1797
+ }
1798
+ /**
1799
+ * Get department compensation stats
1800
+ */
1801
+ async getDepartmentCompensationStats(organizationId, department, options = {}) {
1802
+ let query = this.EmployeeModel.find({
1803
+ organizationId: toObjectId(organizationId),
1804
+ department,
1805
+ status: { $in: ["active", "on_leave"] }
1806
+ });
1807
+ if (options.session) {
1808
+ query = query.session(options.session);
1809
+ }
1810
+ const employees = await query.exec();
1811
+ const breakdowns = employees.map(
1812
+ (emp) => CompensationFactory.calculateBreakdown(emp.compensation)
1813
+ );
1814
+ const totals = breakdowns.reduce(
1815
+ (acc, breakdown) => ({
1816
+ totalBase: acc.totalBase + breakdown.baseAmount,
1817
+ totalGross: acc.totalGross + breakdown.grossAmount,
1818
+ totalNet: acc.totalNet + breakdown.netAmount
1819
+ }),
1820
+ { totalBase: 0, totalGross: 0, totalNet: 0 }
1821
+ );
1822
+ const count = employees.length || 1;
1823
+ return {
1824
+ department,
1825
+ employeeCount: employees.length,
1826
+ ...totals,
1827
+ averageBase: Math.round(totals.totalBase / count),
1828
+ averageGross: Math.round(totals.totalGross / count),
1829
+ averageNet: Math.round(totals.totalNet / count)
1830
+ };
1831
+ }
1832
+ /**
1833
+ * Get organization compensation stats
1834
+ */
1835
+ async getOrganizationCompensationStats(organizationId, options = {}) {
1836
+ let query = this.EmployeeModel.find({
1837
+ organizationId: toObjectId(organizationId),
1838
+ status: { $in: ["active", "on_leave"] }
1839
+ });
1840
+ if (options.session) {
1841
+ query = query.session(options.session);
1842
+ }
1843
+ const employees = await query.exec();
1844
+ const breakdowns = employees.map(
1845
+ (emp) => CompensationFactory.calculateBreakdown(emp.compensation)
1846
+ );
1847
+ const totals = breakdowns.reduce(
1848
+ (acc, breakdown) => ({
1849
+ totalBase: acc.totalBase + breakdown.baseAmount,
1850
+ totalGross: acc.totalGross + breakdown.grossAmount,
1851
+ totalNet: acc.totalNet + breakdown.netAmount
1852
+ }),
1853
+ { totalBase: 0, totalGross: 0, totalNet: 0 }
1854
+ );
1855
+ const byDepartment = {};
1856
+ employees.forEach((emp, i) => {
1857
+ const dept = emp.department || "unassigned";
1858
+ if (!byDepartment[dept]) {
1859
+ byDepartment[dept] = { count: 0, totalNet: 0 };
1860
+ }
1861
+ byDepartment[dept].count++;
1862
+ byDepartment[dept].totalNet += breakdowns[i].netAmount;
1863
+ });
1864
+ const count = employees.length || 1;
1865
+ return {
1866
+ employeeCount: employees.length,
1867
+ ...totals,
1868
+ averageBase: Math.round(totals.totalBase / count),
1869
+ averageGross: Math.round(totals.totalGross / count),
1870
+ averageNet: Math.round(totals.totalNet / count),
1871
+ byDepartment
1872
+ };
1873
+ }
1874
+ /**
1875
+ * Find employee helper with organization validation
1876
+ *
1877
+ * ⚠️ SECURITY: Always validates employee belongs to organization
1878
+ */
1879
+ async findEmployee(employeeId, organizationId, options = {}) {
1880
+ const query = {
1881
+ _id: toObjectId(employeeId),
1882
+ organizationId: toObjectId(organizationId)
1883
+ };
1884
+ let mongooseQuery = this.EmployeeModel.findOne(query);
1885
+ if (options.session) {
1886
+ mongooseQuery = mongooseQuery.session(options.session);
1887
+ }
1888
+ const employee2 = await mongooseQuery.exec();
1889
+ if (!employee2) {
1890
+ throw new Error(`Employee not found in organization ${organizationId}`);
1891
+ }
1892
+ return employee2;
1893
+ }
1894
+ };
1895
+ function createCompensationService(EmployeeModel) {
1896
+ return new CompensationService(EmployeeModel);
1897
+ }
1898
+
1899
+ // src/services/tax-withholding.service.ts
1900
+ var TaxWithholdingService = class {
1901
+ constructor(TaxWithholdingModel, TransactionModel, events) {
1902
+ this.TaxWithholdingModel = TaxWithholdingModel;
1903
+ this.TransactionModel = TransactionModel;
1904
+ this.events = events;
1905
+ }
1906
+ /**
1907
+ * Create tax withholding records from payroll breakdown
1908
+ *
1909
+ * Extracts tax deductions from the breakdown and creates separate
1910
+ * TaxWithholding records for each tax type
1911
+ */
1912
+ async createFromBreakdown(params) {
1913
+ const {
1914
+ organizationId,
1915
+ employeeId,
1916
+ userId,
1917
+ payrollRecordId,
1918
+ transactionId,
1919
+ period,
1920
+ breakdown,
1921
+ currency = "BDT",
1922
+ session,
1923
+ context
1924
+ } = params;
1925
+ const taxDeductions = breakdown.deductions?.filter(
1926
+ (d) => d.type === "tax" || this.isTaxDeduction(d.type)
1927
+ ) || [];
1928
+ if (taxDeductions.length === 0) {
1929
+ return [];
1930
+ }
1931
+ const withholdings = [];
1932
+ for (const deduction of taxDeductions) {
1933
+ const taxType = this.mapDeductionTypeToTaxType(deduction.type);
1934
+ const taxRate = breakdown.taxableAmount && breakdown.taxableAmount > 0 ? deduction.amount / breakdown.taxableAmount : 0;
1935
+ const withholdingData = {
1936
+ organizationId,
1937
+ employeeId,
1938
+ userId,
1939
+ payrollRecordId,
1940
+ transactionId,
1941
+ period,
1942
+ amount: deduction.amount,
1943
+ currency,
1944
+ taxType,
1945
+ taxRate,
1946
+ taxableAmount: breakdown.taxableAmount || breakdown.grossSalary,
1947
+ status: TAX_STATUS.PENDING
1948
+ };
1949
+ const [withholding] = await this.TaxWithholdingModel.create([withholdingData], {
1950
+ session
1951
+ });
1952
+ withholdings.push(withholding);
1953
+ if (this.events) {
1954
+ this.events.emitSync("tax:withheld", {
1955
+ withholding: {
1956
+ id: withholding._id,
1957
+ taxType: withholding.taxType,
1958
+ amount: withholding.amount
1959
+ },
1960
+ employee: {
1961
+ id: employeeId,
1962
+ employeeId: ""
1963
+ // Will be filled by caller if needed
1964
+ },
1965
+ payrollRecord: {
1966
+ id: payrollRecordId
1967
+ },
1968
+ period: {
1969
+ month: period.month,
1970
+ year: period.year
1971
+ },
1972
+ organizationId,
1973
+ context
1974
+ });
1975
+ }
1976
+ logger.info("Tax withholding created", {
1977
+ withholdingId: withholding._id.toString(),
1978
+ employeeId: employeeId.toString(),
1979
+ taxType,
1980
+ amount: deduction.amount,
1981
+ period: `${period.month}/${period.year}`
1982
+ });
1983
+ }
1984
+ return withholdings;
1985
+ }
1986
+ /**
1987
+ * Get pending tax withholdings with optional filters
1988
+ */
1989
+ async getPending(params) {
1990
+ const { organizationId, fromPeriod, toPeriod, taxType, employeeId } = params;
1991
+ const options = {};
1992
+ if (fromPeriod) {
1993
+ options.fromMonth = fromPeriod.month;
1994
+ options.fromYear = fromPeriod.year;
1995
+ }
1996
+ if (toPeriod) {
1997
+ options.toMonth = toPeriod.month;
1998
+ options.toYear = toPeriod.year;
1999
+ }
2000
+ if (taxType) {
2001
+ options.taxType = taxType;
2002
+ }
2003
+ let query = this.TaxWithholdingModel.findPending(
2004
+ toObjectId(organizationId),
2005
+ options
2006
+ );
2007
+ if (employeeId) {
2008
+ query = query.where({ employeeId: toObjectId(employeeId) });
2009
+ }
2010
+ return query.exec();
2011
+ }
2012
+ /**
2013
+ * Get tax summary aggregated by type, period, or employee
2014
+ */
2015
+ async getSummary(params) {
2016
+ const { organizationId, fromPeriod, toPeriod, groupBy = "type" } = params;
2017
+ if (groupBy === "type") {
2018
+ const byType = await this.TaxWithholdingModel.getSummaryByType(
2019
+ toObjectId(organizationId),
2020
+ fromPeriod,
2021
+ toPeriod
2022
+ );
2023
+ const totalAmount = byType.reduce((sum2, item) => sum2 + item.totalAmount, 0);
2024
+ const count = byType.reduce((sum2, item) => sum2 + item.count, 0);
2025
+ return {
2026
+ totalAmount,
2027
+ count,
2028
+ byType,
2029
+ period: {
2030
+ fromMonth: fromPeriod.month,
2031
+ fromYear: fromPeriod.year,
2032
+ toMonth: toPeriod.month,
2033
+ toYear: toPeriod.year
2034
+ }
2035
+ };
2036
+ }
2037
+ throw new Error(`groupBy '${groupBy}' not yet implemented`);
2038
+ }
2039
+ /**
2040
+ * Mark tax withholdings as paid
2041
+ *
2042
+ * Updates status, optionally creates government payment transaction,
2043
+ * and emits tax:paid event
2044
+ */
2045
+ async markPaid(params) {
2046
+ const {
2047
+ organizationId,
2048
+ withholdingIds,
2049
+ createTransaction = false,
2050
+ referenceNumber,
2051
+ paidAt = /* @__PURE__ */ new Date(),
2052
+ notes,
2053
+ context
2054
+ } = params;
2055
+ const session = context?.session;
2056
+ const withholdings = await this.TaxWithholdingModel.find({
2057
+ _id: { $in: withholdingIds.map(toObjectId) },
2058
+ organizationId: toObjectId(organizationId)
2059
+ }).session(session || null);
2060
+ if (withholdings.length === 0) {
2061
+ throw new Error("No tax withholdings found with provided IDs");
2062
+ }
2063
+ const totalAmount = withholdings.reduce((sum2, w) => sum2 + w.amount, 0);
2064
+ let governmentTransaction = null;
2065
+ if (createTransaction && this.TransactionModel) {
2066
+ const transactionData = {
2067
+ organizationId: toObjectId(organizationId),
2068
+ type: "tax_payment",
2069
+ flow: "outflow",
2070
+ tags: ["tax", "government", "withholding"],
2071
+ amount: totalAmount,
2072
+ grossAmount: totalAmount,
2073
+ currency: withholdings[0].currency || "BDT",
2074
+ status: "completed",
2075
+ date: paidAt,
2076
+ description: `Tax payment to government - ${referenceNumber || "Multiple withholdings"}`,
2077
+ notes,
2078
+ metadata: {
2079
+ withholdingIds: withholdingIds.map((id) => id.toString()),
2080
+ referenceNumber
2081
+ }
2082
+ };
2083
+ [governmentTransaction] = await this.TransactionModel.create([transactionData], {
2084
+ session
2085
+ });
2086
+ }
2087
+ for (const withholding of withholdings) {
2088
+ withholding.markAsPaid(
2089
+ governmentTransaction?._id,
2090
+ referenceNumber,
2091
+ paidAt
2092
+ );
2093
+ await withholding.save({ session });
2094
+ }
2095
+ if (this.events) {
2096
+ this.events.emitSync("tax:paid", {
2097
+ withholdings: withholdings.map((w) => ({
2098
+ id: w._id,
2099
+ taxType: w.taxType,
2100
+ amount: w.amount
2101
+ })),
2102
+ transaction: governmentTransaction ? {
2103
+ id: governmentTransaction._id,
2104
+ amount: governmentTransaction.amount
2105
+ } : void 0,
2106
+ totalAmount,
2107
+ referenceNumber,
2108
+ paidAt,
2109
+ organizationId: toObjectId(organizationId),
2110
+ context
2111
+ });
2112
+ }
2113
+ logger.info("Tax withholdings marked as paid", {
2114
+ count: withholdings.length,
2115
+ totalAmount,
2116
+ referenceNumber,
2117
+ transactionId: governmentTransaction?._id.toString()
2118
+ });
2119
+ return {
2120
+ withholdings,
2121
+ transaction: governmentTransaction
2122
+ };
2123
+ }
2124
+ /**
2125
+ * Get tax withholdings for a specific payroll record
2126
+ */
2127
+ async getByPayrollRecord(payrollRecordId) {
2128
+ return this.TaxWithholdingModel.getByPayrollRecord(toObjectId(payrollRecordId)).exec();
2129
+ }
2130
+ /**
2131
+ * Get tax withholdings for a specific employee
2132
+ */
2133
+ async getByEmployee(employeeId, options) {
2134
+ return this.TaxWithholdingModel.findByEmployee(toObjectId(employeeId), options).exec();
2135
+ }
2136
+ // ============================================================================
2137
+ // Private Helpers
2138
+ // ============================================================================
2139
+ /**
2140
+ * Check if deduction type is a tax deduction
2141
+ */
2142
+ isTaxDeduction(deductionType) {
2143
+ const taxTypes = ["tax", "income_tax", "social_security", "health_insurance", "pension", "employment_insurance", "local_tax"];
2144
+ return taxTypes.includes(deductionType.toLowerCase());
2145
+ }
2146
+ /**
2147
+ * Map deduction type to TaxType enum
2148
+ */
2149
+ mapDeductionTypeToTaxType(deductionType) {
2150
+ const typeMap = {
2151
+ "tax": TAX_TYPE.INCOME_TAX,
2152
+ "income_tax": TAX_TYPE.INCOME_TAX,
2153
+ "social_security": TAX_TYPE.SOCIAL_SECURITY,
2154
+ "health_insurance": TAX_TYPE.HEALTH_INSURANCE,
2155
+ "pension": TAX_TYPE.PENSION,
2156
+ "employment_insurance": TAX_TYPE.EMPLOYMENT_INSURANCE,
2157
+ "local_tax": TAX_TYPE.LOCAL_TAX
2158
+ };
2159
+ return typeMap[deductionType.toLowerCase()] || TAX_TYPE.OTHER;
2160
+ }
2161
+ };
2162
+ function createTaxWithholdingService(config) {
2163
+ return new TaxWithholdingService(
2164
+ config.TaxWithholdingModel,
2165
+ config.TransactionModel,
2166
+ config.events
2167
+ );
2168
+ }
2169
+
2170
+ export { CompensationService, EmployeeService, PayrollService, TaxWithholdingService, createCompensationService, createEmployeeService, createPayrollService, createTaxWithholdingService };
2171
+ //# sourceMappingURL=index.js.map
2172
+ //# sourceMappingURL=index.js.map