@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.
- package/README.md +2599 -253
- package/dist/calculators/index.d.ts +433 -0
- package/dist/calculators/index.js +283 -0
- package/dist/calculators/index.js.map +1 -0
- package/dist/core/index.d.ts +85 -251
- package/dist/core/index.js +286 -91
- package/dist/core/index.js.map +1 -1
- package/dist/employee-identity-DXhgOgXE.d.ts +473 -0
- package/dist/employee.factory-BlZqhiCk.d.ts +189 -0
- package/dist/idempotency-Cw2CWicb.d.ts +52 -0
- package/dist/index.d.ts +618 -683
- package/dist/index.js +8336 -3580
- package/dist/index.js.map +1 -1
- package/dist/jurisdiction/index.d.ts +660 -0
- package/dist/jurisdiction/index.js +533 -0
- package/dist/jurisdiction/index.js.map +1 -0
- package/dist/payroll.d.ts +261 -65
- package/dist/payroll.js +4164 -1075
- package/dist/payroll.js.map +1 -1
- package/dist/schemas/index.d.ts +1176 -783
- package/dist/schemas/index.js +368 -28
- package/dist/schemas/index.js.map +1 -1
- package/dist/services/index.d.ts +582 -3
- package/dist/services/index.js +572 -96
- package/dist/services/index.js.map +1 -1
- package/dist/shift-compliance/index.d.ts +1171 -0
- package/dist/shift-compliance/index.js +1479 -0
- package/dist/shift-compliance/index.js.map +1 -0
- package/dist/types-BN3K_Uhr.d.ts +1842 -0
- package/dist/utils/index.d.ts +22 -2
- package/dist/utils/index.js +470 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +24 -6
- package/dist/index-CTjHlCzz.d.ts +0 -721
- package/dist/plugin-D9mOr3_d.d.ts +0 -333
- package/dist/types-BSYyX2KJ.d.ts +0 -671
package/dist/services/index.js
CHANGED
|
@@ -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 ??
|
|
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 ||
|
|
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(
|
|
401
|
-
return this.whereBetween("compensation.netSalary",
|
|
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
|
-
|
|
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
|
-
|
|
642
|
+
mongooseQuery = mongooseQuery.session(options.session);
|
|
554
643
|
}
|
|
555
644
|
if (options.populate) {
|
|
556
|
-
|
|
645
|
+
mongooseQuery = mongooseQuery.populate("userId", "name email phone");
|
|
557
646
|
}
|
|
558
|
-
return
|
|
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
|
-
|
|
622
|
-
|
|
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(
|
|
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.
|
|
801
|
+
async updateCompensation(employeeId, organizationId, compensation, options = {}) {
|
|
802
|
+
const currentEmployee = await this.findById(employeeId, organizationId, options);
|
|
654
803
|
if (!currentEmployee) {
|
|
655
|
-
throw new Error(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1886
|
+
mongooseQuery = mongooseQuery.session(options.session);
|
|
1682
1887
|
}
|
|
1683
|
-
const employee2 = await
|
|
1888
|
+
const employee2 = await mongooseQuery.exec();
|
|
1684
1889
|
if (!employee2) {
|
|
1685
|
-
throw new Error(
|
|
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
|
-
|
|
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
|