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

@@ -41,10 +41,42 @@ function calculateProbationEnd(hireDate, probationMonths) {
41
41
 
42
42
  // src/config.ts
43
43
  var HRM_CONFIG = {
44
+ dataRetention: {
45
+ payrollRecordsTTL: 63072e3,
46
+ // 2 years in seconds
47
+ exportWarningDays: 30,
48
+ archiveBeforeDeletion: true
49
+ },
44
50
  payroll: {
45
- defaultCurrency: "BDT"},
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
+ },
46
64
  employment: {
47
- defaultProbationMonths: 3}};
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
+ };
48
80
  var ORG_ROLES = {
49
81
  OWNER: {
50
82
  key: "owner",
@@ -80,15 +112,24 @@ var ORG_ROLES = {
80
112
  Object.values(ORG_ROLES).map((role) => role.key);
81
113
 
82
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
+ }
83
120
  var EmployeeFactory = class {
84
121
  /**
85
122
  * Create employee data object
86
123
  */
87
- static create(params) {
124
+ static create(params, config = HRM_CONFIG) {
88
125
  const { userId, organizationId, employment, compensation, bankDetails } = params;
89
126
  const hireDate = employment.hireDate || /* @__PURE__ */ new Date();
127
+ const normalizedEmail = normalizeEmail(employment.email);
90
128
  return {
91
- userId,
129
+ ...userId ? { userId } : {},
130
+ // Only include userId if present
131
+ ...normalizedEmail ? { email: normalizedEmail } : {},
132
+ // Include normalized email for guest employees
92
133
  organizationId,
93
134
  employeeId: employment.employeeId || `EMP-${Date.now()}-${Math.random().toString(36).substr(2, 6).toUpperCase()}`,
94
135
  employmentType: employment.type || "full_time",
@@ -98,9 +139,9 @@ var EmployeeFactory = class {
98
139
  hireDate,
99
140
  probationEndDate: calculateProbationEnd(
100
141
  hireDate,
101
- employment.probationMonths ?? HRM_CONFIG.employment.defaultProbationMonths
142
+ employment.probationMonths ?? config.employment.defaultProbationMonths
102
143
  ),
103
- compensation: this.createCompensation(compensation),
144
+ compensation: this.createCompensation(compensation, config),
104
145
  workSchedule: employment.workSchedule || this.defaultWorkSchedule(),
105
146
  bankDetails: bankDetails || {},
106
147
  payrollStats: {
@@ -113,11 +154,11 @@ var EmployeeFactory = class {
113
154
  /**
114
155
  * Create compensation object
115
156
  */
116
- static createCompensation(params) {
157
+ static createCompensation(params, config = HRM_CONFIG) {
117
158
  return {
118
159
  baseAmount: params.baseAmount,
119
160
  frequency: params.frequency || "monthly",
120
- currency: params.currency || HRM_CONFIG.payroll.defaultCurrency,
161
+ currency: params.currency || config.payroll.defaultCurrency,
121
162
  allowances: (params.allowances || []).map((a) => ({
122
163
  type: a.type || "other",
123
164
  name: a.name || a.type || "other",
@@ -325,6 +366,30 @@ var EmployeeQueryBuilder = class extends QueryBuilder {
325
366
  forUser(userId) {
326
367
  return this.where("userId", toObjectId(userId));
327
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
+ }
328
393
  /**
329
394
  * Filter by status(es)
330
395
  */
@@ -397,8 +462,8 @@ var EmployeeQueryBuilder = class extends QueryBuilder {
397
462
  /**
398
463
  * Filter by salary range
399
464
  */
400
- withSalaryRange(min, max) {
401
- return this.whereBetween("compensation.netSalary", min, max);
465
+ withSalaryRange(min2, max2) {
466
+ return this.whereBetween("compensation.netSalary", min2, max2);
402
467
  }
403
468
  };
404
469
  var PayrollQueryBuilder = class extends QueryBuilder {
@@ -410,6 +475,9 @@ var PayrollQueryBuilder = class extends QueryBuilder {
410
475
  }
411
476
  /**
412
477
  * Filter by employee
478
+ *
479
+ * Note: PayrollRecord.employeeId is always ObjectId _id
480
+ * If passing a string business ID, resolve to _id first
413
481
  */
414
482
  forEmployee(employeeId) {
415
483
  return this.where("employeeId", toObjectId(employeeId));
@@ -472,6 +540,17 @@ function employee() {
472
540
  function payroll() {
473
541
  return new PayrollQueryBuilder();
474
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"};
475
554
 
476
555
  // src/utils/validation.ts
477
556
  function isActive(employee2) {
@@ -541,21 +620,31 @@ var logger = {
541
620
 
542
621
  // src/services/employee.service.ts
543
622
  var EmployeeService = class {
544
- constructor(EmployeeModel) {
623
+ constructor(EmployeeModel, config) {
545
624
  this.EmployeeModel = EmployeeModel;
625
+ this.config = config || HRM_CONFIG;
546
626
  }
627
+ config;
547
628
  /**
548
- * Find employee by ID
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
549
634
  */
550
- async findById(employeeId, options = {}) {
551
- let query = this.EmployeeModel.findById(toObjectId(employeeId));
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);
552
641
  if (options.session) {
553
- query = query.session(options.session);
642
+ mongooseQuery = mongooseQuery.session(options.session);
554
643
  }
555
644
  if (options.populate) {
556
- query = query.populate("userId", "name email phone");
645
+ mongooseQuery = mongooseQuery.populate("userId", "name email phone");
557
646
  }
558
- return query.exec();
647
+ return mongooseQuery.exec();
559
648
  }
560
649
  /**
561
650
  * Find employee by user and organization
@@ -568,6 +657,39 @@ var EmployeeService = class {
568
657
  }
569
658
  return mongooseQuery.exec();
570
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
+ }
571
693
  /**
572
694
  * Find active employees in organization
573
695
  */
@@ -617,10 +739,32 @@ var EmployeeService = class {
617
739
  * Create new employee
618
740
  */
619
741
  async create(params, options = {}) {
620
- const employeeData = EmployeeFactory.create(params);
621
- const [employee2] = await this.EmployeeModel.create([employeeData], {
622
- session: options.session
623
- });
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
+ }
624
768
  logger.info("Employee created", {
625
769
  employeeId: employee2.employeeId,
626
770
  organizationId: employee2.organizationId.toString()
@@ -628,31 +772,36 @@ var EmployeeService = class {
628
772
  return employee2;
629
773
  }
630
774
  /**
631
- * Update employee status
775
+ * Update employee status with organization validation
776
+ *
777
+ * ⚠️ SECURITY: Validates employee belongs to organization before update
632
778
  */
633
- async updateStatus(employeeId, status, context = {}, options = {}) {
634
- const employee2 = await this.findById(employeeId, options);
779
+ async updateStatus(employeeId, organizationId, status, context = {}, options = {}) {
780
+ const employee2 = await this.findById(employeeId, organizationId, options);
635
781
  if (!employee2) {
636
- throw new Error("Employee not found");
782
+ throw new Error(`Employee not found in organization ${organizationId}`);
637
783
  }
638
784
  employee2.status = status;
639
785
  await employee2.save({ session: options.session });
640
786
  logger.info("Employee status updated", {
641
787
  employeeId: employee2.employeeId,
788
+ organizationId: organizationId.toString(),
642
789
  newStatus: status
643
790
  });
644
791
  return employee2;
645
792
  }
646
793
  /**
647
- * Update employee compensation
794
+ * Update employee compensation with organization validation
795
+ *
796
+ * ⚠️ SECURITY: Validates employee belongs to organization before update
648
797
  *
649
798
  * NOTE: This merges the compensation fields rather than replacing the entire object.
650
799
  * To update allowances/deductions, use addAllowance/removeAllowance methods.
651
800
  */
652
- async updateCompensation(employeeId, compensation, options = {}) {
653
- const currentEmployee = await this.EmployeeModel.findById(toObjectId(employeeId)).session(options.session || null);
801
+ async updateCompensation(employeeId, organizationId, compensation, options = {}) {
802
+ const currentEmployee = await this.findById(employeeId, organizationId, options);
654
803
  if (!currentEmployee) {
655
- throw new Error("Employee not found");
804
+ throw new Error(`Employee not found in organization ${organizationId}`);
656
805
  }
657
806
  const updateFields = {
658
807
  "compensation.lastModified": /* @__PURE__ */ new Date()
@@ -669,14 +818,22 @@ var EmployeeService = class {
669
818
  if (compensation.effectiveFrom !== void 0) {
670
819
  updateFields["compensation.effectiveFrom"] = compensation.effectiveFrom;
671
820
  }
672
- const employee2 = await this.EmployeeModel.findByIdAndUpdate(
673
- toObjectId(employeeId),
821
+ const query = {
822
+ _id: toObjectId(employeeId),
823
+ organizationId: toObjectId(organizationId)
824
+ };
825
+ const employee2 = await this.EmployeeModel.findOneAndUpdate(
826
+ query,
674
827
  { $set: updateFields },
675
828
  { new: true, runValidators: true, session: options.session }
676
829
  );
677
830
  if (!employee2) {
678
- throw new Error("Employee not found");
831
+ throw new Error(`Employee not found in organization ${organizationId}`);
679
832
  }
833
+ logger.info("Employee compensation updated", {
834
+ employeeId: employee2.employeeId,
835
+ organizationId: organizationId.toString()
836
+ });
680
837
  return employee2;
681
838
  }
682
839
  /**
@@ -742,8 +899,8 @@ var EmployeeService = class {
742
899
  return canReceiveSalary(employee2);
743
900
  }
744
901
  };
745
- function createEmployeeService(EmployeeModel) {
746
- return new EmployeeService(EmployeeModel);
902
+ function createEmployeeService(EmployeeModel, config) {
903
+ return new EmployeeService(EmployeeModel, config);
747
904
  }
748
905
 
749
906
  // src/utils/calculation.ts
@@ -994,12 +1151,14 @@ var PayrollService = class {
994
1151
  return payroll2;
995
1152
  }
996
1153
  /**
997
- * Generate payroll for employee
1154
+ * Generate payroll for employee with organization validation
1155
+ *
1156
+ * ⚠️ SECURITY: Validates employee belongs to organization
998
1157
  */
999
1158
  async generateForEmployee(employeeId, organizationId, month, year, options = {}) {
1000
- const employee2 = await this.employeeService.findById(employeeId, options);
1159
+ const employee2 = await this.employeeService.findById(employeeId, organizationId, options);
1001
1160
  if (!employee2) {
1002
- throw new Error("Employee not found");
1161
+ throw new Error(`Employee not found in organization ${organizationId}`);
1003
1162
  }
1004
1163
  if (!canReceiveSalary(employee2)) {
1005
1164
  throw new Error("Employee not eligible for payroll");
@@ -1086,12 +1245,22 @@ var PayrollService = class {
1086
1245
  };
1087
1246
  }
1088
1247
  /**
1089
- * Mark payroll as paid
1248
+ * Mark payroll as paid with organization validation
1249
+ *
1250
+ * ⚠️ SECURITY: Validates payroll belongs to organization
1090
1251
  */
1091
- async markAsPaid(payrollId, paymentDetails = {}, options = {}) {
1092
- const payroll2 = await this.findById(payrollId, options);
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;
1093
1262
  if (!payroll2) {
1094
- throw new Error("Payroll not found");
1263
+ throw new Error(`Payroll not found in organization ${organizationId}`);
1095
1264
  }
1096
1265
  if (payroll2.status === "paid") {
1097
1266
  throw new Error("Payroll already paid");
@@ -1112,12 +1281,22 @@ var PayrollService = class {
1112
1281
  return updated;
1113
1282
  }
1114
1283
  /**
1115
- * Mark payroll as processed
1284
+ * Mark payroll as processed with organization validation
1285
+ *
1286
+ * ⚠️ SECURITY: Validates payroll belongs to organization
1116
1287
  */
1117
- async markAsProcessed(payrollId, options = {}) {
1118
- const payroll2 = await this.findById(payrollId, options);
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;
1119
1298
  if (!payroll2) {
1120
- throw new Error("Payroll not found");
1299
+ throw new Error(`Payroll not found in organization ${organizationId}`);
1121
1300
  }
1122
1301
  const payrollObj = payroll2.toObject();
1123
1302
  const updatedData = PayrollFactory.markAsProcessed(payrollObj);
@@ -1434,31 +1613,30 @@ var CompensationService = class {
1434
1613
  this.EmployeeModel = EmployeeModel;
1435
1614
  }
1436
1615
  /**
1437
- * Get employee compensation
1616
+ * Get employee compensation with organization validation
1617
+ *
1618
+ * ⚠️ SECURITY: Validates employee belongs to organization
1438
1619
  */
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
- }
1620
+ async getEmployeeCompensation(employeeId, organizationId, options = {}) {
1621
+ const employee2 = await this.findEmployee(employeeId, organizationId, options);
1448
1622
  return employee2.compensation;
1449
1623
  }
1450
1624
  /**
1451
- * Calculate compensation breakdown
1625
+ * Calculate compensation breakdown with organization validation
1626
+ *
1627
+ * ⚠️ SECURITY: Validates employee belongs to organization
1452
1628
  */
1453
- async calculateBreakdown(employeeId, options = {}) {
1454
- const compensation = await this.getEmployeeCompensation(employeeId, options);
1629
+ async calculateBreakdown(employeeId, organizationId, options = {}) {
1630
+ const compensation = await this.getEmployeeCompensation(employeeId, organizationId, options);
1455
1631
  return CompensationFactory.calculateBreakdown(compensation);
1456
1632
  }
1457
1633
  /**
1458
- * Update base amount
1634
+ * Update base amount with organization validation
1635
+ *
1636
+ * ⚠️ SECURITY: Validates employee belongs to organization before update
1459
1637
  */
1460
- async updateBaseAmount(employeeId, newAmount, effectiveFrom = /* @__PURE__ */ new Date(), options = {}) {
1461
- const employee2 = await this.findEmployee(employeeId, options);
1638
+ async updateBaseAmount(employeeId, organizationId, newAmount, effectiveFrom = /* @__PURE__ */ new Date(), options = {}) {
1639
+ const employee2 = await this.findEmployee(employeeId, organizationId, options);
1462
1640
  const updatedCompensation = CompensationFactory.updateBaseAmount(
1463
1641
  employee2.compensation,
1464
1642
  newAmount,
@@ -1468,15 +1646,18 @@ var CompensationService = class {
1468
1646
  await employee2.save({ session: options.session });
1469
1647
  logger.info("Compensation base amount updated", {
1470
1648
  employeeId: employee2.employeeId,
1649
+ organizationId: organizationId.toString(),
1471
1650
  newAmount
1472
1651
  });
1473
- return this.calculateBreakdown(employeeId, options);
1652
+ return this.calculateBreakdown(employeeId, organizationId, options);
1474
1653
  }
1475
1654
  /**
1476
- * Apply salary increment
1655
+ * Apply salary increment with organization validation
1656
+ *
1657
+ * ⚠️ SECURITY: Validates employee belongs to organization before update
1477
1658
  */
1478
- async applyIncrement(employeeId, params, options = {}) {
1479
- const employee2 = await this.findEmployee(employeeId, options);
1659
+ async applyIncrement(employeeId, organizationId, params, options = {}) {
1660
+ const employee2 = await this.findEmployee(employeeId, organizationId, options);
1480
1661
  const previousAmount = employee2.compensation.baseAmount;
1481
1662
  const updatedCompensation = CompensationFactory.applyIncrement(
1482
1663
  employee2.compensation,
@@ -1486,17 +1667,20 @@ var CompensationService = class {
1486
1667
  await employee2.save({ session: options.session });
1487
1668
  logger.info("Salary increment applied", {
1488
1669
  employeeId: employee2.employeeId,
1670
+ organizationId: organizationId.toString(),
1489
1671
  previousAmount,
1490
1672
  newAmount: updatedCompensation.baseAmount,
1491
1673
  percentage: params.percentage
1492
1674
  });
1493
- return this.calculateBreakdown(employeeId, options);
1675
+ return this.calculateBreakdown(employeeId, organizationId, options);
1494
1676
  }
1495
1677
  /**
1496
- * Add allowance
1678
+ * Add allowance with organization validation
1679
+ *
1680
+ * ⚠️ SECURITY: Validates employee belongs to organization before update
1497
1681
  */
1498
- async addAllowance(employeeId, allowance, options = {}) {
1499
- const employee2 = await this.findEmployee(employeeId, options);
1682
+ async addAllowance(employeeId, organizationId, allowance, options = {}) {
1683
+ const employee2 = await this.findEmployee(employeeId, organizationId, options);
1500
1684
  const updatedCompensation = CompensationFactory.addAllowance(
1501
1685
  employee2.compensation,
1502
1686
  allowance
@@ -1505,16 +1689,19 @@ var CompensationService = class {
1505
1689
  await employee2.save({ session: options.session });
1506
1690
  logger.info("Allowance added", {
1507
1691
  employeeId: employee2.employeeId,
1692
+ organizationId: organizationId.toString(),
1508
1693
  type: allowance.type,
1509
1694
  value: allowance.value
1510
1695
  });
1511
- return this.calculateBreakdown(employeeId, options);
1696
+ return this.calculateBreakdown(employeeId, organizationId, options);
1512
1697
  }
1513
1698
  /**
1514
- * Remove allowance
1699
+ * Remove allowance with organization validation
1700
+ *
1701
+ * ⚠️ SECURITY: Validates employee belongs to organization before update
1515
1702
  */
1516
- async removeAllowance(employeeId, allowanceType, options = {}) {
1517
- const employee2 = await this.findEmployee(employeeId, options);
1703
+ async removeAllowance(employeeId, organizationId, allowanceType, options = {}) {
1704
+ const employee2 = await this.findEmployee(employeeId, organizationId, options);
1518
1705
  const updatedCompensation = CompensationFactory.removeAllowance(
1519
1706
  employee2.compensation,
1520
1707
  allowanceType
@@ -1523,15 +1710,18 @@ var CompensationService = class {
1523
1710
  await employee2.save({ session: options.session });
1524
1711
  logger.info("Allowance removed", {
1525
1712
  employeeId: employee2.employeeId,
1713
+ organizationId: organizationId.toString(),
1526
1714
  type: allowanceType
1527
1715
  });
1528
- return this.calculateBreakdown(employeeId, options);
1716
+ return this.calculateBreakdown(employeeId, organizationId, options);
1529
1717
  }
1530
1718
  /**
1531
- * Add deduction
1719
+ * Add deduction with organization validation
1720
+ *
1721
+ * ⚠️ SECURITY: Validates employee belongs to organization before update
1532
1722
  */
1533
- async addDeduction(employeeId, deduction, options = {}) {
1534
- const employee2 = await this.findEmployee(employeeId, options);
1723
+ async addDeduction(employeeId, organizationId, deduction, options = {}) {
1724
+ const employee2 = await this.findEmployee(employeeId, organizationId, options);
1535
1725
  const updatedCompensation = CompensationFactory.addDeduction(
1536
1726
  employee2.compensation,
1537
1727
  deduction
@@ -1540,16 +1730,19 @@ var CompensationService = class {
1540
1730
  await employee2.save({ session: options.session });
1541
1731
  logger.info("Deduction added", {
1542
1732
  employeeId: employee2.employeeId,
1733
+ organizationId: organizationId.toString(),
1543
1734
  type: deduction.type,
1544
1735
  value: deduction.value
1545
1736
  });
1546
- return this.calculateBreakdown(employeeId, options);
1737
+ return this.calculateBreakdown(employeeId, organizationId, options);
1547
1738
  }
1548
1739
  /**
1549
- * Remove deduction
1740
+ * Remove deduction with organization validation
1741
+ *
1742
+ * ⚠️ SECURITY: Validates employee belongs to organization before update
1550
1743
  */
1551
- async removeDeduction(employeeId, deductionType, options = {}) {
1552
- const employee2 = await this.findEmployee(employeeId, options);
1744
+ async removeDeduction(employeeId, organizationId, deductionType, options = {}) {
1745
+ const employee2 = await this.findEmployee(employeeId, organizationId, options);
1553
1746
  const updatedCompensation = CompensationFactory.removeDeduction(
1554
1747
  employee2.compensation,
1555
1748
  deductionType
@@ -1558,29 +1751,35 @@ var CompensationService = class {
1558
1751
  await employee2.save({ session: options.session });
1559
1752
  logger.info("Deduction removed", {
1560
1753
  employeeId: employee2.employeeId,
1754
+ organizationId: organizationId.toString(),
1561
1755
  type: deductionType
1562
1756
  });
1563
- return this.calculateBreakdown(employeeId, options);
1757
+ return this.calculateBreakdown(employeeId, organizationId, options);
1564
1758
  }
1565
1759
  /**
1566
- * Set standard compensation
1760
+ * Set standard compensation with organization validation
1761
+ *
1762
+ * ⚠️ SECURITY: Validates employee belongs to organization before update
1567
1763
  */
1568
- async setStandardCompensation(employeeId, baseAmount, options = {}) {
1569
- const employee2 = await this.findEmployee(employeeId, options);
1764
+ async setStandardCompensation(employeeId, organizationId, baseAmount, options = {}) {
1765
+ const employee2 = await this.findEmployee(employeeId, organizationId, options);
1570
1766
  employee2.compensation = CompensationPresets.standard(baseAmount);
1571
1767
  await employee2.save({ session: options.session });
1572
1768
  logger.info("Standard compensation set", {
1573
1769
  employeeId: employee2.employeeId,
1770
+ organizationId: organizationId.toString(),
1574
1771
  baseAmount
1575
1772
  });
1576
- return this.calculateBreakdown(employeeId, options);
1773
+ return this.calculateBreakdown(employeeId, organizationId, options);
1577
1774
  }
1578
1775
  /**
1579
1776
  * Compare compensation between two employees
1777
+ *
1778
+ * ⚠️ SECURITY: Validates both employees belong to organization
1580
1779
  */
1581
- async compareCompensation(employeeId1, employeeId2, options = {}) {
1582
- const breakdown1 = await this.calculateBreakdown(employeeId1, options);
1583
- const breakdown2 = await this.calculateBreakdown(employeeId2, options);
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);
1584
1783
  return {
1585
1784
  employee1: breakdown1,
1586
1785
  employee2: breakdown2,
@@ -1673,16 +1872,22 @@ var CompensationService = class {
1673
1872
  };
1674
1873
  }
1675
1874
  /**
1676
- * Find employee helper
1875
+ * Find employee helper with organization validation
1876
+ *
1877
+ * ⚠️ SECURITY: Always validates employee belongs to organization
1677
1878
  */
1678
- async findEmployee(employeeId, options = {}) {
1679
- let query = this.EmployeeModel.findById(toObjectId(employeeId));
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);
1680
1885
  if (options.session) {
1681
- query = query.session(options.session);
1886
+ mongooseQuery = mongooseQuery.session(options.session);
1682
1887
  }
1683
- const employee2 = await query.exec();
1888
+ const employee2 = await mongooseQuery.exec();
1684
1889
  if (!employee2) {
1685
- throw new Error("Employee not found");
1890
+ throw new Error(`Employee not found in organization ${organizationId}`);
1686
1891
  }
1687
1892
  return employee2;
1688
1893
  }
@@ -1691,6 +1896,277 @@ function createCompensationService(EmployeeModel) {
1691
1896
  return new CompensationService(EmployeeModel);
1692
1897
  }
1693
1898
 
1694
- export { CompensationService, EmployeeService, PayrollService, createCompensationService, createEmployeeService, createPayrollService };
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 };
1695
2171
  //# sourceMappingURL=index.js.map
1696
2172
  //# sourceMappingURL=index.js.map