@classytic/payroll 1.0.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @classytic/payroll might be problematic. Click here for more details.
- package/README.md +168 -489
- package/dist/core/index.d.ts +480 -0
- package/dist/core/index.js +971 -0
- package/dist/core/index.js.map +1 -0
- package/dist/index-CTjHlCzz.d.ts +721 -0
- package/dist/index.d.ts +967 -0
- package/dist/index.js +4352 -0
- package/dist/index.js.map +1 -0
- package/dist/payroll.d.ts +233 -0
- package/dist/payroll.js +2103 -0
- package/dist/payroll.js.map +1 -0
- package/dist/plugin-D9mOr3_d.d.ts +333 -0
- package/dist/schemas/index.d.ts +2869 -0
- package/dist/schemas/index.js +440 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.js +1696 -0
- package/dist/services/index.js.map +1 -0
- package/dist/types-BSYyX2KJ.d.ts +671 -0
- package/dist/utils/index.d.ts +873 -0
- package/dist/utils/index.js +1046 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +54 -37
- package/dist/types/config.d.ts +0 -162
- package/dist/types/core/compensation.manager.d.ts +0 -54
- package/dist/types/core/employment.manager.d.ts +0 -49
- package/dist/types/core/payroll.manager.d.ts +0 -60
- package/dist/types/enums.d.ts +0 -117
- package/dist/types/factories/compensation.factory.d.ts +0 -196
- package/dist/types/factories/employee.factory.d.ts +0 -149
- package/dist/types/factories/payroll.factory.d.ts +0 -319
- package/dist/types/hrm.orchestrator.d.ts +0 -47
- package/dist/types/index.d.ts +0 -20
- package/dist/types/init.d.ts +0 -30
- package/dist/types/models/payroll-record.model.d.ts +0 -3
- package/dist/types/plugins/employee.plugin.d.ts +0 -2
- package/dist/types/schemas/employment.schema.d.ts +0 -959
- package/dist/types/services/compensation.service.d.ts +0 -94
- package/dist/types/services/employee.service.d.ts +0 -28
- package/dist/types/services/payroll.service.d.ts +0 -30
- package/dist/types/utils/calculation.utils.d.ts +0 -26
- package/dist/types/utils/date.utils.d.ts +0 -35
- package/dist/types/utils/logger.d.ts +0 -12
- package/dist/types/utils/query-builders.d.ts +0 -83
- package/dist/types/utils/validation.utils.d.ts +0 -33
- package/payroll.d.ts +0 -241
- package/src/config.js +0 -177
- package/src/core/compensation.manager.js +0 -242
- package/src/core/employment.manager.js +0 -224
- package/src/core/payroll.manager.js +0 -499
- package/src/enums.js +0 -141
- package/src/factories/compensation.factory.js +0 -198
- package/src/factories/employee.factory.js +0 -173
- package/src/factories/payroll.factory.js +0 -413
- package/src/hrm.orchestrator.js +0 -139
- package/src/index.js +0 -172
- package/src/init.js +0 -62
- package/src/models/payroll-record.model.js +0 -126
- package/src/plugins/employee.plugin.js +0 -164
- package/src/schemas/employment.schema.js +0 -126
- package/src/services/compensation.service.js +0 -231
- package/src/services/employee.service.js +0 -162
- package/src/services/payroll.service.js +0 -213
- package/src/utils/calculation.utils.js +0 -91
- package/src/utils/date.utils.js +0 -120
- package/src/utils/logger.js +0 -36
- package/src/utils/query-builders.js +0 -185
- package/src/utils/validation.utils.js +0 -122
package/src/init.js
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { hrm } from './hrm.orchestrator.js';
|
|
2
|
-
import logger, { setLogger } from './utils/logger.js';
|
|
3
|
-
|
|
4
|
-
let initialized = false;
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Initialize HRM/Payroll framework
|
|
8
|
-
*
|
|
9
|
-
* @param {Object} options - Initialization options
|
|
10
|
-
* @param {Model} options.EmployeeModel - Employee model (required)
|
|
11
|
-
* @param {Model} options.PayrollRecordModel - Payroll record model (required)
|
|
12
|
-
* @param {Model} options.TransactionModel - Transaction model (required)
|
|
13
|
-
* @param {Model} options.AttendanceModel - Optional attendance model for integration
|
|
14
|
-
* @param {Object} options.logger - Optional custom logger
|
|
15
|
-
*
|
|
16
|
-
* @example Multi-Tenant (default)
|
|
17
|
-
* initializeHRM({
|
|
18
|
-
* EmployeeModel,
|
|
19
|
-
* PayrollRecordModel,
|
|
20
|
-
* TransactionModel
|
|
21
|
-
* });
|
|
22
|
-
*
|
|
23
|
-
* @example Single-Tenant
|
|
24
|
-
* // For single-tenant apps, add organizationId with default value in your Employee schema:
|
|
25
|
-
* // organizationId: { type: ObjectId, required: true, default: () => FIXED_ORG_ID }
|
|
26
|
-
*/
|
|
27
|
-
export function initializeHRM({ EmployeeModel, PayrollRecordModel, TransactionModel, AttendanceModel = null, logger: customLogger }) {
|
|
28
|
-
// Allow users to inject their own logger
|
|
29
|
-
if (customLogger) {
|
|
30
|
-
setLogger(customLogger);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (initialized) {
|
|
34
|
-
logger.warn('HRM already initialized, skipping');
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (!EmployeeModel || !PayrollRecordModel || !TransactionModel) {
|
|
39
|
-
throw new Error(
|
|
40
|
-
'HRM initialization requires EmployeeModel, PayrollRecordModel, and TransactionModel'
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
hrm.configure({
|
|
45
|
-
EmployeeModel,
|
|
46
|
-
PayrollRecordModel,
|
|
47
|
-
TransactionModel,
|
|
48
|
-
AttendanceModel,
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
initialized = true;
|
|
52
|
-
|
|
53
|
-
logger.info('HRM library initialized', {
|
|
54
|
-
hasAttendanceIntegration: !!AttendanceModel,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function isInitialized() {
|
|
59
|
-
return initialized;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export default initializeHRM;
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import mongoose from 'mongoose';
|
|
2
|
-
import mongoosePaginate from 'mongoose-paginate-v2';
|
|
3
|
-
import aggregatePaginate from 'mongoose-aggregate-paginate-v2';
|
|
4
|
-
import { PAYROLL_STATUS_VALUES } from '../enums.js';
|
|
5
|
-
import { HRM_CONFIG } from '../config.js';
|
|
6
|
-
|
|
7
|
-
const { Schema } = mongoose;
|
|
8
|
-
|
|
9
|
-
const payrollBreakdownSchema = new Schema({
|
|
10
|
-
baseAmount: { type: Number, required: true, min: 0 },
|
|
11
|
-
|
|
12
|
-
allowances: [{
|
|
13
|
-
type: String,
|
|
14
|
-
amount: { type: Number, min: 0 },
|
|
15
|
-
taxable: { type: Boolean, default: true },
|
|
16
|
-
}],
|
|
17
|
-
|
|
18
|
-
deductions: [{
|
|
19
|
-
type: String,
|
|
20
|
-
amount: { type: Number, min: 0 },
|
|
21
|
-
description: String,
|
|
22
|
-
}],
|
|
23
|
-
|
|
24
|
-
grossSalary: { type: Number, required: true, min: 0 },
|
|
25
|
-
netSalary: { type: Number, required: true, min: 0 },
|
|
26
|
-
|
|
27
|
-
workingDays: { type: Number, min: 0 },
|
|
28
|
-
actualDays: { type: Number, min: 0 },
|
|
29
|
-
proRatedAmount: { type: Number, default: 0, min: 0 },
|
|
30
|
-
attendanceDeduction: { type: Number, default: 0, min: 0 },
|
|
31
|
-
overtimeAmount: { type: Number, default: 0, min: 0 },
|
|
32
|
-
bonusAmount: { type: Number, default: 0, min: 0 },
|
|
33
|
-
}, { _id: false });
|
|
34
|
-
|
|
35
|
-
const periodSchema = new Schema({
|
|
36
|
-
month: { type: Number, required: true, min: 1, max: 12 },
|
|
37
|
-
year: { type: Number, required: true, min: 2020 },
|
|
38
|
-
startDate: { type: Date, required: true },
|
|
39
|
-
endDate: { type: Date, required: true },
|
|
40
|
-
payDate: { type: Date, required: true },
|
|
41
|
-
}, { _id: false });
|
|
42
|
-
|
|
43
|
-
const payrollRecordSchema = new Schema({
|
|
44
|
-
organizationId: { type: Schema.Types.ObjectId, ref: 'Organization', required: true, index: true },
|
|
45
|
-
employeeId: { type: Schema.Types.ObjectId, required: true, index: true },
|
|
46
|
-
userId: { type: Schema.Types.ObjectId, ref: 'User', index: true },
|
|
47
|
-
|
|
48
|
-
period: { type: periodSchema, required: true },
|
|
49
|
-
|
|
50
|
-
breakdown: { type: payrollBreakdownSchema, required: true },
|
|
51
|
-
|
|
52
|
-
transactionId: { type: Schema.Types.ObjectId, ref: 'Transaction' },
|
|
53
|
-
|
|
54
|
-
status: {
|
|
55
|
-
type: String,
|
|
56
|
-
enum: PAYROLL_STATUS_VALUES,
|
|
57
|
-
default: 'pending',
|
|
58
|
-
index: true
|
|
59
|
-
},
|
|
60
|
-
|
|
61
|
-
paidAt: { type: Date },
|
|
62
|
-
paymentMethod: { type: String },
|
|
63
|
-
|
|
64
|
-
processedBy: { type: Schema.Types.ObjectId, ref: 'User' },
|
|
65
|
-
notes: { type: String },
|
|
66
|
-
payslipUrl: { type: String },
|
|
67
|
-
|
|
68
|
-
exported: { type: Boolean, default: false },
|
|
69
|
-
exportedAt: { type: Date },
|
|
70
|
-
}, {
|
|
71
|
-
timestamps: true,
|
|
72
|
-
roleBasedSelect: {
|
|
73
|
-
user: '-notes -processedBy',
|
|
74
|
-
admin: '',
|
|
75
|
-
superadmin: '',
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
payrollRecordSchema.index({ organizationId: 1, employeeId: 1, 'period.month': 1, 'period.year': 1 }, { unique: true });
|
|
80
|
-
payrollRecordSchema.index({ organizationId: 1, 'period.year': 1, 'period.month': 1 });
|
|
81
|
-
payrollRecordSchema.index({ employeeId: 1, 'period.year': -1, 'period.month': -1 });
|
|
82
|
-
payrollRecordSchema.index({ status: 1, createdAt: -1 });
|
|
83
|
-
payrollRecordSchema.index({ organizationId: 1, status: 1, 'period.payDate': 1 });
|
|
84
|
-
|
|
85
|
-
payrollRecordSchema.index(
|
|
86
|
-
{ createdAt: 1 },
|
|
87
|
-
{
|
|
88
|
-
expireAfterSeconds: HRM_CONFIG.dataRetention.payrollRecordsTTL,
|
|
89
|
-
partialFilterExpression: { exported: true }
|
|
90
|
-
}
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
payrollRecordSchema.virtual('totalAmount').get(function() {
|
|
94
|
-
return this.breakdown?.netSalary || 0;
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
payrollRecordSchema.virtual('isPaid').get(function() {
|
|
98
|
-
return this.status === 'paid';
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
payrollRecordSchema.virtual('periodLabel').get(function() {
|
|
102
|
-
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
103
|
-
return `${months[this.period.month - 1]} ${this.period.year}`;
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
payrollRecordSchema.methods.markAsPaid = function(transactionId, paidAt = new Date()) {
|
|
107
|
-
this.status = 'paid';
|
|
108
|
-
this.transactionId = transactionId;
|
|
109
|
-
this.paidAt = paidAt;
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
payrollRecordSchema.methods.markAsExported = function() {
|
|
113
|
-
this.exported = true;
|
|
114
|
-
this.exportedAt = new Date();
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
payrollRecordSchema.methods.canBeDeleted = function() {
|
|
118
|
-
return this.exported && this.status === 'paid';
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
payrollRecordSchema.plugin(mongoosePaginate);
|
|
122
|
-
payrollRecordSchema.plugin(aggregatePaginate);
|
|
123
|
-
|
|
124
|
-
const PayrollRecord = mongoose.models.PayrollRecord || mongoose.model('PayrollRecord', payrollRecordSchema);
|
|
125
|
-
|
|
126
|
-
export default PayrollRecord;
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import logger from '../utils/logger.js';
|
|
2
|
-
import { diffInDays } from '../utils/date.utils.js';
|
|
3
|
-
import { sumDeductions } from '../utils/calculation.utils.js';
|
|
4
|
-
import {
|
|
5
|
-
isActive,
|
|
6
|
-
isTerminated,
|
|
7
|
-
canReceiveSalary as canReceiveSalaryUtil,
|
|
8
|
-
} from '../utils/validation.utils.js';
|
|
9
|
-
import { CompensationFactory } from '../factories/compensation.factory.js';
|
|
10
|
-
|
|
11
|
-
export function employeePlugin(schema, options = {}) {
|
|
12
|
-
|
|
13
|
-
schema.virtual('currentSalary').get(function() {
|
|
14
|
-
return this.compensation?.netSalary || 0;
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
schema.virtual('isActive').get(function() {
|
|
18
|
-
return isActive(this);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
schema.virtual('isTerminated').get(function() {
|
|
22
|
-
return isTerminated(this);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
schema.virtual('yearsOfService').get(function() {
|
|
26
|
-
const end = this.terminationDate || new Date();
|
|
27
|
-
const days = diffInDays(this.hireDate, end);
|
|
28
|
-
return Math.max(0, Math.floor((days / 365.25) * 10) / 10);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
schema.virtual('isOnProbation').get(function() {
|
|
32
|
-
if (!this.probationEndDate) return false;
|
|
33
|
-
return new Date() < this.probationEndDate;
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
schema.methods.calculateSalary = function() {
|
|
37
|
-
if (!this.compensation) {
|
|
38
|
-
return { gross: 0, deductions: 0, net: 0 };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const breakdown = CompensationFactory.calculateBreakdown(this.compensation);
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
gross: breakdown.grossAmount,
|
|
45
|
-
deductions: sumDeductions(breakdown.deductions),
|
|
46
|
-
net: breakdown.netAmount
|
|
47
|
-
};
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
schema.methods.updateSalaryCalculations = function() {
|
|
51
|
-
const calculated = this.calculateSalary();
|
|
52
|
-
this.compensation.grossSalary = calculated.gross;
|
|
53
|
-
this.compensation.netSalary = calculated.net;
|
|
54
|
-
this.compensation.lastModified = new Date();
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
schema.methods.canReceiveSalary = function() {
|
|
58
|
-
return (
|
|
59
|
-
this.status === 'active' &&
|
|
60
|
-
this.compensation?.baseAmount > 0 &&
|
|
61
|
-
(!options.requireBankDetails || this.bankDetails?.accountNumber)
|
|
62
|
-
);
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
schema.methods.addAllowance = function(type, amount, taxable = true) {
|
|
66
|
-
if (!this.compensation.allowances) {
|
|
67
|
-
this.compensation.allowances = [];
|
|
68
|
-
}
|
|
69
|
-
this.compensation.allowances.push({ type, amount, taxable });
|
|
70
|
-
this.updateSalaryCalculations();
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
schema.methods.addDeduction = function(type, amount, auto = false, description = '') {
|
|
74
|
-
if (!this.compensation.deductions) {
|
|
75
|
-
this.compensation.deductions = [];
|
|
76
|
-
}
|
|
77
|
-
this.compensation.deductions.push({ type, amount, auto, description });
|
|
78
|
-
this.updateSalaryCalculations();
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
schema.methods.removeAllowance = function(type) {
|
|
82
|
-
if (!this.compensation.allowances) return;
|
|
83
|
-
this.compensation.allowances = this.compensation.allowances.filter(a => a.type !== type);
|
|
84
|
-
this.updateSalaryCalculations();
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
schema.methods.removeDeduction = function(type) {
|
|
88
|
-
if (!this.compensation.deductions) return;
|
|
89
|
-
this.compensation.deductions = this.compensation.deductions.filter(d => d.type !== type);
|
|
90
|
-
this.updateSalaryCalculations();
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
schema.methods.terminate = function(reason, terminationDate = new Date()) {
|
|
94
|
-
if (this.status === 'terminated') {
|
|
95
|
-
throw new Error('Employee already terminated');
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (!this.employmentHistory) {
|
|
99
|
-
this.employmentHistory = [];
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
this.employmentHistory.push({
|
|
103
|
-
hireDate: this.hireDate,
|
|
104
|
-
terminationDate,
|
|
105
|
-
reason,
|
|
106
|
-
finalSalary: this.compensation?.netSalary || 0,
|
|
107
|
-
position: this.position,
|
|
108
|
-
department: this.department,
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
this.status = 'terminated';
|
|
112
|
-
this.terminationDate = terminationDate;
|
|
113
|
-
|
|
114
|
-
logger.info('Employee terminated', {
|
|
115
|
-
employeeId: this.employeeId,
|
|
116
|
-
organizationId: this.organizationId,
|
|
117
|
-
reason,
|
|
118
|
-
});
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
schema.methods.reHire = function(hireDate = new Date(), position = null, department = null) {
|
|
122
|
-
if (this.status !== 'terminated') {
|
|
123
|
-
throw new Error('Can only re-hire terminated employees');
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
this.status = 'active';
|
|
127
|
-
this.hireDate = hireDate;
|
|
128
|
-
this.terminationDate = null;
|
|
129
|
-
|
|
130
|
-
if (position) this.position = position;
|
|
131
|
-
if (department) this.department = department;
|
|
132
|
-
|
|
133
|
-
logger.info('Employee re-hired', {
|
|
134
|
-
employeeId: this.employeeId,
|
|
135
|
-
organizationId: this.organizationId,
|
|
136
|
-
});
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Pre-save hook to automatically update salary calculations
|
|
141
|
-
* when compensation is modified.
|
|
142
|
-
*
|
|
143
|
-
* Mongoose v9 compatible - uses async function without next callback
|
|
144
|
-
* - Use throw instead of next(err) for errors
|
|
145
|
-
* - Use return instead of return next()
|
|
146
|
-
*/
|
|
147
|
-
schema.pre('save', async function() {
|
|
148
|
-
if (this.isModified('compensation')) {
|
|
149
|
-
this.updateSalaryCalculations();
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
schema.index({ organizationId: 1, employeeId: 1 }, { unique: true });
|
|
154
|
-
schema.index({ userId: 1, organizationId: 1 }, { unique: true });
|
|
155
|
-
schema.index({ organizationId: 1, status: 1 });
|
|
156
|
-
schema.index({ organizationId: 1, department: 1 });
|
|
157
|
-
schema.index({ organizationId: 1, 'compensation.netSalary': -1 });
|
|
158
|
-
|
|
159
|
-
logger.debug('Employee plugin applied', {
|
|
160
|
-
requireBankDetails: options.requireBankDetails || false,
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export default employeePlugin;
|
|
@@ -1,126 +0,0 @@
|
|
|
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;
|
|
@@ -1,231 +0,0 @@
|
|
|
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);
|