@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.
@@ -0,0 +1,139 @@
1
+ import * as EmploymentManager from './core/employment.manager.js';
2
+ import * as CompensationManager from './core/compensation.manager.js';
3
+ import * as PayrollManager from './core/payroll.manager.js';
4
+ import logger from './utils/logger.js';
5
+
6
+ class HRMOrchestrator {
7
+ constructor() {
8
+ this._models = null;
9
+ this._initialized = false;
10
+ }
11
+
12
+ configure({ EmployeeModel, PayrollRecordModel, TransactionModel, AttendanceModel = null }) {
13
+ if (!EmployeeModel || !PayrollRecordModel || !TransactionModel) {
14
+ throw new Error('EmployeeModel, PayrollRecordModel, and TransactionModel are required');
15
+ }
16
+
17
+ this._models = { EmployeeModel, PayrollRecordModel, TransactionModel, AttendanceModel };
18
+ this._initialized = true;
19
+
20
+ logger.info('HRM Orchestrator configured', {
21
+ hasEmployeeModel: !!EmployeeModel,
22
+ hasPayrollRecordModel: !!PayrollRecordModel,
23
+ hasTransactionModel: !!TransactionModel,
24
+ hasAttendanceModel: !!AttendanceModel,
25
+ });
26
+ }
27
+
28
+ _ensureInitialized() {
29
+ if (!this._initialized) {
30
+ throw new Error(
31
+ 'HRM Orchestrator not initialized. ' +
32
+ 'Call initializeHRM({ EmployeeModel, PayrollRecordModel, TransactionModel }) in bootstrap.'
33
+ );
34
+ }
35
+ }
36
+
37
+ isInitialized() {
38
+ return this._initialized;
39
+ }
40
+
41
+ async hire(params) {
42
+ this._ensureInitialized();
43
+ return await EmploymentManager.hireEmployee({ ...this._models, ...params });
44
+ }
45
+
46
+ async updateEmployment(params) {
47
+ this._ensureInitialized();
48
+ return await EmploymentManager.updateEmployment({ ...this._models, ...params });
49
+ }
50
+
51
+ async terminate(params) {
52
+ this._ensureInitialized();
53
+ return await EmploymentManager.terminateEmployee({ ...this._models, ...params });
54
+ }
55
+
56
+ async reHire(params) {
57
+ this._ensureInitialized();
58
+ return await EmploymentManager.reHireEmployee({ ...this._models, ...params });
59
+ }
60
+
61
+ async listEmployees(params) {
62
+ this._ensureInitialized();
63
+ return await EmploymentManager.getEmployeeList({ ...this._models, ...params });
64
+ }
65
+
66
+ async getEmployee(params) {
67
+ this._ensureInitialized();
68
+ return await EmploymentManager.getEmployeeById({ ...this._models, ...params });
69
+ }
70
+
71
+ async updateSalary(params) {
72
+ this._ensureInitialized();
73
+ return await CompensationManager.updateSalary({ ...this._models, ...params });
74
+ }
75
+
76
+ async addAllowance(params) {
77
+ this._ensureInitialized();
78
+ return await CompensationManager.addAllowance({ ...this._models, ...params });
79
+ }
80
+
81
+ async removeAllowance(params) {
82
+ this._ensureInitialized();
83
+ return await CompensationManager.removeAllowance({ ...this._models, ...params });
84
+ }
85
+
86
+ async addDeduction(params) {
87
+ this._ensureInitialized();
88
+ return await CompensationManager.addDeduction({ ...this._models, ...params });
89
+ }
90
+
91
+ async removeDeduction(params) {
92
+ this._ensureInitialized();
93
+ return await CompensationManager.removeDeduction({ ...this._models, ...params });
94
+ }
95
+
96
+ async updateBankDetails(params) {
97
+ this._ensureInitialized();
98
+ return await CompensationManager.updateBankDetails({ ...this._models, ...params });
99
+ }
100
+
101
+ async processSalary(params) {
102
+ this._ensureInitialized();
103
+ return await PayrollManager.processSalary({ ...this._models, ...params });
104
+ }
105
+
106
+ async processBulkPayroll(params) {
107
+ this._ensureInitialized();
108
+ return await PayrollManager.processBulkPayroll({ ...this._models, ...params });
109
+ }
110
+
111
+ async payrollHistory(params) {
112
+ this._ensureInitialized();
113
+ return await PayrollManager.getPayrollHistory({ ...this._models, ...params });
114
+ }
115
+
116
+ async payrollSummary(params) {
117
+ this._ensureInitialized();
118
+ return await PayrollManager.getPayrollSummary({ ...this._models, ...params });
119
+ }
120
+
121
+ async exportPayroll(params) {
122
+ this._ensureInitialized();
123
+ return await PayrollManager.exportPayrollData({ ...this._models, ...params });
124
+ }
125
+
126
+ getEmployeeModel() {
127
+ this._ensureInitialized();
128
+ return this._models.EmployeeModel;
129
+ }
130
+
131
+ getPayrollRecordModel() {
132
+ this._ensureInitialized();
133
+ return this._models.PayrollRecordModel;
134
+ }
135
+ }
136
+
137
+ export const hrmOrchestrator = new HRMOrchestrator();
138
+ export const hrm = hrmOrchestrator;
139
+ export default hrm;
package/src/index.js ADDED
@@ -0,0 +1,172 @@
1
+ export { initializeHRM, isInitialized } from './init.js';
2
+
3
+ // Logger configuration (for custom logger injection)
4
+ export { setLogger } from './utils/logger.js';
5
+
6
+ export {
7
+ EMPLOYMENT_TYPE,
8
+ EMPLOYMENT_TYPE_VALUES,
9
+ EMPLOYEE_STATUS,
10
+ EMPLOYEE_STATUS_VALUES,
11
+ DEPARTMENT,
12
+ DEPARTMENT_VALUES,
13
+ PAYMENT_FREQUENCY,
14
+ PAYMENT_FREQUENCY_VALUES,
15
+ PAYMENT_METHOD,
16
+ PAYMENT_METHOD_VALUES,
17
+ ALLOWANCE_TYPE,
18
+ ALLOWANCE_TYPE_VALUES,
19
+ DEDUCTION_TYPE,
20
+ DEDUCTION_TYPE_VALUES,
21
+ PAYROLL_STATUS,
22
+ PAYROLL_STATUS_VALUES,
23
+ TERMINATION_REASON,
24
+ TERMINATION_REASON_VALUES,
25
+ HRM_TRANSACTION_CATEGORIES,
26
+ HRM_CATEGORY_VALUES,
27
+ isHRMManagedCategory,
28
+ } from './enums.js';
29
+
30
+ export {
31
+ HRM_CONFIG,
32
+ SALARY_BANDS,
33
+ TAX_BRACKETS,
34
+ ORG_ROLES,
35
+ ORG_ROLE_KEYS,
36
+ ROLE_MAPPING,
37
+ calculateTax,
38
+ getSalaryBand,
39
+ determineOrgRole,
40
+ } from './config.js';
41
+
42
+ export {
43
+ employmentFields,
44
+ allowanceSchema,
45
+ deductionSchema,
46
+ compensationSchema,
47
+ workScheduleSchema,
48
+ bankDetailsSchema,
49
+ employmentHistorySchema,
50
+ payrollStatsSchema,
51
+ } from './schemas/employment.schema.js';
52
+
53
+ export { employeePlugin } from './plugins/employee.plugin.js';
54
+
55
+ export { default as PayrollRecord } from './models/payroll-record.model.js';
56
+
57
+ export { hrm, hrmOrchestrator } from './hrm.orchestrator.js';
58
+
59
+ import { hrm as hrmDefault } from './hrm.orchestrator.js';
60
+ export default hrmDefault;
61
+
62
+ // ============================================
63
+ // Pure Utilities - Testable, Reusable Functions
64
+ // ============================================
65
+
66
+ export {
67
+ addDays,
68
+ addMonths,
69
+ diffInDays,
70
+ diffInMonths,
71
+ startOfMonth,
72
+ endOfMonth,
73
+ startOfYear,
74
+ endOfYear,
75
+ isWeekday,
76
+ isWeekend,
77
+ getPayPeriod,
78
+ getCurrentPeriod,
79
+ calculateProbationEnd,
80
+ formatDateForDB,
81
+ parseDBDate,
82
+ } from './utils/date.utils.js';
83
+
84
+ export {
85
+ sum,
86
+ sumBy,
87
+ sumAllowances,
88
+ sumDeductions,
89
+ calculateGross,
90
+ calculateNet,
91
+ applyPercentage,
92
+ calculatePercentage,
93
+ createAllowanceCalculator,
94
+ createDeductionCalculator,
95
+ calculateTotalCompensation,
96
+ pipe,
97
+ compose,
98
+ } from './utils/calculation.utils.js';
99
+
100
+ export {
101
+ isActive,
102
+ isOnLeave,
103
+ isSuspended,
104
+ isTerminated,
105
+ isEmployed,
106
+ canReceiveSalary,
107
+ hasCompensation,
108
+ required,
109
+ minValue,
110
+ maxValue,
111
+ isInRange,
112
+ isPositive,
113
+ isValidStatus,
114
+ isValidEmploymentType,
115
+ compose as composeValidators,
116
+ } from './utils/validation.utils.js';
117
+
118
+ // ============================================
119
+ // Query Builders - Fluent API for MongoDB
120
+ // ============================================
121
+
122
+ export {
123
+ QueryBuilder,
124
+ EmployeeQueryBuilder,
125
+ PayrollQueryBuilder,
126
+ employee,
127
+ payroll,
128
+ toObjectId,
129
+ } from './utils/query-builders.js';
130
+
131
+ // ============================================
132
+ // Factory Methods - Clean Object Creation
133
+ // ============================================
134
+
135
+ export {
136
+ EmployeeFactory,
137
+ EmployeeBuilder,
138
+ createEmployee,
139
+ } from './factories/employee.factory.js';
140
+
141
+ export {
142
+ PayrollFactory,
143
+ PayrollBuilder,
144
+ BatchPayrollFactory,
145
+ createPayroll,
146
+ } from './factories/payroll.factory.js';
147
+
148
+ export {
149
+ CompensationFactory,
150
+ CompensationBuilder,
151
+ CompensationPresets,
152
+ createCompensation,
153
+ } from './factories/compensation.factory.js';
154
+
155
+ // ============================================
156
+ // Service Layer - Clean Abstractions with DI
157
+ // ============================================
158
+
159
+ export {
160
+ EmployeeService,
161
+ createEmployeeService,
162
+ } from './services/employee.service.js';
163
+
164
+ export {
165
+ PayrollService,
166
+ createPayrollService,
167
+ } from './services/payroll.service.js';
168
+
169
+ export {
170
+ CompensationService,
171
+ createCompensationService,
172
+ } from './services/compensation.service.js';
package/src/init.js ADDED
@@ -0,0 +1,41 @@
1
+ import { hrm } from './hrm.orchestrator.js';
2
+ import logger, { setLogger } from './utils/logger.js';
3
+
4
+ let initialized = false;
5
+
6
+ export function initializeHRM({ EmployeeModel, PayrollRecordModel, TransactionModel, AttendanceModel = null, logger: customLogger }) {
7
+ // Allow users to inject their own logger
8
+ if (customLogger) {
9
+ setLogger(customLogger);
10
+ }
11
+
12
+ if (initialized) {
13
+ logger.warn('HRM already initialized, skipping');
14
+ return;
15
+ }
16
+
17
+ if (!EmployeeModel || !PayrollRecordModel || !TransactionModel) {
18
+ throw new Error(
19
+ 'HRM initialization requires EmployeeModel, PayrollRecordModel, and TransactionModel'
20
+ );
21
+ }
22
+
23
+ hrm.configure({
24
+ EmployeeModel,
25
+ PayrollRecordModel,
26
+ TransactionModel,
27
+ AttendanceModel,
28
+ });
29
+
30
+ initialized = true;
31
+
32
+ logger.info('HRM library initialized', {
33
+ hasAttendanceIntegration: !!AttendanceModel,
34
+ });
35
+ }
36
+
37
+ export function isInitialized() {
38
+ return initialized;
39
+ }
40
+
41
+ export default initializeHRM;
@@ -0,0 +1,126 @@
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;
@@ -0,0 +1,157 @@
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
+ schema.pre('save', function(next) {
140
+ if (this.isModified('compensation')) {
141
+ this.updateSalaryCalculations();
142
+ }
143
+ next();
144
+ });
145
+
146
+ schema.index({ organizationId: 1, employeeId: 1 }, { unique: true });
147
+ schema.index({ userId: 1, organizationId: 1 }, { unique: true });
148
+ schema.index({ organizationId: 1, status: 1 });
149
+ schema.index({ organizationId: 1, department: 1 });
150
+ schema.index({ organizationId: 1, 'compensation.netSalary': -1 });
151
+
152
+ logger.debug('Employee plugin applied', {
153
+ requireBankDetails: options.requireBankDetails || false,
154
+ });
155
+ }
156
+
157
+ export default employeePlugin;