@classytic/payroll 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +574 -0
- package/package.json +59 -0
- package/src/config.js +177 -0
- package/src/core/compensation.manager.js +242 -0
- package/src/core/employment.manager.js +224 -0
- package/src/core/payroll.manager.js +499 -0
- package/src/enums.js +141 -0
- package/src/factories/compensation.factory.js +198 -0
- package/src/factories/employee.factory.js +173 -0
- package/src/factories/payroll.factory.js +247 -0
- package/src/hrm.orchestrator.js +139 -0
- package/src/index.js +172 -0
- package/src/init.js +41 -0
- package/src/models/payroll-record.model.js +126 -0
- package/src/plugins/employee.plugin.js +157 -0
- package/src/schemas/employment.schema.js +126 -0
- package/src/services/compensation.service.js +231 -0
- package/src/services/employee.service.js +162 -0
- package/src/services/payroll.service.js +213 -0
- package/src/utils/calculation.utils.js +91 -0
- package/src/utils/date.utils.js +120 -0
- package/src/utils/logger.js +36 -0
- package/src/utils/query-builders.js +185 -0
- package/src/utils/validation.utils.js +122 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import mongoose from 'mongoose';
|
|
2
|
+
import {
|
|
3
|
+
EMPLOYMENT_TYPE_VALUES,
|
|
4
|
+
EMPLOYEE_STATUS_VALUES,
|
|
5
|
+
DEPARTMENT_VALUES,
|
|
6
|
+
PAYMENT_FREQUENCY_VALUES,
|
|
7
|
+
ALLOWANCE_TYPE_VALUES,
|
|
8
|
+
DEDUCTION_TYPE_VALUES,
|
|
9
|
+
TERMINATION_REASON_VALUES,
|
|
10
|
+
} from '../enums.js';
|
|
11
|
+
|
|
12
|
+
const { Schema } = mongoose;
|
|
13
|
+
|
|
14
|
+
const allowanceSchema = new Schema({
|
|
15
|
+
type: { type: String, enum: ALLOWANCE_TYPE_VALUES, required: true },
|
|
16
|
+
amount: { type: Number, required: true, min: 0 },
|
|
17
|
+
taxable: { type: Boolean, default: true },
|
|
18
|
+
recurring: { type: Boolean, default: true },
|
|
19
|
+
effectiveFrom: { type: Date, default: () => new Date() },
|
|
20
|
+
effectiveTo: { type: Date },
|
|
21
|
+
}, { _id: false });
|
|
22
|
+
|
|
23
|
+
const deductionSchema = new Schema({
|
|
24
|
+
type: { type: String, enum: DEDUCTION_TYPE_VALUES, required: true },
|
|
25
|
+
amount: { type: Number, required: true, min: 0 },
|
|
26
|
+
auto: { type: Boolean, default: false },
|
|
27
|
+
recurring: { type: Boolean, default: true },
|
|
28
|
+
effectiveFrom: { type: Date, default: () => new Date() },
|
|
29
|
+
effectiveTo: { type: Date },
|
|
30
|
+
description: { type: String },
|
|
31
|
+
}, { _id: false });
|
|
32
|
+
|
|
33
|
+
const compensationSchema = new Schema({
|
|
34
|
+
baseAmount: { type: Number, required: true, min: 0 },
|
|
35
|
+
frequency: { type: String, enum: PAYMENT_FREQUENCY_VALUES, default: 'monthly' },
|
|
36
|
+
currency: { type: String, default: 'BDT' },
|
|
37
|
+
|
|
38
|
+
allowances: [allowanceSchema],
|
|
39
|
+
deductions: [deductionSchema],
|
|
40
|
+
|
|
41
|
+
grossSalary: { type: Number, default: 0 },
|
|
42
|
+
netSalary: { type: Number, default: 0 },
|
|
43
|
+
|
|
44
|
+
effectiveFrom: { type: Date, default: () => new Date() },
|
|
45
|
+
lastModified: { type: Date, default: () => new Date() },
|
|
46
|
+
}, { _id: false });
|
|
47
|
+
|
|
48
|
+
const workScheduleSchema = new Schema({
|
|
49
|
+
hoursPerWeek: { type: Number, min: 0, max: 168 },
|
|
50
|
+
hoursPerDay: { type: Number, min: 0, max: 24 }, // 🆕 Standard hours per day (e.g., 8 for full-time, 4 for part-time)
|
|
51
|
+
workingDays: [{ type: Number, min: 0, max: 6 }], // Array of days (0=Sunday, 6=Saturday)
|
|
52
|
+
shiftStart: { type: String }, // e.g., "09:00"
|
|
53
|
+
shiftEnd: { type: String }, // e.g., "17:00"
|
|
54
|
+
}, { _id: false });
|
|
55
|
+
|
|
56
|
+
const bankDetailsSchema = new Schema({
|
|
57
|
+
accountName: { type: String },
|
|
58
|
+
accountNumber: { type: String },
|
|
59
|
+
bankName: { type: String },
|
|
60
|
+
branchName: { type: String },
|
|
61
|
+
routingNumber: { type: String },
|
|
62
|
+
}, { _id: false });
|
|
63
|
+
|
|
64
|
+
const employmentHistorySchema = new Schema({
|
|
65
|
+
hireDate: { type: Date, required: true },
|
|
66
|
+
terminationDate: { type: Date, required: true },
|
|
67
|
+
reason: { type: String, enum: TERMINATION_REASON_VALUES },
|
|
68
|
+
finalSalary: { type: Number },
|
|
69
|
+
position: { type: String },
|
|
70
|
+
department: { type: String },
|
|
71
|
+
notes: { type: String },
|
|
72
|
+
}, { timestamps: true });
|
|
73
|
+
|
|
74
|
+
const payrollStatsSchema = new Schema({
|
|
75
|
+
totalPaid: { type: Number, default: 0, min: 0 },
|
|
76
|
+
lastPaymentDate: { type: Date },
|
|
77
|
+
nextPaymentDate: { type: Date },
|
|
78
|
+
paymentsThisYear: { type: Number, default: 0, min: 0 },
|
|
79
|
+
averageMonthly: { type: Number, default: 0, min: 0 },
|
|
80
|
+
updatedAt: { type: Date, default: () => new Date() },
|
|
81
|
+
}, { _id: false });
|
|
82
|
+
|
|
83
|
+
export const employmentFields = {
|
|
84
|
+
userId: { type: Schema.Types.ObjectId, ref: 'User', required: true },
|
|
85
|
+
organizationId: { type: Schema.Types.ObjectId, ref: 'Organization', required: true },
|
|
86
|
+
|
|
87
|
+
employeeId: { type: String, required: true },
|
|
88
|
+
|
|
89
|
+
employmentType: {
|
|
90
|
+
type: String,
|
|
91
|
+
enum: EMPLOYMENT_TYPE_VALUES,
|
|
92
|
+
default: 'full_time'
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
status: {
|
|
96
|
+
type: String,
|
|
97
|
+
enum: EMPLOYEE_STATUS_VALUES,
|
|
98
|
+
default: 'active'
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
department: { type: String, enum: DEPARTMENT_VALUES },
|
|
102
|
+
position: { type: String, required: true },
|
|
103
|
+
|
|
104
|
+
hireDate: { type: Date, required: true },
|
|
105
|
+
terminationDate: { type: Date },
|
|
106
|
+
probationEndDate: { type: Date },
|
|
107
|
+
|
|
108
|
+
employmentHistory: [employmentHistorySchema],
|
|
109
|
+
|
|
110
|
+
compensation: { type: compensationSchema, required: true },
|
|
111
|
+
workSchedule: workScheduleSchema,
|
|
112
|
+
bankDetails: bankDetailsSchema,
|
|
113
|
+
payrollStats: { type: payrollStatsSchema, default: () => ({}) },
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export {
|
|
117
|
+
allowanceSchema,
|
|
118
|
+
deductionSchema,
|
|
119
|
+
compensationSchema,
|
|
120
|
+
workScheduleSchema,
|
|
121
|
+
bankDetailsSchema,
|
|
122
|
+
employmentHistorySchema,
|
|
123
|
+
payrollStatsSchema,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export default employmentFields;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compensation Service - Beautiful Compensation Management
|
|
3
|
+
* Clean abstraction for salary adjustments and calculations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
CompensationFactory,
|
|
8
|
+
CompensationPresets,
|
|
9
|
+
} from '../factories/compensation.factory.js';
|
|
10
|
+
|
|
11
|
+
export class CompensationService {
|
|
12
|
+
constructor(EmployeeModel) {
|
|
13
|
+
this.EmployeeModel = EmployeeModel;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async getEmployeeCompensation(employeeId) {
|
|
17
|
+
const employee = await this.EmployeeModel.findById(employeeId);
|
|
18
|
+
if (!employee) throw new Error('Employee not found');
|
|
19
|
+
|
|
20
|
+
return employee.compensation;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async calculateBreakdown(employeeId) {
|
|
24
|
+
const compensation = await this.getEmployeeCompensation(employeeId);
|
|
25
|
+
return CompensationFactory.calculateBreakdown(compensation);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async updateBaseAmount(employeeId, newAmount, effectiveFrom) {
|
|
29
|
+
const employee = await this.EmployeeModel.findById(employeeId);
|
|
30
|
+
if (!employee) throw new Error('Employee not found');
|
|
31
|
+
|
|
32
|
+
const updatedCompensation = CompensationFactory.updateBaseAmount(
|
|
33
|
+
employee.compensation,
|
|
34
|
+
newAmount,
|
|
35
|
+
effectiveFrom
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
employee.compensation = updatedCompensation;
|
|
39
|
+
await employee.save();
|
|
40
|
+
|
|
41
|
+
return this.calculateBreakdown(employeeId);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async applyIncrement(employeeId, { percentage, amount, effectiveFrom }) {
|
|
45
|
+
const employee = await this.EmployeeModel.findById(employeeId);
|
|
46
|
+
if (!employee) throw new Error('Employee not found');
|
|
47
|
+
|
|
48
|
+
const updatedCompensation = CompensationFactory.applyIncrement(
|
|
49
|
+
employee.compensation,
|
|
50
|
+
{ percentage, amount, effectiveFrom }
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
employee.compensation = updatedCompensation;
|
|
54
|
+
employee.incrementHistory = employee.incrementHistory || [];
|
|
55
|
+
employee.incrementHistory.push({
|
|
56
|
+
previousAmount: employee.compensation.baseAmount,
|
|
57
|
+
newAmount: updatedCompensation.baseAmount,
|
|
58
|
+
percentage,
|
|
59
|
+
amount,
|
|
60
|
+
effectiveFrom: effectiveFrom || new Date(),
|
|
61
|
+
appliedAt: new Date(),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await employee.save();
|
|
65
|
+
|
|
66
|
+
return this.calculateBreakdown(employeeId);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async addAllowance(employeeId, allowance) {
|
|
70
|
+
const employee = await this.EmployeeModel.findById(employeeId);
|
|
71
|
+
if (!employee) throw new Error('Employee not found');
|
|
72
|
+
|
|
73
|
+
const updatedCompensation = CompensationFactory.addAllowance(
|
|
74
|
+
employee.compensation,
|
|
75
|
+
allowance
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
employee.compensation = updatedCompensation;
|
|
79
|
+
await employee.save();
|
|
80
|
+
|
|
81
|
+
return this.calculateBreakdown(employeeId);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async removeAllowance(employeeId, allowanceType) {
|
|
85
|
+
const employee = await this.EmployeeModel.findById(employeeId);
|
|
86
|
+
if (!employee) throw new Error('Employee not found');
|
|
87
|
+
|
|
88
|
+
const updatedCompensation = CompensationFactory.removeAllowance(
|
|
89
|
+
employee.compensation,
|
|
90
|
+
allowanceType
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
employee.compensation = updatedCompensation;
|
|
94
|
+
await employee.save();
|
|
95
|
+
|
|
96
|
+
return this.calculateBreakdown(employeeId);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async addDeduction(employeeId, deduction) {
|
|
100
|
+
const employee = await this.EmployeeModel.findById(employeeId);
|
|
101
|
+
if (!employee) throw new Error('Employee not found');
|
|
102
|
+
|
|
103
|
+
const updatedCompensation = CompensationFactory.addDeduction(
|
|
104
|
+
employee.compensation,
|
|
105
|
+
deduction
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
employee.compensation = updatedCompensation;
|
|
109
|
+
await employee.save();
|
|
110
|
+
|
|
111
|
+
return this.calculateBreakdown(employeeId);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async removeDeduction(employeeId, deductionType) {
|
|
115
|
+
const employee = await this.EmployeeModel.findById(employeeId);
|
|
116
|
+
if (!employee) throw new Error('Employee not found');
|
|
117
|
+
|
|
118
|
+
const updatedCompensation = CompensationFactory.removeDeduction(
|
|
119
|
+
employee.compensation,
|
|
120
|
+
deductionType
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
employee.compensation = updatedCompensation;
|
|
124
|
+
await employee.save();
|
|
125
|
+
|
|
126
|
+
return this.calculateBreakdown(employeeId);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async setStandardCompensation(employeeId, baseAmount) {
|
|
130
|
+
const employee = await this.EmployeeModel.findById(employeeId);
|
|
131
|
+
if (!employee) throw new Error('Employee not found');
|
|
132
|
+
|
|
133
|
+
employee.compensation = CompensationPresets.standard(baseAmount);
|
|
134
|
+
await employee.save();
|
|
135
|
+
|
|
136
|
+
return this.calculateBreakdown(employeeId);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async compareCompensation(employeeId1, employeeId2) {
|
|
140
|
+
const breakdown1 = await this.calculateBreakdown(employeeId1);
|
|
141
|
+
const breakdown2 = await this.calculateBreakdown(employeeId2);
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
employee1: breakdown1,
|
|
145
|
+
employee2: breakdown2,
|
|
146
|
+
difference: {
|
|
147
|
+
base: breakdown2.baseAmount - breakdown1.baseAmount,
|
|
148
|
+
gross: breakdown2.grossAmount - breakdown1.grossAmount,
|
|
149
|
+
net: breakdown2.netAmount - breakdown1.netAmount,
|
|
150
|
+
},
|
|
151
|
+
ratio: {
|
|
152
|
+
base: breakdown2.baseAmount / breakdown1.baseAmount,
|
|
153
|
+
gross: breakdown2.grossAmount / breakdown1.grossAmount,
|
|
154
|
+
net: breakdown2.netAmount / breakdown1.netAmount,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async getDepartmentCompensationStats(organizationId, department) {
|
|
160
|
+
const employees = await this.EmployeeModel.find({
|
|
161
|
+
organizationId,
|
|
162
|
+
department,
|
|
163
|
+
status: { $in: ['active', 'on_leave'] },
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const breakdowns = employees.map((emp) =>
|
|
167
|
+
CompensationFactory.calculateBreakdown(emp.compensation)
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const totals = breakdowns.reduce(
|
|
171
|
+
(acc, breakdown) => ({
|
|
172
|
+
totalBase: acc.totalBase + breakdown.baseAmount,
|
|
173
|
+
totalGross: acc.totalGross + breakdown.grossAmount,
|
|
174
|
+
totalNet: acc.totalNet + breakdown.netAmount,
|
|
175
|
+
}),
|
|
176
|
+
{ totalBase: 0, totalGross: 0, totalNet: 0 }
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
department,
|
|
181
|
+
employeeCount: employees.length,
|
|
182
|
+
...totals,
|
|
183
|
+
averageBase: totals.totalBase / employees.length,
|
|
184
|
+
averageGross: totals.totalGross / employees.length,
|
|
185
|
+
averageNet: totals.totalNet / employees.length,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async getOrganizationCompensationStats(organizationId) {
|
|
190
|
+
const employees = await this.EmployeeModel.find({
|
|
191
|
+
organizationId,
|
|
192
|
+
status: { $in: ['active', 'on_leave'] },
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const breakdowns = employees.map((emp) =>
|
|
196
|
+
CompensationFactory.calculateBreakdown(emp.compensation)
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const totals = breakdowns.reduce(
|
|
200
|
+
(acc, breakdown) => ({
|
|
201
|
+
totalBase: acc.totalBase + breakdown.baseAmount,
|
|
202
|
+
totalGross: acc.totalGross + breakdown.grossAmount,
|
|
203
|
+
totalNet: acc.totalNet + breakdown.netAmount,
|
|
204
|
+
}),
|
|
205
|
+
{ totalBase: 0, totalGross: 0, totalNet: 0 }
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const byDepartment = {};
|
|
209
|
+
employees.forEach((emp) => {
|
|
210
|
+
const dept = emp.department || 'unassigned';
|
|
211
|
+
if (!byDepartment[dept]) {
|
|
212
|
+
byDepartment[dept] = { count: 0, totalNet: 0 };
|
|
213
|
+
}
|
|
214
|
+
const breakdown = CompensationFactory.calculateBreakdown(emp.compensation);
|
|
215
|
+
byDepartment[dept].count++;
|
|
216
|
+
byDepartment[dept].totalNet += breakdown.netAmount;
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
employeeCount: employees.length,
|
|
221
|
+
...totals,
|
|
222
|
+
averageBase: totals.totalBase / employees.length,
|
|
223
|
+
averageGross: totals.totalGross / employees.length,
|
|
224
|
+
averageNet: totals.totalNet / employees.length,
|
|
225
|
+
byDepartment,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export const createCompensationService = (EmployeeModel) =>
|
|
231
|
+
new CompensationService(EmployeeModel);
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Employee Service - Clean Abstraction for Employee Operations
|
|
3
|
+
* Dependency injection, testable, beautiful API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { EmployeeFactory } from '../factories/employee.factory.js';
|
|
7
|
+
import { employee as employeeQuery } from '../utils/query-builders.js';
|
|
8
|
+
import {
|
|
9
|
+
isActive,
|
|
10
|
+
isEmployed,
|
|
11
|
+
canReceiveSalary,
|
|
12
|
+
} from '../utils/validation.utils.js';
|
|
13
|
+
|
|
14
|
+
export class EmployeeService {
|
|
15
|
+
constructor(EmployeeModel) {
|
|
16
|
+
this.EmployeeModel = EmployeeModel;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async findById(employeeId) {
|
|
20
|
+
return this.EmployeeModel.findById(employeeId);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async findByUserId(userId, organizationId) {
|
|
24
|
+
const query = employeeQuery()
|
|
25
|
+
.forUser(userId)
|
|
26
|
+
.forOrganization(organizationId)
|
|
27
|
+
.build();
|
|
28
|
+
|
|
29
|
+
return this.EmployeeModel.findOne(query);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async findActive(organizationId, options = {}) {
|
|
33
|
+
const query = employeeQuery()
|
|
34
|
+
.forOrganization(organizationId)
|
|
35
|
+
.active()
|
|
36
|
+
.build();
|
|
37
|
+
|
|
38
|
+
return this.EmployeeModel.find(query, options.projection);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async findEmployed(organizationId, options = {}) {
|
|
42
|
+
const query = employeeQuery()
|
|
43
|
+
.forOrganization(organizationId)
|
|
44
|
+
.employed()
|
|
45
|
+
.build();
|
|
46
|
+
|
|
47
|
+
return this.EmployeeModel.find(query, options.projection);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async findByDepartment(organizationId, department) {
|
|
51
|
+
const query = employeeQuery()
|
|
52
|
+
.forOrganization(organizationId)
|
|
53
|
+
.inDepartment(department)
|
|
54
|
+
.active()
|
|
55
|
+
.build();
|
|
56
|
+
|
|
57
|
+
return this.EmployeeModel.find(query);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async findEligibleForPayroll(organizationId, month, year) {
|
|
61
|
+
const query = employeeQuery()
|
|
62
|
+
.forOrganization(organizationId)
|
|
63
|
+
.employed()
|
|
64
|
+
.build();
|
|
65
|
+
|
|
66
|
+
const employees = await this.EmployeeModel.find(query);
|
|
67
|
+
return employees.filter(canReceiveSalary);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async create(data) {
|
|
71
|
+
const employeeData = EmployeeFactory.create(data);
|
|
72
|
+
return this.EmployeeModel.create(employeeData);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async updateStatus(employeeId, status, context = {}) {
|
|
76
|
+
const employee = await this.findById(employeeId);
|
|
77
|
+
if (!employee) throw new Error('Employee not found');
|
|
78
|
+
|
|
79
|
+
employee.status = status;
|
|
80
|
+
employee.statusHistory = employee.statusHistory || [];
|
|
81
|
+
employee.statusHistory.push({
|
|
82
|
+
status,
|
|
83
|
+
changedAt: new Date(),
|
|
84
|
+
changedBy: {
|
|
85
|
+
userId: context.userId,
|
|
86
|
+
name: context.userName,
|
|
87
|
+
},
|
|
88
|
+
reason: context.reason,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return employee.save();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async terminate(employeeId, reason, context = {}) {
|
|
95
|
+
const employee = await this.findById(employeeId);
|
|
96
|
+
if (!employee) throw new Error('Employee not found');
|
|
97
|
+
|
|
98
|
+
const terminationData = EmployeeFactory.createTermination({
|
|
99
|
+
reason,
|
|
100
|
+
notes: context.notes,
|
|
101
|
+
context,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
employee.status = 'terminated';
|
|
105
|
+
Object.assign(employee, terminationData);
|
|
106
|
+
|
|
107
|
+
return employee.save();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async updateCompensation(employeeId, compensation) {
|
|
111
|
+
return this.EmployeeModel.findByIdAndUpdate(
|
|
112
|
+
employeeId,
|
|
113
|
+
{ compensation, updatedAt: new Date() },
|
|
114
|
+
{ new: true, runValidators: true }
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async getEmployeeStats(organizationId) {
|
|
119
|
+
const query = employeeQuery().forOrganization(organizationId).build();
|
|
120
|
+
|
|
121
|
+
const employees = await this.EmployeeModel.find(query);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
total: employees.length,
|
|
125
|
+
active: employees.filter(isActive).length,
|
|
126
|
+
employed: employees.filter(isEmployed).length,
|
|
127
|
+
canReceiveSalary: employees.filter(canReceiveSalary).length,
|
|
128
|
+
byStatus: this.groupByStatus(employees),
|
|
129
|
+
byDepartment: this.groupByDepartment(employees),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
groupByStatus(employees) {
|
|
134
|
+
return employees.reduce((acc, emp) => {
|
|
135
|
+
acc[emp.status] = (acc[emp.status] || 0) + 1;
|
|
136
|
+
return acc;
|
|
137
|
+
}, {});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
groupByDepartment(employees) {
|
|
141
|
+
return employees.reduce((acc, emp) => {
|
|
142
|
+
const dept = emp.department || 'unassigned';
|
|
143
|
+
acc[dept] = (acc[dept] || 0) + 1;
|
|
144
|
+
return acc;
|
|
145
|
+
}, {});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
isActive(employee) {
|
|
149
|
+
return isActive(employee);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
isEmployed(employee) {
|
|
153
|
+
return isEmployed(employee);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
canReceiveSalary(employee) {
|
|
157
|
+
return canReceiveSalary(employee);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export const createEmployeeService = (EmployeeModel) =>
|
|
162
|
+
new EmployeeService(EmployeeModel);
|