@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/dist/index.js
ADDED
|
@@ -0,0 +1,4352 @@
|
|
|
1
|
+
import mongoose2, { Schema, Types } from 'mongoose';
|
|
2
|
+
|
|
3
|
+
// src/payroll.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/logger.ts
|
|
6
|
+
var createConsoleLogger = () => ({
|
|
7
|
+
info: (message, meta) => {
|
|
8
|
+
if (meta) {
|
|
9
|
+
console.log(`[Payroll] INFO: ${message}`, meta);
|
|
10
|
+
} else {
|
|
11
|
+
console.log(`[Payroll] INFO: ${message}`);
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
error: (message, meta) => {
|
|
15
|
+
if (meta) {
|
|
16
|
+
console.error(`[Payroll] ERROR: ${message}`, meta);
|
|
17
|
+
} else {
|
|
18
|
+
console.error(`[Payroll] ERROR: ${message}`);
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
warn: (message, meta) => {
|
|
22
|
+
if (meta) {
|
|
23
|
+
console.warn(`[Payroll] WARN: ${message}`, meta);
|
|
24
|
+
} else {
|
|
25
|
+
console.warn(`[Payroll] WARN: ${message}`);
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
debug: (message, meta) => {
|
|
29
|
+
if (process.env.NODE_ENV !== "production") {
|
|
30
|
+
if (meta) {
|
|
31
|
+
console.log(`[Payroll] DEBUG: ${message}`, meta);
|
|
32
|
+
} else {
|
|
33
|
+
console.log(`[Payroll] DEBUG: ${message}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
var currentLogger = createConsoleLogger();
|
|
39
|
+
var loggingEnabled = true;
|
|
40
|
+
function getLogger() {
|
|
41
|
+
return currentLogger;
|
|
42
|
+
}
|
|
43
|
+
function setLogger(logger2) {
|
|
44
|
+
currentLogger = logger2;
|
|
45
|
+
}
|
|
46
|
+
function enableLogging() {
|
|
47
|
+
loggingEnabled = true;
|
|
48
|
+
}
|
|
49
|
+
function disableLogging() {
|
|
50
|
+
loggingEnabled = false;
|
|
51
|
+
}
|
|
52
|
+
function isLoggingEnabled() {
|
|
53
|
+
return loggingEnabled;
|
|
54
|
+
}
|
|
55
|
+
var logger = {
|
|
56
|
+
info: (message, meta) => {
|
|
57
|
+
if (loggingEnabled) currentLogger.info(message, meta);
|
|
58
|
+
},
|
|
59
|
+
error: (message, meta) => {
|
|
60
|
+
if (loggingEnabled) currentLogger.error(message, meta);
|
|
61
|
+
},
|
|
62
|
+
warn: (message, meta) => {
|
|
63
|
+
if (loggingEnabled) currentLogger.warn(message, meta);
|
|
64
|
+
},
|
|
65
|
+
debug: (message, meta) => {
|
|
66
|
+
if (loggingEnabled) currentLogger.debug(message, meta);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// src/config.ts
|
|
71
|
+
var HRM_CONFIG = {
|
|
72
|
+
dataRetention: {
|
|
73
|
+
payrollRecordsTTL: 63072e3,
|
|
74
|
+
// 2 years in seconds
|
|
75
|
+
exportWarningDays: 30,
|
|
76
|
+
archiveBeforeDeletion: true
|
|
77
|
+
},
|
|
78
|
+
payroll: {
|
|
79
|
+
defaultCurrency: "BDT",
|
|
80
|
+
allowProRating: true,
|
|
81
|
+
attendanceIntegration: true,
|
|
82
|
+
autoDeductions: true,
|
|
83
|
+
overtimeEnabled: false,
|
|
84
|
+
overtimeMultiplier: 1.5
|
|
85
|
+
},
|
|
86
|
+
salary: {
|
|
87
|
+
minimumWage: 0,
|
|
88
|
+
maximumAllowances: 10,
|
|
89
|
+
maximumDeductions: 10,
|
|
90
|
+
defaultFrequency: "monthly"
|
|
91
|
+
},
|
|
92
|
+
employment: {
|
|
93
|
+
defaultProbationMonths: 3,
|
|
94
|
+
maxProbationMonths: 6,
|
|
95
|
+
allowReHiring: true,
|
|
96
|
+
trackEmploymentHistory: true
|
|
97
|
+
},
|
|
98
|
+
validation: {
|
|
99
|
+
requireBankDetails: false,
|
|
100
|
+
requireEmployeeId: true,
|
|
101
|
+
uniqueEmployeeIdPerOrg: true,
|
|
102
|
+
allowMultiTenantEmployees: true
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
var TAX_BRACKETS = {
|
|
106
|
+
BDT: [
|
|
107
|
+
{ min: 0, max: 3e5, rate: 0 },
|
|
108
|
+
{ min: 3e5, max: 4e5, rate: 0.05 },
|
|
109
|
+
{ min: 4e5, max: 5e5, rate: 0.1 },
|
|
110
|
+
{ min: 5e5, max: 6e5, rate: 0.15 },
|
|
111
|
+
{ min: 6e5, max: 3e6, rate: 0.2 },
|
|
112
|
+
{ min: 3e6, max: Infinity, rate: 0.25 }
|
|
113
|
+
],
|
|
114
|
+
USD: [
|
|
115
|
+
{ min: 0, max: 1e4, rate: 0.1 },
|
|
116
|
+
{ min: 1e4, max: 4e4, rate: 0.12 },
|
|
117
|
+
{ min: 4e4, max: 85e3, rate: 0.22 },
|
|
118
|
+
{ min: 85e3, max: 165e3, rate: 0.24 },
|
|
119
|
+
{ min: 165e3, max: 215e3, rate: 0.32 },
|
|
120
|
+
{ min: 215e3, max: 54e4, rate: 0.35 },
|
|
121
|
+
{ min: 54e4, max: Infinity, rate: 0.37 }
|
|
122
|
+
]
|
|
123
|
+
};
|
|
124
|
+
var ORG_ROLES = {
|
|
125
|
+
OWNER: {
|
|
126
|
+
key: "owner",
|
|
127
|
+
label: "Owner",
|
|
128
|
+
description: "Full organization access (set by Organization model)"
|
|
129
|
+
},
|
|
130
|
+
MANAGER: {
|
|
131
|
+
key: "manager",
|
|
132
|
+
label: "Manager",
|
|
133
|
+
description: "Management and administrative features"
|
|
134
|
+
},
|
|
135
|
+
TRAINER: {
|
|
136
|
+
key: "trainer",
|
|
137
|
+
label: "Trainer",
|
|
138
|
+
description: "Training and coaching features"
|
|
139
|
+
},
|
|
140
|
+
STAFF: {
|
|
141
|
+
key: "staff",
|
|
142
|
+
label: "Staff",
|
|
143
|
+
description: "General staff access to basic features"
|
|
144
|
+
},
|
|
145
|
+
INTERN: {
|
|
146
|
+
key: "intern",
|
|
147
|
+
label: "Intern",
|
|
148
|
+
description: "Limited access for interns"
|
|
149
|
+
},
|
|
150
|
+
CONSULTANT: {
|
|
151
|
+
key: "consultant",
|
|
152
|
+
label: "Consultant",
|
|
153
|
+
description: "Project-based consultant access"
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
Object.values(ORG_ROLES).map((role) => role.key);
|
|
157
|
+
var ROLE_MAPPING = {
|
|
158
|
+
byDepartment: {
|
|
159
|
+
management: "manager",
|
|
160
|
+
training: "trainer",
|
|
161
|
+
sales: "staff",
|
|
162
|
+
operations: "staff",
|
|
163
|
+
finance: "staff",
|
|
164
|
+
hr: "staff",
|
|
165
|
+
marketing: "staff",
|
|
166
|
+
it: "staff",
|
|
167
|
+
support: "staff",
|
|
168
|
+
maintenance: "staff"
|
|
169
|
+
},
|
|
170
|
+
byEmploymentType: {
|
|
171
|
+
full_time: "staff",
|
|
172
|
+
part_time: "staff",
|
|
173
|
+
contract: "consultant",
|
|
174
|
+
intern: "intern",
|
|
175
|
+
consultant: "consultant"
|
|
176
|
+
},
|
|
177
|
+
default: "staff"
|
|
178
|
+
};
|
|
179
|
+
function calculateTax(annualIncome, currency = "BDT") {
|
|
180
|
+
const brackets = TAX_BRACKETS[currency];
|
|
181
|
+
if (!brackets) return 0;
|
|
182
|
+
let tax = 0;
|
|
183
|
+
for (const bracket of brackets) {
|
|
184
|
+
if (annualIncome > bracket.min) {
|
|
185
|
+
const taxableAmount = Math.min(annualIncome, bracket.max) - bracket.min;
|
|
186
|
+
tax += taxableAmount * bracket.rate;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return Math.round(tax);
|
|
190
|
+
}
|
|
191
|
+
function determineOrgRole(employmentData) {
|
|
192
|
+
const { department, type: employmentType } = employmentData;
|
|
193
|
+
if (department && department in ROLE_MAPPING.byDepartment) {
|
|
194
|
+
return ROLE_MAPPING.byDepartment[department];
|
|
195
|
+
}
|
|
196
|
+
if (employmentType && employmentType in ROLE_MAPPING.byEmploymentType) {
|
|
197
|
+
return ROLE_MAPPING.byEmploymentType[employmentType];
|
|
198
|
+
}
|
|
199
|
+
return ROLE_MAPPING.default;
|
|
200
|
+
}
|
|
201
|
+
function mergeConfig(customConfig) {
|
|
202
|
+
if (!customConfig) return HRM_CONFIG;
|
|
203
|
+
return {
|
|
204
|
+
dataRetention: { ...HRM_CONFIG.dataRetention, ...customConfig.dataRetention },
|
|
205
|
+
payroll: { ...HRM_CONFIG.payroll, ...customConfig.payroll },
|
|
206
|
+
salary: { ...HRM_CONFIG.salary, ...customConfig.salary },
|
|
207
|
+
employment: { ...HRM_CONFIG.employment, ...customConfig.employment },
|
|
208
|
+
validation: { ...HRM_CONFIG.validation, ...customConfig.validation }
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/core/container.ts
|
|
213
|
+
var Container = class _Container {
|
|
214
|
+
static instance = null;
|
|
215
|
+
_models = null;
|
|
216
|
+
_config = HRM_CONFIG;
|
|
217
|
+
_singleTenant = null;
|
|
218
|
+
_logger;
|
|
219
|
+
_initialized = false;
|
|
220
|
+
constructor() {
|
|
221
|
+
this._logger = getLogger();
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Get singleton instance
|
|
225
|
+
*/
|
|
226
|
+
static getInstance() {
|
|
227
|
+
if (!_Container.instance) {
|
|
228
|
+
_Container.instance = new _Container();
|
|
229
|
+
}
|
|
230
|
+
return _Container.instance;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Reset instance (for testing)
|
|
234
|
+
*/
|
|
235
|
+
static resetInstance() {
|
|
236
|
+
_Container.instance = null;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Initialize container with configuration
|
|
240
|
+
*/
|
|
241
|
+
initialize(config) {
|
|
242
|
+
if (this._initialized) {
|
|
243
|
+
this._logger.warn("Container already initialized, re-initializing");
|
|
244
|
+
}
|
|
245
|
+
this._models = config.models;
|
|
246
|
+
this._config = mergeConfig(config.config);
|
|
247
|
+
this._singleTenant = config.singleTenant ?? null;
|
|
248
|
+
if (config.logger) {
|
|
249
|
+
this._logger = config.logger;
|
|
250
|
+
}
|
|
251
|
+
this._initialized = true;
|
|
252
|
+
this._logger.info("Container initialized", {
|
|
253
|
+
hasEmployeeModel: !!this._models.EmployeeModel,
|
|
254
|
+
hasPayrollRecordModel: !!this._models.PayrollRecordModel,
|
|
255
|
+
hasTransactionModel: !!this._models.TransactionModel,
|
|
256
|
+
hasAttendanceModel: !!this._models.AttendanceModel,
|
|
257
|
+
isSingleTenant: !!this._singleTenant
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Check if container is initialized
|
|
262
|
+
*/
|
|
263
|
+
isInitialized() {
|
|
264
|
+
return this._initialized;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Ensure container is initialized
|
|
268
|
+
*/
|
|
269
|
+
ensureInitialized() {
|
|
270
|
+
if (!this._initialized || !this._models) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
"Payroll not initialized. Call Payroll.initialize() first."
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Get models container
|
|
278
|
+
*/
|
|
279
|
+
getModels() {
|
|
280
|
+
this.ensureInitialized();
|
|
281
|
+
return this._models;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Get Employee model
|
|
285
|
+
*/
|
|
286
|
+
getEmployeeModel() {
|
|
287
|
+
this.ensureInitialized();
|
|
288
|
+
return this._models.EmployeeModel;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Get PayrollRecord model
|
|
292
|
+
*/
|
|
293
|
+
getPayrollRecordModel() {
|
|
294
|
+
this.ensureInitialized();
|
|
295
|
+
return this._models.PayrollRecordModel;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Get Transaction model
|
|
299
|
+
*/
|
|
300
|
+
getTransactionModel() {
|
|
301
|
+
this.ensureInitialized();
|
|
302
|
+
return this._models.TransactionModel;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Get Attendance model (optional)
|
|
306
|
+
*/
|
|
307
|
+
getAttendanceModel() {
|
|
308
|
+
this.ensureInitialized();
|
|
309
|
+
return this._models.AttendanceModel ?? null;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Get configuration
|
|
313
|
+
*/
|
|
314
|
+
getConfig() {
|
|
315
|
+
return this._config;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Get specific config section
|
|
319
|
+
*/
|
|
320
|
+
getConfigSection(section) {
|
|
321
|
+
return this._config[section];
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Check if single-tenant mode
|
|
325
|
+
*/
|
|
326
|
+
isSingleTenant() {
|
|
327
|
+
return !!this._singleTenant;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Get single-tenant config
|
|
331
|
+
*/
|
|
332
|
+
getSingleTenantConfig() {
|
|
333
|
+
return this._singleTenant;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Get organization ID (for single-tenant mode)
|
|
337
|
+
*/
|
|
338
|
+
getOrganizationId() {
|
|
339
|
+
if (!this._singleTenant || !this._singleTenant.organizationId) return null;
|
|
340
|
+
return typeof this._singleTenant.organizationId === "string" ? this._singleTenant.organizationId : this._singleTenant.organizationId.toString();
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Get logger
|
|
344
|
+
*/
|
|
345
|
+
getLogger() {
|
|
346
|
+
return this._logger;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Set logger
|
|
350
|
+
*/
|
|
351
|
+
setLogger(logger2) {
|
|
352
|
+
this._logger = logger2;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Has attendance integration
|
|
356
|
+
*/
|
|
357
|
+
hasAttendanceIntegration() {
|
|
358
|
+
return !!this._models?.AttendanceModel && this._config.payroll.attendanceIntegration;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Create operation context with defaults
|
|
362
|
+
*/
|
|
363
|
+
createOperationContext(overrides) {
|
|
364
|
+
const context = {};
|
|
365
|
+
if (this._singleTenant?.autoInject && !overrides?.organizationId) {
|
|
366
|
+
context.organizationId = this.getOrganizationId();
|
|
367
|
+
}
|
|
368
|
+
return { ...context, ...overrides };
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
function initializeContainer(config) {
|
|
372
|
+
Container.getInstance().initialize(config);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/core/events.ts
|
|
376
|
+
var EventBus = class {
|
|
377
|
+
handlers = /* @__PURE__ */ new Map();
|
|
378
|
+
/**
|
|
379
|
+
* Register an event handler
|
|
380
|
+
*/
|
|
381
|
+
on(event, handler) {
|
|
382
|
+
if (!this.handlers.has(event)) {
|
|
383
|
+
this.handlers.set(event, /* @__PURE__ */ new Set());
|
|
384
|
+
}
|
|
385
|
+
this.handlers.get(event).add(handler);
|
|
386
|
+
return () => this.off(event, handler);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Register a one-time event handler
|
|
390
|
+
*/
|
|
391
|
+
once(event, handler) {
|
|
392
|
+
const wrappedHandler = async (payload) => {
|
|
393
|
+
this.off(event, wrappedHandler);
|
|
394
|
+
await handler(payload);
|
|
395
|
+
};
|
|
396
|
+
return this.on(event, wrappedHandler);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Remove an event handler
|
|
400
|
+
*/
|
|
401
|
+
off(event, handler) {
|
|
402
|
+
const eventHandlers = this.handlers.get(event);
|
|
403
|
+
if (eventHandlers) {
|
|
404
|
+
eventHandlers.delete(handler);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Emit an event
|
|
409
|
+
*/
|
|
410
|
+
async emit(event, payload) {
|
|
411
|
+
const eventHandlers = this.handlers.get(event);
|
|
412
|
+
if (!eventHandlers || eventHandlers.size === 0) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const handlers = Array.from(eventHandlers);
|
|
416
|
+
await Promise.all(
|
|
417
|
+
handlers.map(async (handler) => {
|
|
418
|
+
try {
|
|
419
|
+
await handler(payload);
|
|
420
|
+
} catch (error) {
|
|
421
|
+
console.error(`Event handler error for ${event}:`, error);
|
|
422
|
+
}
|
|
423
|
+
})
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Emit event synchronously (fire-and-forget)
|
|
428
|
+
*/
|
|
429
|
+
emitSync(event, payload) {
|
|
430
|
+
void this.emit(event, payload);
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Remove all handlers for an event
|
|
434
|
+
*/
|
|
435
|
+
removeAllListeners(event) {
|
|
436
|
+
if (event) {
|
|
437
|
+
this.handlers.delete(event);
|
|
438
|
+
} else {
|
|
439
|
+
this.handlers.clear();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Get listener count for an event
|
|
444
|
+
*/
|
|
445
|
+
listenerCount(event) {
|
|
446
|
+
return this.handlers.get(event)?.size ?? 0;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Get all registered events
|
|
450
|
+
*/
|
|
451
|
+
eventNames() {
|
|
452
|
+
return Array.from(this.handlers.keys());
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
function createEventBus() {
|
|
456
|
+
return new EventBus();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// src/core/plugin.ts
|
|
460
|
+
var PluginManager = class {
|
|
461
|
+
constructor(context) {
|
|
462
|
+
this.context = context;
|
|
463
|
+
}
|
|
464
|
+
plugins = /* @__PURE__ */ new Map();
|
|
465
|
+
hooks = /* @__PURE__ */ new Map();
|
|
466
|
+
/**
|
|
467
|
+
* Register a plugin
|
|
468
|
+
*/
|
|
469
|
+
async register(plugin) {
|
|
470
|
+
if (this.plugins.has(plugin.name)) {
|
|
471
|
+
throw new Error(`Plugin "${plugin.name}" is already registered`);
|
|
472
|
+
}
|
|
473
|
+
if (plugin.hooks) {
|
|
474
|
+
for (const [hookName, handler] of Object.entries(plugin.hooks)) {
|
|
475
|
+
if (handler) {
|
|
476
|
+
this.addHook(hookName, handler);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (plugin.init) {
|
|
481
|
+
await plugin.init(this.context);
|
|
482
|
+
}
|
|
483
|
+
this.plugins.set(plugin.name, plugin);
|
|
484
|
+
this.context.logger.debug(`Plugin "${plugin.name}" registered`);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Unregister a plugin
|
|
488
|
+
*/
|
|
489
|
+
async unregister(name) {
|
|
490
|
+
const plugin = this.plugins.get(name);
|
|
491
|
+
if (!plugin) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (plugin.destroy) {
|
|
495
|
+
await plugin.destroy();
|
|
496
|
+
}
|
|
497
|
+
this.plugins.delete(name);
|
|
498
|
+
this.context.logger.debug(`Plugin "${name}" unregistered`);
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Add a hook handler
|
|
502
|
+
*/
|
|
503
|
+
addHook(hookName, handler) {
|
|
504
|
+
if (!this.hooks.has(hookName)) {
|
|
505
|
+
this.hooks.set(hookName, []);
|
|
506
|
+
}
|
|
507
|
+
this.hooks.get(hookName).push(handler);
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Execute hooks for a given event
|
|
511
|
+
*/
|
|
512
|
+
async executeHooks(hookName, ...args) {
|
|
513
|
+
const handlers = this.hooks.get(hookName);
|
|
514
|
+
if (!handlers || handlers.length === 0) {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
for (const handler of handlers) {
|
|
518
|
+
try {
|
|
519
|
+
await handler(...args);
|
|
520
|
+
} catch (error) {
|
|
521
|
+
this.context.logger.error(`Hook "${hookName}" error:`, { error });
|
|
522
|
+
const errorHandlers = this.hooks.get("onError");
|
|
523
|
+
if (errorHandlers) {
|
|
524
|
+
for (const errorHandler of errorHandlers) {
|
|
525
|
+
try {
|
|
526
|
+
await errorHandler(error, hookName);
|
|
527
|
+
} catch {
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Get registered plugin names
|
|
536
|
+
*/
|
|
537
|
+
getPluginNames() {
|
|
538
|
+
return Array.from(this.plugins.keys());
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Check if plugin is registered
|
|
542
|
+
*/
|
|
543
|
+
hasPlugin(name) {
|
|
544
|
+
return this.plugins.has(name);
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
// src/utils/date.ts
|
|
549
|
+
function addDays(date, days) {
|
|
550
|
+
const result = new Date(date);
|
|
551
|
+
result.setDate(result.getDate() + days);
|
|
552
|
+
return result;
|
|
553
|
+
}
|
|
554
|
+
function addMonths(date, months) {
|
|
555
|
+
const result = new Date(date);
|
|
556
|
+
result.setMonth(result.getMonth() + months);
|
|
557
|
+
return result;
|
|
558
|
+
}
|
|
559
|
+
function addYears(date, years) {
|
|
560
|
+
const result = new Date(date);
|
|
561
|
+
result.setFullYear(result.getFullYear() + years);
|
|
562
|
+
return result;
|
|
563
|
+
}
|
|
564
|
+
function startOfMonth(date) {
|
|
565
|
+
const result = new Date(date);
|
|
566
|
+
result.setDate(1);
|
|
567
|
+
result.setHours(0, 0, 0, 0);
|
|
568
|
+
return result;
|
|
569
|
+
}
|
|
570
|
+
function endOfMonth(date) {
|
|
571
|
+
const result = new Date(date);
|
|
572
|
+
result.setMonth(result.getMonth() + 1, 0);
|
|
573
|
+
result.setHours(23, 59, 59, 999);
|
|
574
|
+
return result;
|
|
575
|
+
}
|
|
576
|
+
function startOfYear(date) {
|
|
577
|
+
const result = new Date(date);
|
|
578
|
+
result.setMonth(0, 1);
|
|
579
|
+
result.setHours(0, 0, 0, 0);
|
|
580
|
+
return result;
|
|
581
|
+
}
|
|
582
|
+
function endOfYear(date) {
|
|
583
|
+
const result = new Date(date);
|
|
584
|
+
result.setMonth(11, 31);
|
|
585
|
+
result.setHours(23, 59, 59, 999);
|
|
586
|
+
return result;
|
|
587
|
+
}
|
|
588
|
+
function diffInDays(start, end) {
|
|
589
|
+
return Math.ceil(
|
|
590
|
+
(new Date(end).getTime() - new Date(start).getTime()) / (1e3 * 60 * 60 * 24)
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
function diffInMonths(start, end) {
|
|
594
|
+
const startDate = new Date(start);
|
|
595
|
+
const endDate = new Date(end);
|
|
596
|
+
return (endDate.getFullYear() - startDate.getFullYear()) * 12 + (endDate.getMonth() - startDate.getMonth());
|
|
597
|
+
}
|
|
598
|
+
function isWeekday(date) {
|
|
599
|
+
const day = new Date(date).getDay();
|
|
600
|
+
return day >= 1 && day <= 5;
|
|
601
|
+
}
|
|
602
|
+
function getPayPeriod(month, year) {
|
|
603
|
+
const startDate = new Date(year, month - 1, 1);
|
|
604
|
+
return {
|
|
605
|
+
month,
|
|
606
|
+
year,
|
|
607
|
+
startDate: startOfMonth(startDate),
|
|
608
|
+
endDate: endOfMonth(startDate)
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
function getCurrentPeriod(date = /* @__PURE__ */ new Date()) {
|
|
612
|
+
const d = new Date(date);
|
|
613
|
+
return {
|
|
614
|
+
year: d.getFullYear(),
|
|
615
|
+
month: d.getMonth() + 1
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function getWorkingDaysInMonth(year, month) {
|
|
619
|
+
const start = new Date(year, month - 1, 1);
|
|
620
|
+
const end = endOfMonth(start);
|
|
621
|
+
let count = 0;
|
|
622
|
+
const current = new Date(start);
|
|
623
|
+
while (current <= end) {
|
|
624
|
+
if (isWeekday(current)) {
|
|
625
|
+
count++;
|
|
626
|
+
}
|
|
627
|
+
current.setDate(current.getDate() + 1);
|
|
628
|
+
}
|
|
629
|
+
return count;
|
|
630
|
+
}
|
|
631
|
+
function calculateProbationEnd(hireDate, probationMonths) {
|
|
632
|
+
if (!probationMonths || probationMonths <= 0) return null;
|
|
633
|
+
return addMonths(hireDate, probationMonths);
|
|
634
|
+
}
|
|
635
|
+
function isOnProbation(probationEndDate, now = /* @__PURE__ */ new Date()) {
|
|
636
|
+
if (!probationEndDate) return false;
|
|
637
|
+
return now < new Date(probationEndDate);
|
|
638
|
+
}
|
|
639
|
+
function isDateInRange(date, start, end) {
|
|
640
|
+
const checkDate = new Date(date);
|
|
641
|
+
return checkDate >= new Date(start) && checkDate <= new Date(end);
|
|
642
|
+
}
|
|
643
|
+
function formatDateForDB(date) {
|
|
644
|
+
if (!date) return "";
|
|
645
|
+
return new Date(date).toISOString();
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// src/factories/employee.factory.ts
|
|
649
|
+
var EmployeeFactory = class {
|
|
650
|
+
/**
|
|
651
|
+
* Create employee data object
|
|
652
|
+
*/
|
|
653
|
+
static create(params) {
|
|
654
|
+
const { userId, organizationId, employment, compensation, bankDetails } = params;
|
|
655
|
+
const hireDate = employment.hireDate || /* @__PURE__ */ new Date();
|
|
656
|
+
return {
|
|
657
|
+
userId,
|
|
658
|
+
organizationId,
|
|
659
|
+
employeeId: employment.employeeId || `EMP-${Date.now()}-${Math.random().toString(36).substr(2, 6).toUpperCase()}`,
|
|
660
|
+
employmentType: employment.type || "full_time",
|
|
661
|
+
status: "active",
|
|
662
|
+
department: employment.department,
|
|
663
|
+
position: employment.position,
|
|
664
|
+
hireDate,
|
|
665
|
+
probationEndDate: calculateProbationEnd(
|
|
666
|
+
hireDate,
|
|
667
|
+
employment.probationMonths ?? HRM_CONFIG.employment.defaultProbationMonths
|
|
668
|
+
),
|
|
669
|
+
compensation: this.createCompensation(compensation),
|
|
670
|
+
workSchedule: employment.workSchedule || this.defaultWorkSchedule(),
|
|
671
|
+
bankDetails: bankDetails || {},
|
|
672
|
+
payrollStats: {
|
|
673
|
+
totalPaid: 0,
|
|
674
|
+
paymentsThisYear: 0,
|
|
675
|
+
averageMonthly: 0
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Create compensation object
|
|
681
|
+
*/
|
|
682
|
+
static createCompensation(params) {
|
|
683
|
+
return {
|
|
684
|
+
baseAmount: params.baseAmount,
|
|
685
|
+
frequency: params.frequency || "monthly",
|
|
686
|
+
currency: params.currency || HRM_CONFIG.payroll.defaultCurrency,
|
|
687
|
+
allowances: (params.allowances || []).map((a) => ({
|
|
688
|
+
type: a.type || "other",
|
|
689
|
+
name: a.name || a.type || "other",
|
|
690
|
+
amount: a.amount || 0,
|
|
691
|
+
taxable: a.taxable,
|
|
692
|
+
recurring: a.recurring,
|
|
693
|
+
effectiveFrom: a.effectiveFrom,
|
|
694
|
+
effectiveTo: a.effectiveTo
|
|
695
|
+
})),
|
|
696
|
+
deductions: (params.deductions || []).map((d) => ({
|
|
697
|
+
type: d.type || "other",
|
|
698
|
+
name: d.name || d.type || "other",
|
|
699
|
+
amount: d.amount || 0,
|
|
700
|
+
auto: d.auto,
|
|
701
|
+
recurring: d.recurring,
|
|
702
|
+
description: d.description,
|
|
703
|
+
effectiveFrom: d.effectiveFrom,
|
|
704
|
+
effectiveTo: d.effectiveTo
|
|
705
|
+
})),
|
|
706
|
+
grossSalary: 0,
|
|
707
|
+
netSalary: 0,
|
|
708
|
+
effectiveFrom: /* @__PURE__ */ new Date(),
|
|
709
|
+
lastModified: /* @__PURE__ */ new Date()
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Create allowance object
|
|
714
|
+
*/
|
|
715
|
+
static createAllowance(params) {
|
|
716
|
+
return {
|
|
717
|
+
type: params.type,
|
|
718
|
+
name: params.name || params.type,
|
|
719
|
+
amount: params.amount,
|
|
720
|
+
isPercentage: params.isPercentage ?? false,
|
|
721
|
+
taxable: params.taxable ?? true,
|
|
722
|
+
recurring: params.recurring ?? true,
|
|
723
|
+
effectiveFrom: /* @__PURE__ */ new Date()
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Create deduction object
|
|
728
|
+
*/
|
|
729
|
+
static createDeduction(params) {
|
|
730
|
+
return {
|
|
731
|
+
type: params.type,
|
|
732
|
+
name: params.name || params.type,
|
|
733
|
+
amount: params.amount,
|
|
734
|
+
isPercentage: params.isPercentage ?? false,
|
|
735
|
+
auto: params.auto ?? false,
|
|
736
|
+
recurring: params.recurring ?? true,
|
|
737
|
+
description: params.description,
|
|
738
|
+
effectiveFrom: /* @__PURE__ */ new Date()
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Default work schedule
|
|
743
|
+
*/
|
|
744
|
+
static defaultWorkSchedule() {
|
|
745
|
+
return {
|
|
746
|
+
hoursPerWeek: 40,
|
|
747
|
+
hoursPerDay: 8,
|
|
748
|
+
workingDays: [1, 2, 3, 4, 5],
|
|
749
|
+
// Mon-Fri
|
|
750
|
+
shiftStart: "09:00",
|
|
751
|
+
shiftEnd: "17:00"
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Create termination data
|
|
756
|
+
*/
|
|
757
|
+
static createTermination(params) {
|
|
758
|
+
return {
|
|
759
|
+
terminatedAt: params.date || /* @__PURE__ */ new Date(),
|
|
760
|
+
terminationReason: params.reason,
|
|
761
|
+
terminationNotes: params.notes,
|
|
762
|
+
terminatedBy: {
|
|
763
|
+
userId: params.context?.userId,
|
|
764
|
+
name: params.context?.userName,
|
|
765
|
+
role: params.context?.userRole
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
var EmployeeBuilder = class {
|
|
771
|
+
data = {
|
|
772
|
+
employment: {},
|
|
773
|
+
compensation: {},
|
|
774
|
+
bankDetails: {}
|
|
775
|
+
};
|
|
776
|
+
/**
|
|
777
|
+
* Set user ID
|
|
778
|
+
*/
|
|
779
|
+
forUser(userId) {
|
|
780
|
+
this.data.userId = userId;
|
|
781
|
+
return this;
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Set organization ID
|
|
785
|
+
*/
|
|
786
|
+
inOrganization(organizationId) {
|
|
787
|
+
this.data.organizationId = organizationId;
|
|
788
|
+
return this;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Set employee ID
|
|
792
|
+
*/
|
|
793
|
+
withEmployeeId(employeeId) {
|
|
794
|
+
this.data.employment = { ...this.data.employment, employeeId };
|
|
795
|
+
return this;
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Set department
|
|
799
|
+
*/
|
|
800
|
+
inDepartment(department) {
|
|
801
|
+
this.data.employment = { ...this.data.employment, department };
|
|
802
|
+
return this;
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Set position
|
|
806
|
+
*/
|
|
807
|
+
asPosition(position) {
|
|
808
|
+
this.data.employment = { ...this.data.employment, position };
|
|
809
|
+
return this;
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Set employment type
|
|
813
|
+
*/
|
|
814
|
+
withEmploymentType(type) {
|
|
815
|
+
this.data.employment = { ...this.data.employment, type };
|
|
816
|
+
return this;
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Set hire date
|
|
820
|
+
*/
|
|
821
|
+
hiredOn(date) {
|
|
822
|
+
this.data.employment = { ...this.data.employment, hireDate: date };
|
|
823
|
+
return this;
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Set probation period
|
|
827
|
+
*/
|
|
828
|
+
withProbation(months) {
|
|
829
|
+
this.data.employment = { ...this.data.employment, probationMonths: months };
|
|
830
|
+
return this;
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Set work schedule
|
|
834
|
+
*/
|
|
835
|
+
withSchedule(schedule) {
|
|
836
|
+
this.data.employment = { ...this.data.employment, workSchedule: schedule };
|
|
837
|
+
return this;
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Set base salary
|
|
841
|
+
*/
|
|
842
|
+
withBaseSalary(amount, frequency = "monthly", currency = "BDT") {
|
|
843
|
+
this.data.compensation = {
|
|
844
|
+
...this.data.compensation,
|
|
845
|
+
baseAmount: amount,
|
|
846
|
+
frequency,
|
|
847
|
+
currency
|
|
848
|
+
};
|
|
849
|
+
return this;
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Add allowance
|
|
853
|
+
*/
|
|
854
|
+
addAllowance(type, amount, options = {}) {
|
|
855
|
+
const allowances = this.data.compensation?.allowances || [];
|
|
856
|
+
this.data.compensation = {
|
|
857
|
+
...this.data.compensation,
|
|
858
|
+
allowances: [
|
|
859
|
+
...allowances,
|
|
860
|
+
{ type, amount, taxable: options.taxable, recurring: options.recurring }
|
|
861
|
+
]
|
|
862
|
+
};
|
|
863
|
+
return this;
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Add deduction
|
|
867
|
+
*/
|
|
868
|
+
addDeduction(type, amount, options = {}) {
|
|
869
|
+
const deductions = this.data.compensation?.deductions || [];
|
|
870
|
+
this.data.compensation = {
|
|
871
|
+
...this.data.compensation,
|
|
872
|
+
deductions: [
|
|
873
|
+
...deductions,
|
|
874
|
+
{
|
|
875
|
+
type,
|
|
876
|
+
amount,
|
|
877
|
+
auto: options.auto,
|
|
878
|
+
recurring: options.recurring,
|
|
879
|
+
description: options.description
|
|
880
|
+
}
|
|
881
|
+
]
|
|
882
|
+
};
|
|
883
|
+
return this;
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Set bank details
|
|
887
|
+
*/
|
|
888
|
+
withBankDetails(bankDetails) {
|
|
889
|
+
this.data.bankDetails = bankDetails;
|
|
890
|
+
return this;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Build employee data
|
|
894
|
+
*/
|
|
895
|
+
build() {
|
|
896
|
+
if (!this.data.userId || !this.data.organizationId) {
|
|
897
|
+
throw new Error("userId and organizationId are required");
|
|
898
|
+
}
|
|
899
|
+
if (!this.data.employment?.employeeId || !this.data.employment?.position) {
|
|
900
|
+
throw new Error("employeeId and position are required");
|
|
901
|
+
}
|
|
902
|
+
if (!this.data.compensation?.baseAmount) {
|
|
903
|
+
throw new Error("baseAmount is required");
|
|
904
|
+
}
|
|
905
|
+
return EmployeeFactory.create(this.data);
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
// src/enums.ts
|
|
910
|
+
var EMPLOYMENT_TYPE = {
|
|
911
|
+
FULL_TIME: "full_time",
|
|
912
|
+
PART_TIME: "part_time",
|
|
913
|
+
CONTRACT: "contract",
|
|
914
|
+
INTERN: "intern",
|
|
915
|
+
CONSULTANT: "consultant"
|
|
916
|
+
};
|
|
917
|
+
var EMPLOYMENT_TYPE_VALUES = Object.values(EMPLOYMENT_TYPE);
|
|
918
|
+
var EMPLOYEE_STATUS = {
|
|
919
|
+
ACTIVE: "active",
|
|
920
|
+
ON_LEAVE: "on_leave",
|
|
921
|
+
SUSPENDED: "suspended",
|
|
922
|
+
TERMINATED: "terminated"
|
|
923
|
+
};
|
|
924
|
+
var EMPLOYEE_STATUS_VALUES = Object.values(EMPLOYEE_STATUS);
|
|
925
|
+
var DEPARTMENT = {
|
|
926
|
+
MANAGEMENT: "management",
|
|
927
|
+
TRAINING: "training",
|
|
928
|
+
SALES: "sales",
|
|
929
|
+
OPERATIONS: "operations",
|
|
930
|
+
SUPPORT: "support",
|
|
931
|
+
HR: "hr",
|
|
932
|
+
MAINTENANCE: "maintenance",
|
|
933
|
+
MARKETING: "marketing",
|
|
934
|
+
FINANCE: "finance",
|
|
935
|
+
IT: "it"
|
|
936
|
+
};
|
|
937
|
+
var DEPARTMENT_VALUES = Object.values(DEPARTMENT);
|
|
938
|
+
var PAYMENT_FREQUENCY = {
|
|
939
|
+
MONTHLY: "monthly",
|
|
940
|
+
BI_WEEKLY: "bi_weekly",
|
|
941
|
+
WEEKLY: "weekly",
|
|
942
|
+
HOURLY: "hourly",
|
|
943
|
+
DAILY: "daily"
|
|
944
|
+
};
|
|
945
|
+
var PAYMENT_FREQUENCY_VALUES = Object.values(PAYMENT_FREQUENCY);
|
|
946
|
+
var ALLOWANCE_TYPE = {
|
|
947
|
+
HOUSING: "housing",
|
|
948
|
+
TRANSPORT: "transport",
|
|
949
|
+
MEAL: "meal",
|
|
950
|
+
MOBILE: "mobile",
|
|
951
|
+
MEDICAL: "medical",
|
|
952
|
+
EDUCATION: "education",
|
|
953
|
+
BONUS: "bonus",
|
|
954
|
+
OTHER: "other"
|
|
955
|
+
};
|
|
956
|
+
var ALLOWANCE_TYPE_VALUES = Object.values(ALLOWANCE_TYPE);
|
|
957
|
+
var DEDUCTION_TYPE = {
|
|
958
|
+
TAX: "tax",
|
|
959
|
+
LOAN: "loan",
|
|
960
|
+
ADVANCE: "advance",
|
|
961
|
+
PROVIDENT_FUND: "provident_fund",
|
|
962
|
+
INSURANCE: "insurance",
|
|
963
|
+
ABSENCE: "absence",
|
|
964
|
+
OTHER: "other"
|
|
965
|
+
};
|
|
966
|
+
var DEDUCTION_TYPE_VALUES = Object.values(DEDUCTION_TYPE);
|
|
967
|
+
var PAYROLL_STATUS = {
|
|
968
|
+
PENDING: "pending",
|
|
969
|
+
PROCESSING: "processing",
|
|
970
|
+
PAID: "paid",
|
|
971
|
+
FAILED: "failed",
|
|
972
|
+
CANCELLED: "cancelled"
|
|
973
|
+
};
|
|
974
|
+
Object.values(PAYROLL_STATUS);
|
|
975
|
+
var TERMINATION_REASON = {
|
|
976
|
+
RESIGNATION: "resignation",
|
|
977
|
+
RETIREMENT: "retirement",
|
|
978
|
+
TERMINATION: "termination",
|
|
979
|
+
CONTRACT_END: "contract_end",
|
|
980
|
+
MUTUAL_AGREEMENT: "mutual_agreement",
|
|
981
|
+
OTHER: "other"
|
|
982
|
+
};
|
|
983
|
+
var TERMINATION_REASON_VALUES = Object.values(TERMINATION_REASON);
|
|
984
|
+
var HRM_TRANSACTION_CATEGORIES = {
|
|
985
|
+
SALARY: "salary",
|
|
986
|
+
BONUS: "bonus",
|
|
987
|
+
COMMISSION: "commission",
|
|
988
|
+
OVERTIME: "overtime",
|
|
989
|
+
SEVERANCE: "severance"
|
|
990
|
+
};
|
|
991
|
+
Object.values(HRM_TRANSACTION_CATEGORIES);
|
|
992
|
+
function toObjectId(id) {
|
|
993
|
+
if (id instanceof Types.ObjectId) return id;
|
|
994
|
+
return new Types.ObjectId(id);
|
|
995
|
+
}
|
|
996
|
+
var QueryBuilder = class {
|
|
997
|
+
query;
|
|
998
|
+
constructor(initialQuery = {}) {
|
|
999
|
+
this.query = { ...initialQuery };
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Add where condition
|
|
1003
|
+
*/
|
|
1004
|
+
where(field, value) {
|
|
1005
|
+
this.query[field] = value;
|
|
1006
|
+
return this;
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Add $in condition
|
|
1010
|
+
*/
|
|
1011
|
+
whereIn(field, values) {
|
|
1012
|
+
this.query[field] = { $in: values };
|
|
1013
|
+
return this;
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Add $nin condition
|
|
1017
|
+
*/
|
|
1018
|
+
whereNotIn(field, values) {
|
|
1019
|
+
this.query[field] = { $nin: values };
|
|
1020
|
+
return this;
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Add $gte condition
|
|
1024
|
+
*/
|
|
1025
|
+
whereGte(field, value) {
|
|
1026
|
+
const existing = this.query[field] || {};
|
|
1027
|
+
this.query[field] = { ...existing, $gte: value };
|
|
1028
|
+
return this;
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Add $lte condition
|
|
1032
|
+
*/
|
|
1033
|
+
whereLte(field, value) {
|
|
1034
|
+
const existing = this.query[field] || {};
|
|
1035
|
+
this.query[field] = { ...existing, $lte: value };
|
|
1036
|
+
return this;
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Add $gt condition
|
|
1040
|
+
*/
|
|
1041
|
+
whereGt(field, value) {
|
|
1042
|
+
const existing = this.query[field] || {};
|
|
1043
|
+
this.query[field] = { ...existing, $gt: value };
|
|
1044
|
+
return this;
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Add $lt condition
|
|
1048
|
+
*/
|
|
1049
|
+
whereLt(field, value) {
|
|
1050
|
+
const existing = this.query[field] || {};
|
|
1051
|
+
this.query[field] = { ...existing, $lt: value };
|
|
1052
|
+
return this;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Add between condition
|
|
1056
|
+
*/
|
|
1057
|
+
whereBetween(field, start, end) {
|
|
1058
|
+
this.query[field] = { $gte: start, $lte: end };
|
|
1059
|
+
return this;
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Add $exists condition
|
|
1063
|
+
*/
|
|
1064
|
+
whereExists(field) {
|
|
1065
|
+
this.query[field] = { $exists: true };
|
|
1066
|
+
return this;
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Add $exists: false condition
|
|
1070
|
+
*/
|
|
1071
|
+
whereNotExists(field) {
|
|
1072
|
+
this.query[field] = { $exists: false };
|
|
1073
|
+
return this;
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Add $ne condition
|
|
1077
|
+
*/
|
|
1078
|
+
whereNot(field, value) {
|
|
1079
|
+
this.query[field] = { $ne: value };
|
|
1080
|
+
return this;
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Add regex condition
|
|
1084
|
+
*/
|
|
1085
|
+
whereRegex(field, pattern, flags = "i") {
|
|
1086
|
+
this.query[field] = { $regex: pattern, $options: flags };
|
|
1087
|
+
return this;
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Merge another query
|
|
1091
|
+
*/
|
|
1092
|
+
merge(otherQuery) {
|
|
1093
|
+
this.query = { ...this.query, ...otherQuery };
|
|
1094
|
+
return this;
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Build and return the query
|
|
1098
|
+
*/
|
|
1099
|
+
build() {
|
|
1100
|
+
return { ...this.query };
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
var EmployeeQueryBuilder = class extends QueryBuilder {
|
|
1104
|
+
/**
|
|
1105
|
+
* Filter by organization
|
|
1106
|
+
*/
|
|
1107
|
+
forOrganization(organizationId) {
|
|
1108
|
+
return this.where("organizationId", toObjectId(organizationId));
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Filter by user
|
|
1112
|
+
*/
|
|
1113
|
+
forUser(userId) {
|
|
1114
|
+
return this.where("userId", toObjectId(userId));
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Filter by status(es)
|
|
1118
|
+
*/
|
|
1119
|
+
withStatus(...statuses) {
|
|
1120
|
+
if (statuses.length === 1) {
|
|
1121
|
+
return this.where("status", statuses[0]);
|
|
1122
|
+
}
|
|
1123
|
+
return this.whereIn("status", statuses);
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Filter active employees
|
|
1127
|
+
*/
|
|
1128
|
+
active() {
|
|
1129
|
+
return this.withStatus("active");
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Filter employed employees (not terminated)
|
|
1133
|
+
*/
|
|
1134
|
+
employed() {
|
|
1135
|
+
return this.whereIn("status", ["active", "on_leave", "suspended"]);
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Filter terminated employees
|
|
1139
|
+
*/
|
|
1140
|
+
terminated() {
|
|
1141
|
+
return this.withStatus("terminated");
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Filter by department
|
|
1145
|
+
*/
|
|
1146
|
+
inDepartment(department) {
|
|
1147
|
+
return this.where("department", department);
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Filter by position
|
|
1151
|
+
*/
|
|
1152
|
+
inPosition(position) {
|
|
1153
|
+
return this.where("position", position);
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Filter by employment type
|
|
1157
|
+
*/
|
|
1158
|
+
withEmploymentType(type) {
|
|
1159
|
+
return this.where("employmentType", type);
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Filter by hire date (after)
|
|
1163
|
+
*/
|
|
1164
|
+
hiredAfter(date) {
|
|
1165
|
+
return this.whereGte("hireDate", date);
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Filter by hire date (before)
|
|
1169
|
+
*/
|
|
1170
|
+
hiredBefore(date) {
|
|
1171
|
+
return this.whereLte("hireDate", date);
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Filter by minimum salary
|
|
1175
|
+
*/
|
|
1176
|
+
withMinSalary(amount) {
|
|
1177
|
+
return this.whereGte("compensation.netSalary", amount);
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Filter by maximum salary
|
|
1181
|
+
*/
|
|
1182
|
+
withMaxSalary(amount) {
|
|
1183
|
+
return this.whereLte("compensation.netSalary", amount);
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Filter by salary range
|
|
1187
|
+
*/
|
|
1188
|
+
withSalaryRange(min2, max2) {
|
|
1189
|
+
return this.whereBetween("compensation.netSalary", min2, max2);
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
var PayrollQueryBuilder = class extends QueryBuilder {
|
|
1193
|
+
/**
|
|
1194
|
+
* Filter by organization
|
|
1195
|
+
*/
|
|
1196
|
+
forOrganization(organizationId) {
|
|
1197
|
+
return this.where("organizationId", toObjectId(organizationId));
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Filter by employee
|
|
1201
|
+
*/
|
|
1202
|
+
forEmployee(employeeId) {
|
|
1203
|
+
return this.where("employeeId", toObjectId(employeeId));
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Filter by period
|
|
1207
|
+
*/
|
|
1208
|
+
forPeriod(month, year) {
|
|
1209
|
+
if (month !== void 0) {
|
|
1210
|
+
this.where("period.month", month);
|
|
1211
|
+
}
|
|
1212
|
+
if (year !== void 0) {
|
|
1213
|
+
this.where("period.year", year);
|
|
1214
|
+
}
|
|
1215
|
+
return this;
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Filter by status(es)
|
|
1219
|
+
*/
|
|
1220
|
+
withStatus(...statuses) {
|
|
1221
|
+
if (statuses.length === 1) {
|
|
1222
|
+
return this.where("status", statuses[0]);
|
|
1223
|
+
}
|
|
1224
|
+
return this.whereIn("status", statuses);
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Filter paid records
|
|
1228
|
+
*/
|
|
1229
|
+
paid() {
|
|
1230
|
+
return this.withStatus("paid");
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Filter pending records
|
|
1234
|
+
*/
|
|
1235
|
+
pending() {
|
|
1236
|
+
return this.whereIn("status", ["pending", "processing"]);
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Filter by date range
|
|
1240
|
+
*/
|
|
1241
|
+
inDateRange(start, end) {
|
|
1242
|
+
return this.whereBetween("period.payDate", start, end);
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Filter exported records
|
|
1246
|
+
*/
|
|
1247
|
+
exported() {
|
|
1248
|
+
return this.where("exported", true);
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* Filter not exported records
|
|
1252
|
+
*/
|
|
1253
|
+
notExported() {
|
|
1254
|
+
return this.where("exported", false);
|
|
1255
|
+
}
|
|
1256
|
+
};
|
|
1257
|
+
function employee() {
|
|
1258
|
+
return new EmployeeQueryBuilder();
|
|
1259
|
+
}
|
|
1260
|
+
function payroll() {
|
|
1261
|
+
return new PayrollQueryBuilder();
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// src/utils/calculation.ts
|
|
1265
|
+
function sum(numbers) {
|
|
1266
|
+
return numbers.reduce((total, n) => total + n, 0);
|
|
1267
|
+
}
|
|
1268
|
+
function sumBy(items, getter) {
|
|
1269
|
+
return items.reduce((total, item) => total + getter(item), 0);
|
|
1270
|
+
}
|
|
1271
|
+
function sumAllowances(allowances) {
|
|
1272
|
+
return sumBy(allowances, (a) => a.amount);
|
|
1273
|
+
}
|
|
1274
|
+
function sumDeductions(deductions) {
|
|
1275
|
+
return sumBy(deductions, (d) => d.amount);
|
|
1276
|
+
}
|
|
1277
|
+
function applyPercentage(amount, percentage) {
|
|
1278
|
+
return Math.round(amount * (percentage / 100));
|
|
1279
|
+
}
|
|
1280
|
+
function calculateGross(baseAmount, allowances) {
|
|
1281
|
+
return baseAmount + sumAllowances(allowances);
|
|
1282
|
+
}
|
|
1283
|
+
function calculateNet(gross, deductions) {
|
|
1284
|
+
return Math.max(0, gross - sumDeductions(deductions));
|
|
1285
|
+
}
|
|
1286
|
+
function calculateProRating(hireDate, periodStart, periodEnd) {
|
|
1287
|
+
const totalDays = diffInDays(periodStart, periodEnd) + 1;
|
|
1288
|
+
if (hireDate <= periodStart) {
|
|
1289
|
+
return {
|
|
1290
|
+
isProRated: false,
|
|
1291
|
+
totalDays,
|
|
1292
|
+
actualDays: totalDays,
|
|
1293
|
+
ratio: 1
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
if (hireDate > periodStart && hireDate <= periodEnd) {
|
|
1297
|
+
const actualDays = diffInDays(hireDate, periodEnd) + 1;
|
|
1298
|
+
const ratio = actualDays / totalDays;
|
|
1299
|
+
return {
|
|
1300
|
+
isProRated: true,
|
|
1301
|
+
totalDays,
|
|
1302
|
+
actualDays,
|
|
1303
|
+
ratio
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
return {
|
|
1307
|
+
isProRated: false,
|
|
1308
|
+
totalDays,
|
|
1309
|
+
actualDays: 0,
|
|
1310
|
+
ratio: 0
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
function applyTaxBrackets(amount, brackets) {
|
|
1314
|
+
let tax = 0;
|
|
1315
|
+
for (const bracket of brackets) {
|
|
1316
|
+
if (amount > bracket.min) {
|
|
1317
|
+
const taxableAmount = Math.min(amount, bracket.max) - bracket.min;
|
|
1318
|
+
tax += taxableAmount * bracket.rate;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
return Math.round(tax);
|
|
1322
|
+
}
|
|
1323
|
+
function pipe(...fns) {
|
|
1324
|
+
return (value) => fns.reduce((acc, fn) => fn(acc), value);
|
|
1325
|
+
}
|
|
1326
|
+
function compose(...fns) {
|
|
1327
|
+
return (value) => fns.reduceRight((acc, fn) => fn(acc), value);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// src/errors/index.ts
|
|
1331
|
+
var PayrollError = class _PayrollError extends Error {
|
|
1332
|
+
code;
|
|
1333
|
+
status;
|
|
1334
|
+
context;
|
|
1335
|
+
timestamp;
|
|
1336
|
+
constructor(message, code = "PAYROLL_ERROR", status = 500, context) {
|
|
1337
|
+
super(message);
|
|
1338
|
+
this.name = "PayrollError";
|
|
1339
|
+
this.code = code;
|
|
1340
|
+
this.status = status;
|
|
1341
|
+
this.context = context;
|
|
1342
|
+
this.timestamp = /* @__PURE__ */ new Date();
|
|
1343
|
+
if (Error.captureStackTrace) {
|
|
1344
|
+
Error.captureStackTrace(this, _PayrollError);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
toJSON() {
|
|
1348
|
+
return {
|
|
1349
|
+
name: this.name,
|
|
1350
|
+
code: this.code,
|
|
1351
|
+
message: this.message,
|
|
1352
|
+
status: this.status,
|
|
1353
|
+
context: this.context,
|
|
1354
|
+
timestamp: this.timestamp.toISOString()
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
var NotInitializedError = class extends PayrollError {
|
|
1359
|
+
constructor(message = "Payroll not initialized. Call Payroll.initialize() first.") {
|
|
1360
|
+
super(message, "NOT_INITIALIZED", 500);
|
|
1361
|
+
this.name = "NotInitializedError";
|
|
1362
|
+
}
|
|
1363
|
+
};
|
|
1364
|
+
var EmployeeNotFoundError = class extends PayrollError {
|
|
1365
|
+
constructor(employeeId, context) {
|
|
1366
|
+
super(
|
|
1367
|
+
employeeId ? `Employee not found: ${employeeId}` : "Employee not found",
|
|
1368
|
+
"EMPLOYEE_NOT_FOUND",
|
|
1369
|
+
404,
|
|
1370
|
+
context
|
|
1371
|
+
);
|
|
1372
|
+
this.name = "EmployeeNotFoundError";
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
var DuplicatePayrollError = class extends PayrollError {
|
|
1376
|
+
constructor(employeeId, month, year, context) {
|
|
1377
|
+
super(
|
|
1378
|
+
`Payroll already processed for employee ${employeeId} in ${month}/${year}`,
|
|
1379
|
+
"DUPLICATE_PAYROLL",
|
|
1380
|
+
409,
|
|
1381
|
+
{ employeeId, month, year, ...context }
|
|
1382
|
+
);
|
|
1383
|
+
this.name = "DuplicatePayrollError";
|
|
1384
|
+
}
|
|
1385
|
+
};
|
|
1386
|
+
var ValidationError = class extends PayrollError {
|
|
1387
|
+
errors;
|
|
1388
|
+
constructor(errors, context) {
|
|
1389
|
+
const errorArray = Array.isArray(errors) ? errors : [errors];
|
|
1390
|
+
super(errorArray.join(", "), "VALIDATION_ERROR", 400, context);
|
|
1391
|
+
this.name = "ValidationError";
|
|
1392
|
+
this.errors = errorArray;
|
|
1393
|
+
}
|
|
1394
|
+
};
|
|
1395
|
+
var EmployeeTerminatedError = class extends PayrollError {
|
|
1396
|
+
constructor(employeeId, context) {
|
|
1397
|
+
super(
|
|
1398
|
+
employeeId ? `Cannot perform operation on terminated employee: ${employeeId}` : "Cannot perform operation on terminated employee",
|
|
1399
|
+
"EMPLOYEE_TERMINATED",
|
|
1400
|
+
400,
|
|
1401
|
+
context
|
|
1402
|
+
);
|
|
1403
|
+
this.name = "EmployeeTerminatedError";
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
var NotEligibleError = class extends PayrollError {
|
|
1407
|
+
constructor(message, context) {
|
|
1408
|
+
super(message, "NOT_ELIGIBLE", 400, context);
|
|
1409
|
+
this.name = "NotEligibleError";
|
|
1410
|
+
}
|
|
1411
|
+
};
|
|
1412
|
+
|
|
1413
|
+
// src/payroll.ts
|
|
1414
|
+
function hasPluginMethod(obj, method) {
|
|
1415
|
+
return typeof obj === "object" && obj !== null && typeof obj[method] === "function";
|
|
1416
|
+
}
|
|
1417
|
+
function assertPluginMethod(obj, method, context) {
|
|
1418
|
+
if (!hasPluginMethod(obj, method)) {
|
|
1419
|
+
throw new Error(
|
|
1420
|
+
`Method '${method}' not found on employee. Did you forget to apply employeePlugin to your Employee schema? Context: ${context}`
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
function isEffectiveForPeriod(item, periodStart, periodEnd) {
|
|
1425
|
+
const effectiveFrom = item.effectiveFrom ? new Date(item.effectiveFrom) : /* @__PURE__ */ new Date(0);
|
|
1426
|
+
const effectiveTo = item.effectiveTo ? new Date(item.effectiveTo) : /* @__PURE__ */ new Date("2099-12-31");
|
|
1427
|
+
return effectiveFrom <= periodEnd && effectiveTo >= periodStart;
|
|
1428
|
+
}
|
|
1429
|
+
var Payroll = class _Payroll {
|
|
1430
|
+
_container;
|
|
1431
|
+
_events;
|
|
1432
|
+
_plugins = null;
|
|
1433
|
+
_initialized = false;
|
|
1434
|
+
constructor() {
|
|
1435
|
+
this._container = Container.getInstance();
|
|
1436
|
+
this._events = createEventBus();
|
|
1437
|
+
}
|
|
1438
|
+
// ========================================
|
|
1439
|
+
// Initialization
|
|
1440
|
+
// ========================================
|
|
1441
|
+
/**
|
|
1442
|
+
* Initialize Payroll with models and configuration
|
|
1443
|
+
*/
|
|
1444
|
+
initialize(config) {
|
|
1445
|
+
const { EmployeeModel, PayrollRecordModel, TransactionModel, AttendanceModel, singleTenant, logger: customLogger, config: customConfig } = config;
|
|
1446
|
+
if (!EmployeeModel || !PayrollRecordModel || !TransactionModel) {
|
|
1447
|
+
throw new Error("EmployeeModel, PayrollRecordModel, and TransactionModel are required");
|
|
1448
|
+
}
|
|
1449
|
+
if (customLogger) {
|
|
1450
|
+
setLogger(customLogger);
|
|
1451
|
+
}
|
|
1452
|
+
initializeContainer({
|
|
1453
|
+
models: {
|
|
1454
|
+
EmployeeModel,
|
|
1455
|
+
PayrollRecordModel,
|
|
1456
|
+
TransactionModel,
|
|
1457
|
+
AttendanceModel: AttendanceModel ?? null
|
|
1458
|
+
},
|
|
1459
|
+
config: customConfig,
|
|
1460
|
+
singleTenant: singleTenant ?? null,
|
|
1461
|
+
logger: customLogger
|
|
1462
|
+
});
|
|
1463
|
+
const pluginContext = {
|
|
1464
|
+
payroll: this,
|
|
1465
|
+
events: this._events,
|
|
1466
|
+
logger: getLogger(),
|
|
1467
|
+
getConfig: (key) => {
|
|
1468
|
+
const config2 = this._container.getConfig();
|
|
1469
|
+
return config2[key];
|
|
1470
|
+
},
|
|
1471
|
+
addHook: (event, handler) => this._events.on(event, handler)
|
|
1472
|
+
};
|
|
1473
|
+
this._plugins = new PluginManager(pluginContext);
|
|
1474
|
+
this._initialized = true;
|
|
1475
|
+
getLogger().info("Payroll initialized", {
|
|
1476
|
+
hasAttendanceIntegration: !!AttendanceModel,
|
|
1477
|
+
isSingleTenant: !!singleTenant
|
|
1478
|
+
});
|
|
1479
|
+
return this;
|
|
1480
|
+
}
|
|
1481
|
+
/**
|
|
1482
|
+
* Check if initialized
|
|
1483
|
+
*/
|
|
1484
|
+
isInitialized() {
|
|
1485
|
+
return this._initialized;
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Ensure initialized
|
|
1489
|
+
*/
|
|
1490
|
+
ensureInitialized() {
|
|
1491
|
+
if (!this._initialized) {
|
|
1492
|
+
throw new NotInitializedError();
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
/**
|
|
1496
|
+
* Get models
|
|
1497
|
+
*/
|
|
1498
|
+
get models() {
|
|
1499
|
+
this.ensureInitialized();
|
|
1500
|
+
return this._container.getModels();
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* Get config
|
|
1504
|
+
*/
|
|
1505
|
+
get config() {
|
|
1506
|
+
return this._container.getConfig();
|
|
1507
|
+
}
|
|
1508
|
+
// ========================================
|
|
1509
|
+
// Plugin System
|
|
1510
|
+
// ========================================
|
|
1511
|
+
/**
|
|
1512
|
+
* Register a plugin
|
|
1513
|
+
*/
|
|
1514
|
+
async use(plugin) {
|
|
1515
|
+
this.ensureInitialized();
|
|
1516
|
+
await this._plugins.register(plugin);
|
|
1517
|
+
return this;
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* Subscribe to events
|
|
1521
|
+
*/
|
|
1522
|
+
on(event, handler) {
|
|
1523
|
+
return this._events.on(event, handler);
|
|
1524
|
+
}
|
|
1525
|
+
// ========================================
|
|
1526
|
+
// Employment Lifecycle
|
|
1527
|
+
// ========================================
|
|
1528
|
+
/**
|
|
1529
|
+
* Hire a new employee
|
|
1530
|
+
*/
|
|
1531
|
+
async hire(params) {
|
|
1532
|
+
this.ensureInitialized();
|
|
1533
|
+
const { userId, employment, compensation, bankDetails, context } = params;
|
|
1534
|
+
const session = context?.session;
|
|
1535
|
+
const organizationId = params.organizationId ?? this._container.getOrganizationId();
|
|
1536
|
+
if (!organizationId) {
|
|
1537
|
+
throw new Error("organizationId is required (or configure single-tenant mode)");
|
|
1538
|
+
}
|
|
1539
|
+
const existingQuery = employee().forUser(userId).forOrganization(organizationId).employed().build();
|
|
1540
|
+
let existing = this.models.EmployeeModel.findOne(existingQuery);
|
|
1541
|
+
if (session) existing = existing.session(session);
|
|
1542
|
+
if (await existing) {
|
|
1543
|
+
throw new Error("User is already an active employee in this organization");
|
|
1544
|
+
}
|
|
1545
|
+
const employeeData = EmployeeFactory.create({
|
|
1546
|
+
userId,
|
|
1547
|
+
organizationId,
|
|
1548
|
+
employment,
|
|
1549
|
+
compensation: {
|
|
1550
|
+
...compensation,
|
|
1551
|
+
currency: compensation.currency || this.config.payroll.defaultCurrency
|
|
1552
|
+
},
|
|
1553
|
+
bankDetails
|
|
1554
|
+
});
|
|
1555
|
+
const [employee2] = await this.models.EmployeeModel.create([employeeData], { session });
|
|
1556
|
+
this._events.emitSync("employee:hired", {
|
|
1557
|
+
employee: {
|
|
1558
|
+
id: employee2._id,
|
|
1559
|
+
employeeId: employee2.employeeId,
|
|
1560
|
+
position: employee2.position,
|
|
1561
|
+
department: employee2.department
|
|
1562
|
+
},
|
|
1563
|
+
organizationId: employee2.organizationId,
|
|
1564
|
+
context
|
|
1565
|
+
});
|
|
1566
|
+
getLogger().info("Employee hired", {
|
|
1567
|
+
employeeId: employee2.employeeId,
|
|
1568
|
+
organizationId: organizationId.toString(),
|
|
1569
|
+
position: employment.position
|
|
1570
|
+
});
|
|
1571
|
+
return employee2;
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Update employment details
|
|
1575
|
+
* NOTE: Status changes to 'terminated' must use terminate() method
|
|
1576
|
+
*/
|
|
1577
|
+
async updateEmployment(params) {
|
|
1578
|
+
this.ensureInitialized();
|
|
1579
|
+
const { employeeId, updates, context } = params;
|
|
1580
|
+
const session = context?.session;
|
|
1581
|
+
let query = this.models.EmployeeModel.findById(toObjectId(employeeId));
|
|
1582
|
+
if (session) query = query.session(session);
|
|
1583
|
+
const employee2 = await query;
|
|
1584
|
+
if (!employee2) {
|
|
1585
|
+
throw new EmployeeNotFoundError(employeeId.toString());
|
|
1586
|
+
}
|
|
1587
|
+
if (employee2.status === "terminated") {
|
|
1588
|
+
throw new EmployeeTerminatedError(employee2.employeeId);
|
|
1589
|
+
}
|
|
1590
|
+
if (updates.status === "terminated") {
|
|
1591
|
+
throw new ValidationError(
|
|
1592
|
+
"Cannot set status to terminated directly. Use the terminate() method instead to ensure proper history tracking.",
|
|
1593
|
+
{ field: "status" }
|
|
1594
|
+
);
|
|
1595
|
+
}
|
|
1596
|
+
const allowedUpdates = ["department", "position", "employmentType", "status", "workSchedule"];
|
|
1597
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
1598
|
+
if (allowedUpdates.includes(key)) {
|
|
1599
|
+
employee2[key] = value;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
await employee2.save({ session });
|
|
1603
|
+
getLogger().info("Employee updated", {
|
|
1604
|
+
employeeId: employee2.employeeId,
|
|
1605
|
+
updates: Object.keys(updates)
|
|
1606
|
+
});
|
|
1607
|
+
return employee2;
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Terminate employee
|
|
1611
|
+
*/
|
|
1612
|
+
async terminate(params) {
|
|
1613
|
+
this.ensureInitialized();
|
|
1614
|
+
const { employeeId, terminationDate = /* @__PURE__ */ new Date(), reason = "resignation", notes, context } = params;
|
|
1615
|
+
const session = context?.session;
|
|
1616
|
+
let query = this.models.EmployeeModel.findById(toObjectId(employeeId));
|
|
1617
|
+
if (session) query = query.session(session);
|
|
1618
|
+
const employee2 = await query;
|
|
1619
|
+
if (!employee2) {
|
|
1620
|
+
throw new EmployeeNotFoundError(employeeId.toString());
|
|
1621
|
+
}
|
|
1622
|
+
assertPluginMethod(employee2, "terminate", "terminate()");
|
|
1623
|
+
employee2.terminate(reason, terminationDate);
|
|
1624
|
+
if (notes) {
|
|
1625
|
+
employee2.notes = (employee2.notes || "") + `
|
|
1626
|
+
Termination: ${notes}`;
|
|
1627
|
+
}
|
|
1628
|
+
await employee2.save({ session });
|
|
1629
|
+
this._events.emitSync("employee:terminated", {
|
|
1630
|
+
employee: {
|
|
1631
|
+
id: employee2._id,
|
|
1632
|
+
employeeId: employee2.employeeId
|
|
1633
|
+
},
|
|
1634
|
+
terminationDate,
|
|
1635
|
+
reason,
|
|
1636
|
+
organizationId: employee2.organizationId,
|
|
1637
|
+
context
|
|
1638
|
+
});
|
|
1639
|
+
getLogger().info("Employee terminated", {
|
|
1640
|
+
employeeId: employee2.employeeId,
|
|
1641
|
+
reason
|
|
1642
|
+
});
|
|
1643
|
+
return employee2;
|
|
1644
|
+
}
|
|
1645
|
+
/**
|
|
1646
|
+
* Re-hire terminated employee
|
|
1647
|
+
*/
|
|
1648
|
+
async reHire(params) {
|
|
1649
|
+
this.ensureInitialized();
|
|
1650
|
+
const { employeeId, hireDate = /* @__PURE__ */ new Date(), position, department, compensation, context } = params;
|
|
1651
|
+
const session = context?.session;
|
|
1652
|
+
if (!this.config.employment.allowReHiring) {
|
|
1653
|
+
throw new Error("Re-hiring is not enabled");
|
|
1654
|
+
}
|
|
1655
|
+
let query = this.models.EmployeeModel.findById(toObjectId(employeeId));
|
|
1656
|
+
if (session) query = query.session(session);
|
|
1657
|
+
const employee2 = await query;
|
|
1658
|
+
if (!employee2) {
|
|
1659
|
+
throw new EmployeeNotFoundError(employeeId.toString());
|
|
1660
|
+
}
|
|
1661
|
+
assertPluginMethod(employee2, "reHire", "reHire()");
|
|
1662
|
+
employee2.reHire(hireDate, position, department);
|
|
1663
|
+
if (compensation) {
|
|
1664
|
+
employee2.compensation = { ...employee2.compensation, ...compensation };
|
|
1665
|
+
}
|
|
1666
|
+
await employee2.save({ session });
|
|
1667
|
+
this._events.emitSync("employee:rehired", {
|
|
1668
|
+
employee: {
|
|
1669
|
+
id: employee2._id,
|
|
1670
|
+
employeeId: employee2.employeeId,
|
|
1671
|
+
position: employee2.position
|
|
1672
|
+
},
|
|
1673
|
+
organizationId: employee2.organizationId,
|
|
1674
|
+
context
|
|
1675
|
+
});
|
|
1676
|
+
getLogger().info("Employee re-hired", {
|
|
1677
|
+
employeeId: employee2.employeeId
|
|
1678
|
+
});
|
|
1679
|
+
return employee2;
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* Get employee by ID
|
|
1683
|
+
*/
|
|
1684
|
+
async getEmployee(params) {
|
|
1685
|
+
this.ensureInitialized();
|
|
1686
|
+
const { employeeId, populateUser = true, session } = params;
|
|
1687
|
+
let query = this.models.EmployeeModel.findById(toObjectId(employeeId));
|
|
1688
|
+
if (session) query = query.session(session);
|
|
1689
|
+
if (populateUser) query = query.populate("userId", "name email phone");
|
|
1690
|
+
const employee2 = await query;
|
|
1691
|
+
if (!employee2) {
|
|
1692
|
+
throw new EmployeeNotFoundError(employeeId.toString());
|
|
1693
|
+
}
|
|
1694
|
+
return employee2;
|
|
1695
|
+
}
|
|
1696
|
+
/**
|
|
1697
|
+
* List employees
|
|
1698
|
+
*/
|
|
1699
|
+
async listEmployees(params) {
|
|
1700
|
+
this.ensureInitialized();
|
|
1701
|
+
const { organizationId, filters = {}, pagination = {} } = params;
|
|
1702
|
+
let queryBuilder = employee().forOrganization(organizationId);
|
|
1703
|
+
if (filters.status) queryBuilder = queryBuilder.withStatus(filters.status);
|
|
1704
|
+
if (filters.department) queryBuilder = queryBuilder.inDepartment(filters.department);
|
|
1705
|
+
if (filters.employmentType) queryBuilder = queryBuilder.withEmploymentType(filters.employmentType);
|
|
1706
|
+
if (filters.minSalary) queryBuilder = queryBuilder.withMinSalary(filters.minSalary);
|
|
1707
|
+
if (filters.maxSalary) queryBuilder = queryBuilder.withMaxSalary(filters.maxSalary);
|
|
1708
|
+
const query = queryBuilder.build();
|
|
1709
|
+
const page = pagination.page || 1;
|
|
1710
|
+
const limit = pagination.limit || 20;
|
|
1711
|
+
const sort = pagination.sort || { createdAt: -1 };
|
|
1712
|
+
const [docs, totalDocs] = await Promise.all([
|
|
1713
|
+
this.models.EmployeeModel.find(query).populate("userId", "name email phone").sort(sort).skip((page - 1) * limit).limit(limit),
|
|
1714
|
+
this.models.EmployeeModel.countDocuments(query)
|
|
1715
|
+
]);
|
|
1716
|
+
return { docs, totalDocs, page, limit };
|
|
1717
|
+
}
|
|
1718
|
+
// ========================================
|
|
1719
|
+
// Compensation Management
|
|
1720
|
+
// ========================================
|
|
1721
|
+
/**
|
|
1722
|
+
* Update employee salary
|
|
1723
|
+
*/
|
|
1724
|
+
async updateSalary(params) {
|
|
1725
|
+
this.ensureInitialized();
|
|
1726
|
+
const { employeeId, compensation, effectiveFrom = /* @__PURE__ */ new Date(), context } = params;
|
|
1727
|
+
const session = context?.session;
|
|
1728
|
+
let query = this.models.EmployeeModel.findById(toObjectId(employeeId));
|
|
1729
|
+
if (session) query = query.session(session);
|
|
1730
|
+
const employee2 = await query;
|
|
1731
|
+
if (!employee2) {
|
|
1732
|
+
throw new EmployeeNotFoundError(employeeId.toString());
|
|
1733
|
+
}
|
|
1734
|
+
if (employee2.status === "terminated") {
|
|
1735
|
+
throw new EmployeeTerminatedError(employee2.employeeId);
|
|
1736
|
+
}
|
|
1737
|
+
const oldSalary = employee2.compensation.netSalary;
|
|
1738
|
+
if (compensation.baseAmount !== void 0) {
|
|
1739
|
+
employee2.compensation.baseAmount = compensation.baseAmount;
|
|
1740
|
+
}
|
|
1741
|
+
if (compensation.frequency) {
|
|
1742
|
+
employee2.compensation.frequency = compensation.frequency;
|
|
1743
|
+
}
|
|
1744
|
+
if (compensation.currency) {
|
|
1745
|
+
employee2.compensation.currency = compensation.currency;
|
|
1746
|
+
}
|
|
1747
|
+
employee2.compensation.effectiveFrom = effectiveFrom;
|
|
1748
|
+
if (hasPluginMethod(employee2, "updateSalaryCalculations")) {
|
|
1749
|
+
employee2.updateSalaryCalculations();
|
|
1750
|
+
}
|
|
1751
|
+
await employee2.save({ session });
|
|
1752
|
+
this._events.emitSync("salary:updated", {
|
|
1753
|
+
employee: { id: employee2._id, employeeId: employee2.employeeId },
|
|
1754
|
+
previousSalary: oldSalary || 0,
|
|
1755
|
+
newSalary: employee2.compensation.netSalary || 0,
|
|
1756
|
+
effectiveFrom,
|
|
1757
|
+
organizationId: employee2.organizationId,
|
|
1758
|
+
context
|
|
1759
|
+
});
|
|
1760
|
+
getLogger().info("Salary updated", {
|
|
1761
|
+
employeeId: employee2.employeeId,
|
|
1762
|
+
oldSalary,
|
|
1763
|
+
newSalary: employee2.compensation.netSalary
|
|
1764
|
+
});
|
|
1765
|
+
return employee2;
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* Add allowance to employee
|
|
1769
|
+
*/
|
|
1770
|
+
async addAllowance(params) {
|
|
1771
|
+
this.ensureInitialized();
|
|
1772
|
+
const { employeeId, type, amount, taxable = true, recurring = true, effectiveFrom = /* @__PURE__ */ new Date(), effectiveTo, context } = params;
|
|
1773
|
+
const session = context?.session;
|
|
1774
|
+
let query = this.models.EmployeeModel.findById(toObjectId(employeeId));
|
|
1775
|
+
if (session) query = query.session(session);
|
|
1776
|
+
const employee2 = await query;
|
|
1777
|
+
if (!employee2) {
|
|
1778
|
+
throw new EmployeeNotFoundError(employeeId.toString());
|
|
1779
|
+
}
|
|
1780
|
+
if (employee2.status === "terminated") {
|
|
1781
|
+
throw new EmployeeTerminatedError(employee2.employeeId);
|
|
1782
|
+
}
|
|
1783
|
+
if (!employee2.compensation.allowances) {
|
|
1784
|
+
employee2.compensation.allowances = [];
|
|
1785
|
+
}
|
|
1786
|
+
employee2.compensation.allowances.push({
|
|
1787
|
+
type,
|
|
1788
|
+
name: type,
|
|
1789
|
+
amount,
|
|
1790
|
+
taxable,
|
|
1791
|
+
recurring,
|
|
1792
|
+
effectiveFrom,
|
|
1793
|
+
effectiveTo
|
|
1794
|
+
});
|
|
1795
|
+
if (hasPluginMethod(employee2, "updateSalaryCalculations")) {
|
|
1796
|
+
employee2.updateSalaryCalculations();
|
|
1797
|
+
}
|
|
1798
|
+
await employee2.save({ session });
|
|
1799
|
+
getLogger().info("Allowance added", {
|
|
1800
|
+
employeeId: employee2.employeeId,
|
|
1801
|
+
type,
|
|
1802
|
+
amount
|
|
1803
|
+
});
|
|
1804
|
+
return employee2;
|
|
1805
|
+
}
|
|
1806
|
+
/**
|
|
1807
|
+
* Remove allowance from employee
|
|
1808
|
+
*/
|
|
1809
|
+
async removeAllowance(params) {
|
|
1810
|
+
this.ensureInitialized();
|
|
1811
|
+
const { employeeId, type, context } = params;
|
|
1812
|
+
const session = context?.session;
|
|
1813
|
+
let query = this.models.EmployeeModel.findById(toObjectId(employeeId));
|
|
1814
|
+
if (session) query = query.session(session);
|
|
1815
|
+
const employee2 = await query;
|
|
1816
|
+
if (!employee2) {
|
|
1817
|
+
throw new EmployeeNotFoundError(employeeId.toString());
|
|
1818
|
+
}
|
|
1819
|
+
const before = employee2.compensation.allowances?.length || 0;
|
|
1820
|
+
if (hasPluginMethod(employee2, "removeAllowance")) {
|
|
1821
|
+
employee2.removeAllowance(type);
|
|
1822
|
+
} else {
|
|
1823
|
+
if (employee2.compensation.allowances) {
|
|
1824
|
+
employee2.compensation.allowances = employee2.compensation.allowances.filter(
|
|
1825
|
+
(a) => a.type !== type
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
const after = employee2.compensation.allowances?.length || 0;
|
|
1830
|
+
if (before === after) {
|
|
1831
|
+
throw new Error(`Allowance type '${type}' not found`);
|
|
1832
|
+
}
|
|
1833
|
+
await employee2.save({ session });
|
|
1834
|
+
getLogger().info("Allowance removed", {
|
|
1835
|
+
employeeId: employee2.employeeId,
|
|
1836
|
+
type
|
|
1837
|
+
});
|
|
1838
|
+
return employee2;
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* Add deduction to employee
|
|
1842
|
+
*/
|
|
1843
|
+
async addDeduction(params) {
|
|
1844
|
+
this.ensureInitialized();
|
|
1845
|
+
const { employeeId, type, amount, auto = false, recurring = true, description, effectiveFrom = /* @__PURE__ */ new Date(), effectiveTo, context } = params;
|
|
1846
|
+
const session = context?.session;
|
|
1847
|
+
let query = this.models.EmployeeModel.findById(toObjectId(employeeId));
|
|
1848
|
+
if (session) query = query.session(session);
|
|
1849
|
+
const employee2 = await query;
|
|
1850
|
+
if (!employee2) {
|
|
1851
|
+
throw new EmployeeNotFoundError(employeeId.toString());
|
|
1852
|
+
}
|
|
1853
|
+
if (employee2.status === "terminated") {
|
|
1854
|
+
throw new EmployeeTerminatedError(employee2.employeeId);
|
|
1855
|
+
}
|
|
1856
|
+
if (!employee2.compensation.deductions) {
|
|
1857
|
+
employee2.compensation.deductions = [];
|
|
1858
|
+
}
|
|
1859
|
+
employee2.compensation.deductions.push({
|
|
1860
|
+
type,
|
|
1861
|
+
name: type,
|
|
1862
|
+
amount,
|
|
1863
|
+
auto,
|
|
1864
|
+
recurring,
|
|
1865
|
+
description,
|
|
1866
|
+
effectiveFrom,
|
|
1867
|
+
effectiveTo
|
|
1868
|
+
});
|
|
1869
|
+
if (hasPluginMethod(employee2, "updateSalaryCalculations")) {
|
|
1870
|
+
employee2.updateSalaryCalculations();
|
|
1871
|
+
}
|
|
1872
|
+
await employee2.save({ session });
|
|
1873
|
+
getLogger().info("Deduction added", {
|
|
1874
|
+
employeeId: employee2.employeeId,
|
|
1875
|
+
type,
|
|
1876
|
+
amount,
|
|
1877
|
+
auto
|
|
1878
|
+
});
|
|
1879
|
+
return employee2;
|
|
1880
|
+
}
|
|
1881
|
+
/**
|
|
1882
|
+
* Remove deduction from employee
|
|
1883
|
+
*/
|
|
1884
|
+
async removeDeduction(params) {
|
|
1885
|
+
this.ensureInitialized();
|
|
1886
|
+
const { employeeId, type, context } = params;
|
|
1887
|
+
const session = context?.session;
|
|
1888
|
+
let query = this.models.EmployeeModel.findById(toObjectId(employeeId));
|
|
1889
|
+
if (session) query = query.session(session);
|
|
1890
|
+
const employee2 = await query;
|
|
1891
|
+
if (!employee2) {
|
|
1892
|
+
throw new EmployeeNotFoundError(employeeId.toString());
|
|
1893
|
+
}
|
|
1894
|
+
const before = employee2.compensation.deductions?.length || 0;
|
|
1895
|
+
if (hasPluginMethod(employee2, "removeDeduction")) {
|
|
1896
|
+
employee2.removeDeduction(type);
|
|
1897
|
+
} else {
|
|
1898
|
+
if (employee2.compensation.deductions) {
|
|
1899
|
+
employee2.compensation.deductions = employee2.compensation.deductions.filter(
|
|
1900
|
+
(d) => d.type !== type
|
|
1901
|
+
);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
const after = employee2.compensation.deductions?.length || 0;
|
|
1905
|
+
if (before === after) {
|
|
1906
|
+
throw new Error(`Deduction type '${type}' not found`);
|
|
1907
|
+
}
|
|
1908
|
+
await employee2.save({ session });
|
|
1909
|
+
getLogger().info("Deduction removed", {
|
|
1910
|
+
employeeId: employee2.employeeId,
|
|
1911
|
+
type
|
|
1912
|
+
});
|
|
1913
|
+
return employee2;
|
|
1914
|
+
}
|
|
1915
|
+
/**
|
|
1916
|
+
* Update bank details
|
|
1917
|
+
*/
|
|
1918
|
+
async updateBankDetails(params) {
|
|
1919
|
+
this.ensureInitialized();
|
|
1920
|
+
const { employeeId, bankDetails, context } = params;
|
|
1921
|
+
const session = context?.session;
|
|
1922
|
+
let query = this.models.EmployeeModel.findById(toObjectId(employeeId));
|
|
1923
|
+
if (session) query = query.session(session);
|
|
1924
|
+
const employee2 = await query;
|
|
1925
|
+
if (!employee2) {
|
|
1926
|
+
throw new EmployeeNotFoundError(employeeId.toString());
|
|
1927
|
+
}
|
|
1928
|
+
employee2.bankDetails = { ...employee2.bankDetails, ...bankDetails };
|
|
1929
|
+
await employee2.save({ session });
|
|
1930
|
+
getLogger().info("Bank details updated", {
|
|
1931
|
+
employeeId: employee2.employeeId
|
|
1932
|
+
});
|
|
1933
|
+
return employee2;
|
|
1934
|
+
}
|
|
1935
|
+
// ========================================
|
|
1936
|
+
// Payroll Processing
|
|
1937
|
+
// ========================================
|
|
1938
|
+
/**
|
|
1939
|
+
* Process salary for single employee
|
|
1940
|
+
*
|
|
1941
|
+
* ATOMICITY: This method creates its own transaction if none provided.
|
|
1942
|
+
* All database operations (PayrollRecord, Transaction, Employee stats)
|
|
1943
|
+
* are atomic - either all succeed or all fail.
|
|
1944
|
+
*/
|
|
1945
|
+
async processSalary(params) {
|
|
1946
|
+
this.ensureInitialized();
|
|
1947
|
+
const { employeeId, month, year, paymentDate = /* @__PURE__ */ new Date(), paymentMethod = "bank", context } = params;
|
|
1948
|
+
const providedSession = context?.session;
|
|
1949
|
+
const session = providedSession || await mongoose2.startSession();
|
|
1950
|
+
const shouldManageTransaction = !providedSession && session != null;
|
|
1951
|
+
try {
|
|
1952
|
+
if (shouldManageTransaction) {
|
|
1953
|
+
await session.startTransaction();
|
|
1954
|
+
}
|
|
1955
|
+
let query = this.models.EmployeeModel.findById(toObjectId(employeeId)).populate("userId", "name email");
|
|
1956
|
+
if (session) query = query.session(session);
|
|
1957
|
+
const employee2 = await query;
|
|
1958
|
+
if (!employee2) {
|
|
1959
|
+
throw new EmployeeNotFoundError(employeeId.toString());
|
|
1960
|
+
}
|
|
1961
|
+
const canReceive = hasPluginMethod(employee2, "canReceiveSalary") ? employee2.canReceiveSalary() : employee2.status === "active" && (employee2.compensation?.baseAmount || 0) > 0;
|
|
1962
|
+
if (!canReceive) {
|
|
1963
|
+
throw new NotEligibleError("Employee is not eligible to receive salary");
|
|
1964
|
+
}
|
|
1965
|
+
const existingQuery = payroll().forEmployee(employeeId).forPeriod(month, year).whereIn("status", ["paid", "processing"]).build();
|
|
1966
|
+
let existingRecordQuery = this.models.PayrollRecordModel.findOne(existingQuery);
|
|
1967
|
+
if (session) existingRecordQuery = existingRecordQuery.session(session);
|
|
1968
|
+
const existingRecord = await existingRecordQuery;
|
|
1969
|
+
if (existingRecord) {
|
|
1970
|
+
throw new DuplicatePayrollError(employee2.employeeId, month, year);
|
|
1971
|
+
}
|
|
1972
|
+
const period = { ...getPayPeriod(month, year), payDate: paymentDate };
|
|
1973
|
+
const breakdown = await this.calculateSalaryBreakdown(employee2, period, session);
|
|
1974
|
+
const userIdValue = employee2.userId ? typeof employee2.userId === "object" && "_id" in employee2.userId ? employee2.userId._id : employee2.userId : void 0;
|
|
1975
|
+
const [payrollRecord] = await this.models.PayrollRecordModel.create([{
|
|
1976
|
+
organizationId: employee2.organizationId,
|
|
1977
|
+
employeeId: employee2._id,
|
|
1978
|
+
userId: userIdValue,
|
|
1979
|
+
period,
|
|
1980
|
+
breakdown,
|
|
1981
|
+
status: "processing",
|
|
1982
|
+
paymentMethod,
|
|
1983
|
+
processedAt: /* @__PURE__ */ new Date(),
|
|
1984
|
+
processedBy: context?.userId ? toObjectId(context.userId) : void 0
|
|
1985
|
+
}], session ? { session } : {});
|
|
1986
|
+
const [transaction] = await this.models.TransactionModel.create([{
|
|
1987
|
+
organizationId: employee2.organizationId,
|
|
1988
|
+
type: "expense",
|
|
1989
|
+
category: HRM_TRANSACTION_CATEGORIES.SALARY,
|
|
1990
|
+
amount: breakdown.netSalary,
|
|
1991
|
+
method: paymentMethod,
|
|
1992
|
+
status: "completed",
|
|
1993
|
+
date: paymentDate,
|
|
1994
|
+
referenceId: employee2._id,
|
|
1995
|
+
referenceModel: "Employee",
|
|
1996
|
+
handledBy: context?.userId ? toObjectId(context.userId) : void 0,
|
|
1997
|
+
notes: `Salary payment - ${employee2.userId?.name || employee2.employeeId} (${month}/${year})`,
|
|
1998
|
+
metadata: {
|
|
1999
|
+
employeeId: employee2.employeeId,
|
|
2000
|
+
payrollRecordId: payrollRecord._id,
|
|
2001
|
+
period: { month, year },
|
|
2002
|
+
breakdown: {
|
|
2003
|
+
base: breakdown.baseAmount,
|
|
2004
|
+
allowances: sumAllowances(breakdown.allowances),
|
|
2005
|
+
deductions: sumDeductions(breakdown.deductions),
|
|
2006
|
+
tax: breakdown.taxAmount || 0,
|
|
2007
|
+
gross: breakdown.grossSalary,
|
|
2008
|
+
net: breakdown.netSalary
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
}], session ? { session } : {});
|
|
2012
|
+
payrollRecord.transactionId = transaction._id;
|
|
2013
|
+
payrollRecord.status = "paid";
|
|
2014
|
+
payrollRecord.paidAt = paymentDate;
|
|
2015
|
+
await payrollRecord.save(session ? { session } : {});
|
|
2016
|
+
await this.updatePayrollStats(employee2, breakdown.netSalary, paymentDate, session);
|
|
2017
|
+
if (shouldManageTransaction) {
|
|
2018
|
+
await session.commitTransaction();
|
|
2019
|
+
}
|
|
2020
|
+
this._events.emitSync("salary:processed", {
|
|
2021
|
+
employee: {
|
|
2022
|
+
id: employee2._id,
|
|
2023
|
+
employeeId: employee2.employeeId,
|
|
2024
|
+
name: employee2.userId?.name
|
|
2025
|
+
},
|
|
2026
|
+
payroll: {
|
|
2027
|
+
id: payrollRecord._id,
|
|
2028
|
+
period: { month, year },
|
|
2029
|
+
grossAmount: breakdown.grossSalary,
|
|
2030
|
+
netAmount: breakdown.netSalary
|
|
2031
|
+
},
|
|
2032
|
+
transactionId: transaction._id,
|
|
2033
|
+
organizationId: employee2.organizationId,
|
|
2034
|
+
context
|
|
2035
|
+
});
|
|
2036
|
+
getLogger().info("Salary processed", {
|
|
2037
|
+
employeeId: employee2.employeeId,
|
|
2038
|
+
month,
|
|
2039
|
+
year,
|
|
2040
|
+
amount: breakdown.netSalary
|
|
2041
|
+
});
|
|
2042
|
+
return { payrollRecord, transaction, employee: employee2 };
|
|
2043
|
+
} catch (error) {
|
|
2044
|
+
if (shouldManageTransaction && session?.inTransaction()) {
|
|
2045
|
+
await session.abortTransaction();
|
|
2046
|
+
}
|
|
2047
|
+
throw error;
|
|
2048
|
+
} finally {
|
|
2049
|
+
if (shouldManageTransaction && session) {
|
|
2050
|
+
await session.endSession();
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
/**
|
|
2055
|
+
* Process bulk payroll
|
|
2056
|
+
*
|
|
2057
|
+
* ATOMICITY STRATEGY: Each employee is processed in its own transaction.
|
|
2058
|
+
* This allows partial success - some employees can succeed while others fail.
|
|
2059
|
+
* Failed employees don't affect successful ones.
|
|
2060
|
+
*/
|
|
2061
|
+
async processBulkPayroll(params) {
|
|
2062
|
+
this.ensureInitialized();
|
|
2063
|
+
const { organizationId, month, year, employeeIds = [], paymentDate = /* @__PURE__ */ new Date(), paymentMethod = "bank", context } = params;
|
|
2064
|
+
const query = { organizationId: toObjectId(organizationId), status: "active" };
|
|
2065
|
+
if (employeeIds.length > 0) {
|
|
2066
|
+
query._id = { $in: employeeIds.map(toObjectId) };
|
|
2067
|
+
}
|
|
2068
|
+
const employees = await this.models.EmployeeModel.find(query);
|
|
2069
|
+
const results = {
|
|
2070
|
+
successful: [],
|
|
2071
|
+
failed: [],
|
|
2072
|
+
total: employees.length
|
|
2073
|
+
};
|
|
2074
|
+
for (const employee2 of employees) {
|
|
2075
|
+
try {
|
|
2076
|
+
const result = await this.processSalary({
|
|
2077
|
+
employeeId: employee2._id,
|
|
2078
|
+
month,
|
|
2079
|
+
year,
|
|
2080
|
+
paymentDate,
|
|
2081
|
+
paymentMethod,
|
|
2082
|
+
context: { ...context, session: void 0 }
|
|
2083
|
+
// Don't pass session - let processSalary create its own
|
|
2084
|
+
});
|
|
2085
|
+
results.successful.push({
|
|
2086
|
+
employeeId: employee2.employeeId,
|
|
2087
|
+
amount: result.payrollRecord.breakdown.netSalary,
|
|
2088
|
+
transactionId: result.transaction._id
|
|
2089
|
+
});
|
|
2090
|
+
} catch (error) {
|
|
2091
|
+
results.failed.push({
|
|
2092
|
+
employeeId: employee2.employeeId,
|
|
2093
|
+
error: error.message
|
|
2094
|
+
});
|
|
2095
|
+
getLogger().error("Failed to process salary", {
|
|
2096
|
+
employeeId: employee2.employeeId,
|
|
2097
|
+
error: error.message
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
this._events.emitSync("payroll:completed", {
|
|
2102
|
+
organizationId: toObjectId(organizationId),
|
|
2103
|
+
period: { month, year },
|
|
2104
|
+
summary: {
|
|
2105
|
+
total: results.total,
|
|
2106
|
+
successful: results.successful.length,
|
|
2107
|
+
failed: results.failed.length,
|
|
2108
|
+
totalAmount: results.successful.reduce((sum2, r) => sum2 + r.amount, 0)
|
|
2109
|
+
},
|
|
2110
|
+
context
|
|
2111
|
+
});
|
|
2112
|
+
getLogger().info("Bulk payroll processed", {
|
|
2113
|
+
organizationId: organizationId.toString(),
|
|
2114
|
+
month,
|
|
2115
|
+
year,
|
|
2116
|
+
total: results.total,
|
|
2117
|
+
successful: results.successful.length,
|
|
2118
|
+
failed: results.failed.length
|
|
2119
|
+
});
|
|
2120
|
+
return results;
|
|
2121
|
+
}
|
|
2122
|
+
/**
|
|
2123
|
+
* Get payroll history
|
|
2124
|
+
*/
|
|
2125
|
+
async payrollHistory(params) {
|
|
2126
|
+
this.ensureInitialized();
|
|
2127
|
+
const { employeeId, organizationId, month, year, status, pagination = {} } = params;
|
|
2128
|
+
let queryBuilder = payroll();
|
|
2129
|
+
if (employeeId) queryBuilder = queryBuilder.forEmployee(employeeId);
|
|
2130
|
+
if (organizationId) queryBuilder = queryBuilder.forOrganization(organizationId);
|
|
2131
|
+
if (month || year) queryBuilder = queryBuilder.forPeriod(month, year);
|
|
2132
|
+
if (status) queryBuilder = queryBuilder.withStatus(status);
|
|
2133
|
+
const query = queryBuilder.build();
|
|
2134
|
+
const page = pagination.page || 1;
|
|
2135
|
+
const limit = pagination.limit || 20;
|
|
2136
|
+
const sort = pagination.sort || { "period.year": -1, "period.month": -1 };
|
|
2137
|
+
return this.models.PayrollRecordModel.find(query).populate("employeeId", "employeeId position department").populate("userId", "name email").populate("transactionId", "amount method status date").sort(sort).skip((page - 1) * limit).limit(limit);
|
|
2138
|
+
}
|
|
2139
|
+
/**
|
|
2140
|
+
* Get payroll summary
|
|
2141
|
+
*/
|
|
2142
|
+
async payrollSummary(params) {
|
|
2143
|
+
this.ensureInitialized();
|
|
2144
|
+
const { organizationId, month, year } = params;
|
|
2145
|
+
const query = { organizationId: toObjectId(organizationId) };
|
|
2146
|
+
if (month) query["period.month"] = month;
|
|
2147
|
+
if (year) query["period.year"] = year;
|
|
2148
|
+
const [summary] = await this.models.PayrollRecordModel.aggregate([
|
|
2149
|
+
{ $match: query },
|
|
2150
|
+
{
|
|
2151
|
+
$group: {
|
|
2152
|
+
_id: null,
|
|
2153
|
+
totalGross: { $sum: "$breakdown.grossSalary" },
|
|
2154
|
+
totalNet: { $sum: "$breakdown.netSalary" },
|
|
2155
|
+
totalDeductions: { $sum: { $sum: "$breakdown.deductions.amount" } },
|
|
2156
|
+
totalTax: { $sum: { $ifNull: ["$breakdown.taxAmount", 0] } },
|
|
2157
|
+
employeeCount: { $sum: 1 },
|
|
2158
|
+
paidCount: { $sum: { $cond: [{ $eq: ["$status", "paid"] }, 1, 0] } },
|
|
2159
|
+
pendingCount: { $sum: { $cond: [{ $eq: ["$status", "pending"] }, 1, 0] } }
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
]);
|
|
2163
|
+
return summary || {
|
|
2164
|
+
totalGross: 0,
|
|
2165
|
+
totalNet: 0,
|
|
2166
|
+
totalDeductions: 0,
|
|
2167
|
+
totalTax: 0,
|
|
2168
|
+
employeeCount: 0,
|
|
2169
|
+
paidCount: 0,
|
|
2170
|
+
pendingCount: 0
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
/**
|
|
2174
|
+
* Export payroll data
|
|
2175
|
+
*/
|
|
2176
|
+
async exportPayroll(params) {
|
|
2177
|
+
this.ensureInitialized();
|
|
2178
|
+
const { organizationId, startDate, endDate } = params;
|
|
2179
|
+
const query = {
|
|
2180
|
+
organizationId: toObjectId(organizationId),
|
|
2181
|
+
"period.payDate": { $gte: startDate, $lte: endDate }
|
|
2182
|
+
};
|
|
2183
|
+
const records = await this.models.PayrollRecordModel.find(query).populate("employeeId", "employeeId position department").populate("userId", "name email").populate("transactionId", "amount method status date").sort({ "period.year": -1, "period.month": -1 });
|
|
2184
|
+
await this.models.PayrollRecordModel.updateMany(query, {
|
|
2185
|
+
exported: true,
|
|
2186
|
+
exportedAt: /* @__PURE__ */ new Date()
|
|
2187
|
+
});
|
|
2188
|
+
this._events.emitSync("payroll:exported", {
|
|
2189
|
+
organizationId: toObjectId(organizationId),
|
|
2190
|
+
dateRange: { start: startDate, end: endDate },
|
|
2191
|
+
recordCount: records.length,
|
|
2192
|
+
format: "json"
|
|
2193
|
+
});
|
|
2194
|
+
getLogger().info("Payroll data exported", {
|
|
2195
|
+
organizationId: organizationId.toString(),
|
|
2196
|
+
count: records.length
|
|
2197
|
+
});
|
|
2198
|
+
return records;
|
|
2199
|
+
}
|
|
2200
|
+
// ========================================
|
|
2201
|
+
// Helper Methods
|
|
2202
|
+
// ========================================
|
|
2203
|
+
/**
|
|
2204
|
+
* Calculate salary breakdown with proper handling for:
|
|
2205
|
+
* - Effective dates on allowances/deductions
|
|
2206
|
+
* - Pro-rating for mid-period hires AND terminations
|
|
2207
|
+
* - Tax calculation
|
|
2208
|
+
* - Working days vs calendar days for attendance
|
|
2209
|
+
*/
|
|
2210
|
+
async calculateSalaryBreakdown(employee2, period, session) {
|
|
2211
|
+
const comp = employee2.compensation;
|
|
2212
|
+
let baseAmount = comp.baseAmount;
|
|
2213
|
+
const workingDaysInMonth = getWorkingDaysInMonth(period.year, period.month);
|
|
2214
|
+
const proRating = this.calculateProRatingAdvanced(
|
|
2215
|
+
employee2.hireDate,
|
|
2216
|
+
employee2.terminationDate || null,
|
|
2217
|
+
period.startDate,
|
|
2218
|
+
period.endDate,
|
|
2219
|
+
workingDaysInMonth
|
|
2220
|
+
);
|
|
2221
|
+
if (proRating.isProRated && this.config.payroll.allowProRating) {
|
|
2222
|
+
baseAmount = Math.round(baseAmount * proRating.ratio);
|
|
2223
|
+
}
|
|
2224
|
+
const effectiveAllowances = (comp.allowances || []).filter((a) => isEffectiveForPeriod(a, period.startDate, period.endDate)).filter((a) => a.recurring !== false);
|
|
2225
|
+
const effectiveDeductions = (comp.deductions || []).filter((d) => isEffectiveForPeriod(d, period.startDate, period.endDate)).filter((d) => d.auto || d.recurring);
|
|
2226
|
+
const allowances = effectiveAllowances.map((a) => ({
|
|
2227
|
+
type: a.type,
|
|
2228
|
+
amount: proRating.isProRated && this.config.payroll.allowProRating ? Math.round(a.amount * proRating.ratio) : a.amount,
|
|
2229
|
+
taxable: a.taxable ?? true
|
|
2230
|
+
}));
|
|
2231
|
+
const deductions = effectiveDeductions.map((d) => ({
|
|
2232
|
+
type: d.type,
|
|
2233
|
+
amount: proRating.isProRated && this.config.payroll.allowProRating ? Math.round(d.amount * proRating.ratio) : d.amount,
|
|
2234
|
+
description: d.description
|
|
2235
|
+
}));
|
|
2236
|
+
let attendanceDeduction = 0;
|
|
2237
|
+
if (this.models.AttendanceModel && this.config.payroll.attendanceIntegration) {
|
|
2238
|
+
attendanceDeduction = await this.calculateAttendanceDeduction(
|
|
2239
|
+
employee2._id,
|
|
2240
|
+
employee2.organizationId,
|
|
2241
|
+
period,
|
|
2242
|
+
baseAmount / proRating.workingDays,
|
|
2243
|
+
// Daily rate based on working days
|
|
2244
|
+
proRating.workingDays,
|
|
2245
|
+
session
|
|
2246
|
+
);
|
|
2247
|
+
}
|
|
2248
|
+
if (attendanceDeduction > 0) {
|
|
2249
|
+
deductions.push({
|
|
2250
|
+
type: "absence",
|
|
2251
|
+
amount: attendanceDeduction,
|
|
2252
|
+
description: "Unpaid leave deduction"
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
const grossSalary = calculateGross(baseAmount, allowances);
|
|
2256
|
+
const taxableAllowances = allowances.filter((a) => a.taxable);
|
|
2257
|
+
const taxableAmount = baseAmount + sumAllowances(taxableAllowances);
|
|
2258
|
+
let taxAmount = 0;
|
|
2259
|
+
const currency = comp.currency || this.config.payroll.defaultCurrency;
|
|
2260
|
+
const taxBrackets = TAX_BRACKETS[currency] || [];
|
|
2261
|
+
if (taxBrackets.length > 0 && this.config.payroll.autoDeductions) {
|
|
2262
|
+
const annualTaxable = taxableAmount * 12;
|
|
2263
|
+
const annualTax = applyTaxBrackets(annualTaxable, taxBrackets);
|
|
2264
|
+
taxAmount = Math.round(annualTax / 12);
|
|
2265
|
+
}
|
|
2266
|
+
if (taxAmount > 0) {
|
|
2267
|
+
deductions.push({
|
|
2268
|
+
type: "tax",
|
|
2269
|
+
amount: taxAmount,
|
|
2270
|
+
description: "Income tax"
|
|
2271
|
+
});
|
|
2272
|
+
}
|
|
2273
|
+
const netSalary = calculateNet(grossSalary, deductions);
|
|
2274
|
+
return {
|
|
2275
|
+
baseAmount,
|
|
2276
|
+
allowances,
|
|
2277
|
+
deductions,
|
|
2278
|
+
grossSalary,
|
|
2279
|
+
netSalary,
|
|
2280
|
+
taxableAmount,
|
|
2281
|
+
taxAmount,
|
|
2282
|
+
workingDays: proRating.workingDays,
|
|
2283
|
+
actualDays: proRating.actualDays,
|
|
2284
|
+
proRatedAmount: proRating.isProRated ? baseAmount : 0,
|
|
2285
|
+
attendanceDeduction
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
/**
|
|
2289
|
+
* Advanced pro-rating calculation that handles:
|
|
2290
|
+
* - Mid-period hires
|
|
2291
|
+
* - Mid-period terminations
|
|
2292
|
+
* - Working days (not calendar days)
|
|
2293
|
+
*/
|
|
2294
|
+
calculateProRatingAdvanced(hireDate, terminationDate, periodStart, periodEnd, workingDaysInMonth) {
|
|
2295
|
+
const hire = new Date(hireDate);
|
|
2296
|
+
const termination = terminationDate ? new Date(terminationDate) : null;
|
|
2297
|
+
const effectiveStart = hire > periodStart ? hire : periodStart;
|
|
2298
|
+
const effectiveEnd = termination && termination < periodEnd ? termination : periodEnd;
|
|
2299
|
+
if (effectiveStart > periodEnd || termination && termination < periodStart) {
|
|
2300
|
+
return {
|
|
2301
|
+
isProRated: true,
|
|
2302
|
+
ratio: 0,
|
|
2303
|
+
workingDays: workingDaysInMonth,
|
|
2304
|
+
actualDays: 0
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
const totalDays = diffInDays(periodStart, periodEnd) + 1;
|
|
2308
|
+
const actualDays = diffInDays(effectiveStart, effectiveEnd) + 1;
|
|
2309
|
+
const ratio = actualDays / totalDays;
|
|
2310
|
+
const actualWorkingDays = Math.round(workingDaysInMonth * ratio);
|
|
2311
|
+
const isProRated = hire > periodStart || termination !== null && termination < periodEnd;
|
|
2312
|
+
return {
|
|
2313
|
+
isProRated,
|
|
2314
|
+
ratio,
|
|
2315
|
+
workingDays: workingDaysInMonth,
|
|
2316
|
+
actualDays: actualWorkingDays
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2319
|
+
/**
|
|
2320
|
+
* Calculate attendance deduction using working days (not calendar days)
|
|
2321
|
+
*/
|
|
2322
|
+
async calculateAttendanceDeduction(employeeId, organizationId, period, dailyRate, expectedWorkingDays, session) {
|
|
2323
|
+
try {
|
|
2324
|
+
if (!this.models.AttendanceModel) return 0;
|
|
2325
|
+
let query = this.models.AttendanceModel.findOne({
|
|
2326
|
+
tenantId: organizationId,
|
|
2327
|
+
targetId: employeeId,
|
|
2328
|
+
targetModel: "Employee",
|
|
2329
|
+
year: period.year,
|
|
2330
|
+
month: period.month
|
|
2331
|
+
});
|
|
2332
|
+
if (session) query = query.session(session);
|
|
2333
|
+
const attendance = await query;
|
|
2334
|
+
if (!attendance) return 0;
|
|
2335
|
+
const workedDays = attendance.totalWorkDays || 0;
|
|
2336
|
+
const absentDays = Math.max(0, expectedWorkingDays - workedDays);
|
|
2337
|
+
return Math.round(absentDays * dailyRate);
|
|
2338
|
+
} catch (error) {
|
|
2339
|
+
getLogger().warn("Failed to calculate attendance deduction", {
|
|
2340
|
+
employeeId: employeeId.toString(),
|
|
2341
|
+
error: error.message
|
|
2342
|
+
});
|
|
2343
|
+
return 0;
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
async updatePayrollStats(employee2, amount, paymentDate, session) {
|
|
2347
|
+
if (!employee2.payrollStats) {
|
|
2348
|
+
employee2.payrollStats = {
|
|
2349
|
+
totalPaid: 0,
|
|
2350
|
+
paymentsThisYear: 0,
|
|
2351
|
+
averageMonthly: 0
|
|
2352
|
+
};
|
|
2353
|
+
}
|
|
2354
|
+
employee2.payrollStats.totalPaid = (employee2.payrollStats.totalPaid || 0) + amount;
|
|
2355
|
+
employee2.payrollStats.lastPaymentDate = paymentDate;
|
|
2356
|
+
employee2.payrollStats.paymentsThisYear = (employee2.payrollStats.paymentsThisYear || 0) + 1;
|
|
2357
|
+
employee2.payrollStats.averageMonthly = Math.round(
|
|
2358
|
+
employee2.payrollStats.totalPaid / employee2.payrollStats.paymentsThisYear
|
|
2359
|
+
);
|
|
2360
|
+
employee2.payrollStats.nextPaymentDate = addMonths(paymentDate, 1);
|
|
2361
|
+
await employee2.save(session ? { session } : {});
|
|
2362
|
+
}
|
|
2363
|
+
// ========================================
|
|
2364
|
+
// Static Factory
|
|
2365
|
+
// ========================================
|
|
2366
|
+
/**
|
|
2367
|
+
* Create a new Payroll instance
|
|
2368
|
+
*/
|
|
2369
|
+
static create() {
|
|
2370
|
+
return new _Payroll();
|
|
2371
|
+
}
|
|
2372
|
+
};
|
|
2373
|
+
var PayrollBuilder = class {
|
|
2374
|
+
_models = null;
|
|
2375
|
+
_config;
|
|
2376
|
+
_singleTenant = null;
|
|
2377
|
+
_logger;
|
|
2378
|
+
/**
|
|
2379
|
+
* Set models
|
|
2380
|
+
*/
|
|
2381
|
+
withModels(models) {
|
|
2382
|
+
this._models = models;
|
|
2383
|
+
return this;
|
|
2384
|
+
}
|
|
2385
|
+
/**
|
|
2386
|
+
* Set config overrides
|
|
2387
|
+
*/
|
|
2388
|
+
withConfig(config) {
|
|
2389
|
+
this._config = config;
|
|
2390
|
+
return this;
|
|
2391
|
+
}
|
|
2392
|
+
/**
|
|
2393
|
+
* Enable single-tenant mode
|
|
2394
|
+
*
|
|
2395
|
+
* Use this when building a single-organization HRM (no organizationId needed)
|
|
2396
|
+
*
|
|
2397
|
+
* @example
|
|
2398
|
+
* ```typescript
|
|
2399
|
+
* const payroll = createPayrollInstance()
|
|
2400
|
+
* .withModels({ EmployeeModel, PayrollRecordModel, TransactionModel })
|
|
2401
|
+
* .withSingleTenant({ organizationId: 'my-company' })
|
|
2402
|
+
* .build();
|
|
2403
|
+
* ```
|
|
2404
|
+
*/
|
|
2405
|
+
withSingleTenant(config) {
|
|
2406
|
+
this._singleTenant = config;
|
|
2407
|
+
return this;
|
|
2408
|
+
}
|
|
2409
|
+
/**
|
|
2410
|
+
* Enable single-tenant mode (shorthand)
|
|
2411
|
+
*
|
|
2412
|
+
* Alias for withSingleTenant() - consistent with @classytic/clockin API
|
|
2413
|
+
*
|
|
2414
|
+
* @example
|
|
2415
|
+
* ```typescript
|
|
2416
|
+
* const payroll = createPayrollInstance()
|
|
2417
|
+
* .withModels({ ... })
|
|
2418
|
+
* .forSingleTenant() // ← No organizationId needed!
|
|
2419
|
+
* .build();
|
|
2420
|
+
* ```
|
|
2421
|
+
*/
|
|
2422
|
+
forSingleTenant(config = {}) {
|
|
2423
|
+
return this.withSingleTenant(config);
|
|
2424
|
+
}
|
|
2425
|
+
/**
|
|
2426
|
+
* Set custom logger
|
|
2427
|
+
*/
|
|
2428
|
+
withLogger(logger2) {
|
|
2429
|
+
this._logger = logger2;
|
|
2430
|
+
return this;
|
|
2431
|
+
}
|
|
2432
|
+
/**
|
|
2433
|
+
* Build and initialize Payroll instance
|
|
2434
|
+
*/
|
|
2435
|
+
build() {
|
|
2436
|
+
if (!this._models) {
|
|
2437
|
+
throw new Error("Models are required. Call withModels() first.");
|
|
2438
|
+
}
|
|
2439
|
+
const payroll3 = new Payroll();
|
|
2440
|
+
payroll3.initialize({
|
|
2441
|
+
EmployeeModel: this._models.EmployeeModel,
|
|
2442
|
+
PayrollRecordModel: this._models.PayrollRecordModel,
|
|
2443
|
+
TransactionModel: this._models.TransactionModel,
|
|
2444
|
+
AttendanceModel: this._models.AttendanceModel,
|
|
2445
|
+
config: this._config,
|
|
2446
|
+
singleTenant: this._singleTenant,
|
|
2447
|
+
logger: this._logger
|
|
2448
|
+
});
|
|
2449
|
+
return payroll3;
|
|
2450
|
+
}
|
|
2451
|
+
};
|
|
2452
|
+
function createPayrollInstance() {
|
|
2453
|
+
return new PayrollBuilder();
|
|
2454
|
+
}
|
|
2455
|
+
var payrollInstance = null;
|
|
2456
|
+
function getPayroll() {
|
|
2457
|
+
if (!payrollInstance) {
|
|
2458
|
+
payrollInstance = new Payroll();
|
|
2459
|
+
}
|
|
2460
|
+
return payrollInstance;
|
|
2461
|
+
}
|
|
2462
|
+
function resetPayroll() {
|
|
2463
|
+
payrollInstance = null;
|
|
2464
|
+
Container.resetInstance();
|
|
2465
|
+
}
|
|
2466
|
+
var payroll2 = getPayroll();
|
|
2467
|
+
var allowanceSchema = new Schema(
|
|
2468
|
+
{
|
|
2469
|
+
type: {
|
|
2470
|
+
type: String,
|
|
2471
|
+
enum: ALLOWANCE_TYPE_VALUES,
|
|
2472
|
+
required: true
|
|
2473
|
+
},
|
|
2474
|
+
name: { type: String },
|
|
2475
|
+
amount: { type: Number, required: true, min: 0 },
|
|
2476
|
+
isPercentage: { type: Boolean, default: false },
|
|
2477
|
+
value: { type: Number },
|
|
2478
|
+
taxable: { type: Boolean, default: true },
|
|
2479
|
+
recurring: { type: Boolean, default: true },
|
|
2480
|
+
effectiveFrom: { type: Date, default: () => /* @__PURE__ */ new Date() },
|
|
2481
|
+
effectiveTo: { type: Date }
|
|
2482
|
+
},
|
|
2483
|
+
{ _id: false }
|
|
2484
|
+
);
|
|
2485
|
+
var deductionSchema = new Schema(
|
|
2486
|
+
{
|
|
2487
|
+
type: {
|
|
2488
|
+
type: String,
|
|
2489
|
+
enum: DEDUCTION_TYPE_VALUES,
|
|
2490
|
+
required: true
|
|
2491
|
+
},
|
|
2492
|
+
name: { type: String },
|
|
2493
|
+
amount: { type: Number, required: true, min: 0 },
|
|
2494
|
+
isPercentage: { type: Boolean, default: false },
|
|
2495
|
+
value: { type: Number },
|
|
2496
|
+
auto: { type: Boolean, default: false },
|
|
2497
|
+
recurring: { type: Boolean, default: true },
|
|
2498
|
+
effectiveFrom: { type: Date, default: () => /* @__PURE__ */ new Date() },
|
|
2499
|
+
effectiveTo: { type: Date },
|
|
2500
|
+
description: { type: String }
|
|
2501
|
+
},
|
|
2502
|
+
{ _id: false }
|
|
2503
|
+
);
|
|
2504
|
+
var compensationSchema = new Schema(
|
|
2505
|
+
{
|
|
2506
|
+
baseAmount: { type: Number, required: true, min: 0 },
|
|
2507
|
+
frequency: {
|
|
2508
|
+
type: String,
|
|
2509
|
+
enum: PAYMENT_FREQUENCY_VALUES,
|
|
2510
|
+
default: "monthly"
|
|
2511
|
+
},
|
|
2512
|
+
currency: { type: String, default: "BDT" },
|
|
2513
|
+
allowances: [allowanceSchema],
|
|
2514
|
+
deductions: [deductionSchema],
|
|
2515
|
+
grossSalary: { type: Number, default: 0 },
|
|
2516
|
+
netSalary: { type: Number, default: 0 },
|
|
2517
|
+
effectiveFrom: { type: Date, default: () => /* @__PURE__ */ new Date() },
|
|
2518
|
+
lastModified: { type: Date, default: () => /* @__PURE__ */ new Date() }
|
|
2519
|
+
},
|
|
2520
|
+
{ _id: false }
|
|
2521
|
+
);
|
|
2522
|
+
var workScheduleSchema = new Schema(
|
|
2523
|
+
{
|
|
2524
|
+
hoursPerWeek: { type: Number, min: 0, max: 168 },
|
|
2525
|
+
hoursPerDay: { type: Number, min: 0, max: 24 },
|
|
2526
|
+
workingDays: [{ type: Number, min: 0, max: 6 }],
|
|
2527
|
+
shiftStart: { type: String },
|
|
2528
|
+
shiftEnd: { type: String }
|
|
2529
|
+
},
|
|
2530
|
+
{ _id: false }
|
|
2531
|
+
);
|
|
2532
|
+
var bankDetailsSchema = new Schema(
|
|
2533
|
+
{
|
|
2534
|
+
accountName: { type: String },
|
|
2535
|
+
accountNumber: { type: String },
|
|
2536
|
+
bankName: { type: String },
|
|
2537
|
+
branchName: { type: String },
|
|
2538
|
+
routingNumber: { type: String }
|
|
2539
|
+
},
|
|
2540
|
+
{ _id: false }
|
|
2541
|
+
);
|
|
2542
|
+
var employmentHistorySchema = new Schema(
|
|
2543
|
+
{
|
|
2544
|
+
hireDate: { type: Date, required: true },
|
|
2545
|
+
terminationDate: { type: Date, required: true },
|
|
2546
|
+
reason: { type: String, enum: TERMINATION_REASON_VALUES },
|
|
2547
|
+
finalSalary: { type: Number },
|
|
2548
|
+
position: { type: String },
|
|
2549
|
+
department: { type: String },
|
|
2550
|
+
notes: { type: String }
|
|
2551
|
+
},
|
|
2552
|
+
{ timestamps: true }
|
|
2553
|
+
);
|
|
2554
|
+
var payrollStatsSchema = new Schema(
|
|
2555
|
+
{
|
|
2556
|
+
totalPaid: { type: Number, default: 0, min: 0 },
|
|
2557
|
+
lastPaymentDate: { type: Date },
|
|
2558
|
+
nextPaymentDate: { type: Date },
|
|
2559
|
+
paymentsThisYear: { type: Number, default: 0, min: 0 },
|
|
2560
|
+
averageMonthly: { type: Number, default: 0, min: 0 },
|
|
2561
|
+
updatedAt: { type: Date, default: () => /* @__PURE__ */ new Date() }
|
|
2562
|
+
},
|
|
2563
|
+
{ _id: false }
|
|
2564
|
+
);
|
|
2565
|
+
var employmentFields = {
|
|
2566
|
+
userId: {
|
|
2567
|
+
type: Schema.Types.ObjectId,
|
|
2568
|
+
ref: "User",
|
|
2569
|
+
required: true
|
|
2570
|
+
},
|
|
2571
|
+
organizationId: {
|
|
2572
|
+
type: Schema.Types.ObjectId,
|
|
2573
|
+
ref: "Organization",
|
|
2574
|
+
required: true
|
|
2575
|
+
},
|
|
2576
|
+
employeeId: { type: String, required: true },
|
|
2577
|
+
employmentType: {
|
|
2578
|
+
type: String,
|
|
2579
|
+
enum: EMPLOYMENT_TYPE_VALUES,
|
|
2580
|
+
default: "full_time"
|
|
2581
|
+
},
|
|
2582
|
+
status: {
|
|
2583
|
+
type: String,
|
|
2584
|
+
enum: EMPLOYEE_STATUS_VALUES,
|
|
2585
|
+
default: "active"
|
|
2586
|
+
},
|
|
2587
|
+
department: { type: String, enum: DEPARTMENT_VALUES },
|
|
2588
|
+
position: { type: String, required: true },
|
|
2589
|
+
hireDate: { type: Date, required: true },
|
|
2590
|
+
terminationDate: { type: Date },
|
|
2591
|
+
probationEndDate: { type: Date },
|
|
2592
|
+
employmentHistory: [employmentHistorySchema],
|
|
2593
|
+
compensation: { type: compensationSchema, required: true },
|
|
2594
|
+
workSchedule: workScheduleSchema,
|
|
2595
|
+
bankDetails: bankDetailsSchema,
|
|
2596
|
+
payrollStats: { type: payrollStatsSchema, default: () => ({}) }
|
|
2597
|
+
};
|
|
2598
|
+
new Schema(
|
|
2599
|
+
{
|
|
2600
|
+
baseAmount: { type: Number, required: true, min: 0 },
|
|
2601
|
+
allowances: [
|
|
2602
|
+
{
|
|
2603
|
+
type: { type: String },
|
|
2604
|
+
amount: { type: Number, min: 0 },
|
|
2605
|
+
taxable: { type: Boolean, default: true }
|
|
2606
|
+
}
|
|
2607
|
+
],
|
|
2608
|
+
deductions: [
|
|
2609
|
+
{
|
|
2610
|
+
type: { type: String },
|
|
2611
|
+
amount: { type: Number, min: 0 },
|
|
2612
|
+
description: { type: String }
|
|
2613
|
+
}
|
|
2614
|
+
],
|
|
2615
|
+
grossSalary: { type: Number, required: true, min: 0 },
|
|
2616
|
+
netSalary: { type: Number, required: true, min: 0 },
|
|
2617
|
+
workingDays: { type: Number, min: 0 },
|
|
2618
|
+
actualDays: { type: Number, min: 0 },
|
|
2619
|
+
proRatedAmount: { type: Number, default: 0, min: 0 },
|
|
2620
|
+
attendanceDeduction: { type: Number, default: 0, min: 0 },
|
|
2621
|
+
overtimeAmount: { type: Number, default: 0, min: 0 },
|
|
2622
|
+
bonusAmount: { type: Number, default: 0, min: 0 }
|
|
2623
|
+
},
|
|
2624
|
+
{ _id: false }
|
|
2625
|
+
);
|
|
2626
|
+
new Schema(
|
|
2627
|
+
{
|
|
2628
|
+
month: { type: Number, required: true, min: 1, max: 12 },
|
|
2629
|
+
year: { type: Number, required: true, min: 2020 },
|
|
2630
|
+
startDate: { type: Date, required: true },
|
|
2631
|
+
endDate: { type: Date, required: true },
|
|
2632
|
+
payDate: { type: Date, required: true }
|
|
2633
|
+
},
|
|
2634
|
+
{ _id: false }
|
|
2635
|
+
);
|
|
2636
|
+
({
|
|
2637
|
+
organizationId: {
|
|
2638
|
+
type: Schema.Types.ObjectId},
|
|
2639
|
+
employeeId: {
|
|
2640
|
+
type: Schema.Types.ObjectId},
|
|
2641
|
+
userId: {
|
|
2642
|
+
type: Schema.Types.ObjectId},
|
|
2643
|
+
transactionId: { type: Schema.Types.ObjectId},
|
|
2644
|
+
processedBy: { type: Schema.Types.ObjectId}});
|
|
2645
|
+
[
|
|
2646
|
+
{
|
|
2647
|
+
fields: { organizationId: 1, employeeId: 1, "period.month": 1, "period.year": 1 },
|
|
2648
|
+
options: { unique: true }
|
|
2649
|
+
},
|
|
2650
|
+
{ fields: { organizationId: 1, "period.year": 1, "period.month": 1 } },
|
|
2651
|
+
{ fields: { employeeId: 1, "period.year": -1, "period.month": -1 } },
|
|
2652
|
+
{ fields: { status: 1, createdAt: -1 } },
|
|
2653
|
+
{ fields: { organizationId: 1, status: 1, "period.payDate": 1 } },
|
|
2654
|
+
{
|
|
2655
|
+
fields: { createdAt: 1 },
|
|
2656
|
+
options: {
|
|
2657
|
+
expireAfterSeconds: HRM_CONFIG.dataRetention.payrollRecordsTTL,
|
|
2658
|
+
partialFilterExpression: { exported: true }
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
];
|
|
2662
|
+
var allowanceBreakdownSchema = new Schema(
|
|
2663
|
+
{
|
|
2664
|
+
type: { type: String, required: true },
|
|
2665
|
+
amount: { type: Number, required: true },
|
|
2666
|
+
taxable: { type: Boolean, default: true }
|
|
2667
|
+
},
|
|
2668
|
+
{ _id: false }
|
|
2669
|
+
);
|
|
2670
|
+
var deductionBreakdownSchema = new Schema(
|
|
2671
|
+
{
|
|
2672
|
+
type: { type: String, required: true },
|
|
2673
|
+
amount: { type: Number, required: true },
|
|
2674
|
+
description: { type: String }
|
|
2675
|
+
},
|
|
2676
|
+
{ _id: false }
|
|
2677
|
+
);
|
|
2678
|
+
var breakdownSchema = new Schema(
|
|
2679
|
+
{
|
|
2680
|
+
baseAmount: { type: Number, required: true },
|
|
2681
|
+
allowances: [allowanceBreakdownSchema],
|
|
2682
|
+
deductions: [deductionBreakdownSchema],
|
|
2683
|
+
grossSalary: { type: Number, required: true },
|
|
2684
|
+
netSalary: { type: Number, required: true },
|
|
2685
|
+
taxableAmount: { type: Number, default: 0 },
|
|
2686
|
+
taxAmount: { type: Number, default: 0 },
|
|
2687
|
+
workingDays: { type: Number },
|
|
2688
|
+
actualDays: { type: Number },
|
|
2689
|
+
proRatedAmount: { type: Number, default: 0 },
|
|
2690
|
+
attendanceDeduction: { type: Number, default: 0 }
|
|
2691
|
+
},
|
|
2692
|
+
{ _id: false }
|
|
2693
|
+
);
|
|
2694
|
+
var periodSchema2 = new Schema(
|
|
2695
|
+
{
|
|
2696
|
+
month: { type: Number, required: true, min: 1, max: 12 },
|
|
2697
|
+
year: { type: Number, required: true },
|
|
2698
|
+
startDate: { type: Date, required: true },
|
|
2699
|
+
endDate: { type: Date, required: true },
|
|
2700
|
+
payDate: { type: Date }
|
|
2701
|
+
},
|
|
2702
|
+
{ _id: false }
|
|
2703
|
+
);
|
|
2704
|
+
var payrollRecordSchema = new Schema(
|
|
2705
|
+
{
|
|
2706
|
+
organizationId: {
|
|
2707
|
+
type: Schema.Types.ObjectId,
|
|
2708
|
+
required: true,
|
|
2709
|
+
ref: "Organization",
|
|
2710
|
+
index: true
|
|
2711
|
+
},
|
|
2712
|
+
employeeId: {
|
|
2713
|
+
type: Schema.Types.ObjectId,
|
|
2714
|
+
required: true,
|
|
2715
|
+
ref: "Employee",
|
|
2716
|
+
index: true
|
|
2717
|
+
},
|
|
2718
|
+
userId: {
|
|
2719
|
+
type: Schema.Types.ObjectId,
|
|
2720
|
+
required: true,
|
|
2721
|
+
ref: "User",
|
|
2722
|
+
index: true
|
|
2723
|
+
},
|
|
2724
|
+
period: {
|
|
2725
|
+
type: periodSchema2,
|
|
2726
|
+
required: true
|
|
2727
|
+
},
|
|
2728
|
+
breakdown: {
|
|
2729
|
+
type: breakdownSchema,
|
|
2730
|
+
required: true
|
|
2731
|
+
},
|
|
2732
|
+
status: {
|
|
2733
|
+
type: String,
|
|
2734
|
+
enum: Object.values(PAYROLL_STATUS),
|
|
2735
|
+
default: PAYROLL_STATUS.PENDING,
|
|
2736
|
+
index: true
|
|
2737
|
+
},
|
|
2738
|
+
paymentMethod: {
|
|
2739
|
+
type: String,
|
|
2740
|
+
enum: ["cash", "bank", "check", "mobile", "bkash", "nagad", "rocket"],
|
|
2741
|
+
default: "bank"
|
|
2742
|
+
},
|
|
2743
|
+
transactionId: {
|
|
2744
|
+
type: Schema.Types.ObjectId,
|
|
2745
|
+
ref: "Transaction"
|
|
2746
|
+
},
|
|
2747
|
+
paidAt: Date,
|
|
2748
|
+
processedAt: Date,
|
|
2749
|
+
processedBy: {
|
|
2750
|
+
type: Schema.Types.ObjectId,
|
|
2751
|
+
ref: "User"
|
|
2752
|
+
},
|
|
2753
|
+
notes: String,
|
|
2754
|
+
metadata: {
|
|
2755
|
+
type: Schema.Types.Mixed,
|
|
2756
|
+
default: {}
|
|
2757
|
+
},
|
|
2758
|
+
exported: {
|
|
2759
|
+
type: Boolean,
|
|
2760
|
+
default: false
|
|
2761
|
+
},
|
|
2762
|
+
exportedAt: Date,
|
|
2763
|
+
corrections: [
|
|
2764
|
+
{
|
|
2765
|
+
previousAmount: Number,
|
|
2766
|
+
newAmount: Number,
|
|
2767
|
+
reason: String,
|
|
2768
|
+
correctedBy: { type: Schema.Types.ObjectId, ref: "User" },
|
|
2769
|
+
correctedAt: { type: Date, default: Date.now }
|
|
2770
|
+
}
|
|
2771
|
+
]
|
|
2772
|
+
},
|
|
2773
|
+
{
|
|
2774
|
+
timestamps: true
|
|
2775
|
+
}
|
|
2776
|
+
);
|
|
2777
|
+
payrollRecordSchema.index({ organizationId: 1, "period.month": 1, "period.year": 1 });
|
|
2778
|
+
payrollRecordSchema.index({ employeeId: 1, "period.month": 1, "period.year": 1 }, { unique: true });
|
|
2779
|
+
payrollRecordSchema.index({ organizationId: 1, status: 1 });
|
|
2780
|
+
payrollRecordSchema.index({ createdAt: 1 }, { expireAfterSeconds: HRM_CONFIG.dataRetention.payrollRecordsTTL });
|
|
2781
|
+
payrollRecordSchema.virtual("isPaid").get(function() {
|
|
2782
|
+
return this.status === PAYROLL_STATUS.PAID;
|
|
2783
|
+
});
|
|
2784
|
+
payrollRecordSchema.virtual("totalDeductions").get(function() {
|
|
2785
|
+
return (this.breakdown?.deductions || []).reduce(
|
|
2786
|
+
(sum2, d) => sum2 + d.amount,
|
|
2787
|
+
0
|
|
2788
|
+
);
|
|
2789
|
+
});
|
|
2790
|
+
payrollRecordSchema.virtual("totalAllowances").get(function() {
|
|
2791
|
+
return (this.breakdown?.allowances || []).reduce(
|
|
2792
|
+
(sum2, a) => sum2 + a.amount,
|
|
2793
|
+
0
|
|
2794
|
+
);
|
|
2795
|
+
});
|
|
2796
|
+
payrollRecordSchema.methods.markAsPaid = function(transactionId, paidAt = /* @__PURE__ */ new Date()) {
|
|
2797
|
+
this.status = PAYROLL_STATUS.PAID;
|
|
2798
|
+
this.transactionId = transactionId;
|
|
2799
|
+
this.paidAt = paidAt;
|
|
2800
|
+
};
|
|
2801
|
+
payrollRecordSchema.methods.markAsCancelled = function(reason) {
|
|
2802
|
+
if (this.status === PAYROLL_STATUS.PAID) {
|
|
2803
|
+
throw new Error("Cannot cancel paid payroll record");
|
|
2804
|
+
}
|
|
2805
|
+
this.status = PAYROLL_STATUS.CANCELLED;
|
|
2806
|
+
this.notes = (this.notes || "") + `
|
|
2807
|
+
Cancelled: ${reason}`;
|
|
2808
|
+
};
|
|
2809
|
+
payrollRecordSchema.methods.addCorrection = function(previousAmount, newAmount, reason, correctedBy) {
|
|
2810
|
+
if (!this.corrections) {
|
|
2811
|
+
this.corrections = [];
|
|
2812
|
+
}
|
|
2813
|
+
this.corrections.push({
|
|
2814
|
+
previousAmount,
|
|
2815
|
+
newAmount,
|
|
2816
|
+
reason,
|
|
2817
|
+
correctedBy,
|
|
2818
|
+
correctedAt: /* @__PURE__ */ new Date()
|
|
2819
|
+
});
|
|
2820
|
+
this.breakdown.netSalary = newAmount;
|
|
2821
|
+
logger.info("Payroll correction added", {
|
|
2822
|
+
recordId: this._id.toString(),
|
|
2823
|
+
previousAmount,
|
|
2824
|
+
newAmount,
|
|
2825
|
+
reason
|
|
2826
|
+
});
|
|
2827
|
+
};
|
|
2828
|
+
payrollRecordSchema.methods.getBreakdownSummary = function() {
|
|
2829
|
+
const { baseAmount, allowances, deductions, grossSalary, netSalary } = this.breakdown;
|
|
2830
|
+
return {
|
|
2831
|
+
base: baseAmount,
|
|
2832
|
+
totalAllowances: (allowances || []).reduce(
|
|
2833
|
+
(sum2, a) => sum2 + a.amount,
|
|
2834
|
+
0
|
|
2835
|
+
),
|
|
2836
|
+
totalDeductions: (deductions || []).reduce(
|
|
2837
|
+
(sum2, d) => sum2 + d.amount,
|
|
2838
|
+
0
|
|
2839
|
+
),
|
|
2840
|
+
gross: grossSalary,
|
|
2841
|
+
net: netSalary
|
|
2842
|
+
};
|
|
2843
|
+
};
|
|
2844
|
+
payrollRecordSchema.statics.findByPeriod = function(organizationId, month, year) {
|
|
2845
|
+
return this.find({
|
|
2846
|
+
organizationId,
|
|
2847
|
+
"period.month": month,
|
|
2848
|
+
"period.year": year
|
|
2849
|
+
});
|
|
2850
|
+
};
|
|
2851
|
+
payrollRecordSchema.statics.findByEmployee = function(employeeId, limit = 12) {
|
|
2852
|
+
return this.find({ employeeId }).sort({ "period.year": -1, "period.month": -1 }).limit(limit);
|
|
2853
|
+
};
|
|
2854
|
+
payrollRecordSchema.statics.getSummary = function(organizationId, month, year) {
|
|
2855
|
+
const match2 = { organizationId };
|
|
2856
|
+
if (month) match2["period.month"] = month;
|
|
2857
|
+
if (year) match2["period.year"] = year;
|
|
2858
|
+
return this.aggregate([
|
|
2859
|
+
{ $match: match2 },
|
|
2860
|
+
{
|
|
2861
|
+
$group: {
|
|
2862
|
+
_id: null,
|
|
2863
|
+
totalGross: { $sum: "$breakdown.grossSalary" },
|
|
2864
|
+
totalNet: { $sum: "$breakdown.netSalary" },
|
|
2865
|
+
count: { $sum: 1 },
|
|
2866
|
+
paidCount: {
|
|
2867
|
+
$sum: { $cond: [{ $eq: ["$status", PAYROLL_STATUS.PAID] }, 1, 0] }
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
]).then((results) => results[0] || { totalGross: 0, totalNet: 0, count: 0, paidCount: 0 });
|
|
2872
|
+
};
|
|
2873
|
+
payrollRecordSchema.statics.getExpiringSoon = function(organizationId, daysBeforeExpiry = 30) {
|
|
2874
|
+
const expiryThreshold = /* @__PURE__ */ new Date();
|
|
2875
|
+
expiryThreshold.setSeconds(
|
|
2876
|
+
expiryThreshold.getSeconds() + HRM_CONFIG.dataRetention.payrollRecordsTTL - daysBeforeExpiry * 24 * 60 * 60
|
|
2877
|
+
);
|
|
2878
|
+
return this.find({
|
|
2879
|
+
organizationId,
|
|
2880
|
+
exported: false,
|
|
2881
|
+
createdAt: { $lte: expiryThreshold }
|
|
2882
|
+
});
|
|
2883
|
+
};
|
|
2884
|
+
function getPayrollRecordModel(connection = mongoose2.connection) {
|
|
2885
|
+
const modelName = "PayrollRecord";
|
|
2886
|
+
if (connection.models[modelName]) {
|
|
2887
|
+
return connection.models[modelName];
|
|
2888
|
+
}
|
|
2889
|
+
return connection.model(
|
|
2890
|
+
modelName,
|
|
2891
|
+
payrollRecordSchema
|
|
2892
|
+
);
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
// src/utils/validation.ts
|
|
2896
|
+
function isActive(employee2) {
|
|
2897
|
+
return employee2?.status === "active";
|
|
2898
|
+
}
|
|
2899
|
+
function isOnLeave(employee2) {
|
|
2900
|
+
return employee2?.status === "on_leave";
|
|
2901
|
+
}
|
|
2902
|
+
function isSuspended(employee2) {
|
|
2903
|
+
return employee2?.status === "suspended";
|
|
2904
|
+
}
|
|
2905
|
+
function isTerminated(employee2) {
|
|
2906
|
+
return employee2?.status === "terminated";
|
|
2907
|
+
}
|
|
2908
|
+
function isEmployed(employee2) {
|
|
2909
|
+
return isActive(employee2) || isOnLeave(employee2) || isSuspended(employee2);
|
|
2910
|
+
}
|
|
2911
|
+
function canReceiveSalary(employee2) {
|
|
2912
|
+
return (isActive(employee2) || isOnLeave(employee2)) && (employee2.compensation?.baseAmount ?? 0) > 0;
|
|
2913
|
+
}
|
|
2914
|
+
function required(fieldName) {
|
|
2915
|
+
return (value) => value !== void 0 && value !== null && value !== "" ? true : `${fieldName} is required`;
|
|
2916
|
+
}
|
|
2917
|
+
function min(minValue2, fieldName) {
|
|
2918
|
+
return (value) => value >= minValue2 ? true : `${fieldName} must be at least ${minValue2}`;
|
|
2919
|
+
}
|
|
2920
|
+
function max(maxValue2, fieldName) {
|
|
2921
|
+
return (value) => value <= maxValue2 ? true : `${fieldName} must not exceed ${maxValue2}`;
|
|
2922
|
+
}
|
|
2923
|
+
function inRange(minValue2, maxValue2, fieldName) {
|
|
2924
|
+
return (value) => value >= minValue2 && value <= maxValue2 ? true : `${fieldName} must be between ${minValue2} and ${maxValue2}`;
|
|
2925
|
+
}
|
|
2926
|
+
function oneOf(allowedValues, fieldName) {
|
|
2927
|
+
return (value) => allowedValues.includes(value) ? true : `${fieldName} must be one of: ${allowedValues.join(", ")}`;
|
|
2928
|
+
}
|
|
2929
|
+
function createValidator(validationFns) {
|
|
2930
|
+
return (data) => {
|
|
2931
|
+
const errors = [];
|
|
2932
|
+
for (const [field, validator] of Object.entries(validationFns)) {
|
|
2933
|
+
const result = validator(data[field], data);
|
|
2934
|
+
if (result !== true) {
|
|
2935
|
+
errors.push(result);
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
return {
|
|
2939
|
+
valid: errors.length === 0,
|
|
2940
|
+
errors
|
|
2941
|
+
};
|
|
2942
|
+
};
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
// src/factories/compensation.factory.ts
|
|
2946
|
+
var CompensationFactory = class {
|
|
2947
|
+
/**
|
|
2948
|
+
* Create compensation object
|
|
2949
|
+
*/
|
|
2950
|
+
static create(params) {
|
|
2951
|
+
const {
|
|
2952
|
+
baseAmount,
|
|
2953
|
+
frequency = "monthly",
|
|
2954
|
+
currency = HRM_CONFIG.payroll.defaultCurrency,
|
|
2955
|
+
allowances = [],
|
|
2956
|
+
deductions = [],
|
|
2957
|
+
effectiveFrom = /* @__PURE__ */ new Date()
|
|
2958
|
+
} = params;
|
|
2959
|
+
return {
|
|
2960
|
+
baseAmount,
|
|
2961
|
+
frequency,
|
|
2962
|
+
currency,
|
|
2963
|
+
allowances: allowances.map((a) => this.createAllowance(a, baseAmount)),
|
|
2964
|
+
deductions: deductions.map((d) => this.createDeduction(d, baseAmount)),
|
|
2965
|
+
effectiveFrom,
|
|
2966
|
+
lastModified: /* @__PURE__ */ new Date()
|
|
2967
|
+
};
|
|
2968
|
+
}
|
|
2969
|
+
/**
|
|
2970
|
+
* Create allowance
|
|
2971
|
+
*/
|
|
2972
|
+
static createAllowance(params, baseAmount) {
|
|
2973
|
+
const amount = params.isPercentage && baseAmount ? applyPercentage(baseAmount, params.value) : params.value;
|
|
2974
|
+
return {
|
|
2975
|
+
type: params.type,
|
|
2976
|
+
name: params.name || params.type,
|
|
2977
|
+
amount,
|
|
2978
|
+
isPercentage: params.isPercentage ?? false,
|
|
2979
|
+
value: params.isPercentage ? params.value : void 0,
|
|
2980
|
+
taxable: params.taxable ?? true,
|
|
2981
|
+
recurring: true,
|
|
2982
|
+
effectiveFrom: /* @__PURE__ */ new Date()
|
|
2983
|
+
};
|
|
2984
|
+
}
|
|
2985
|
+
/**
|
|
2986
|
+
* Create deduction
|
|
2987
|
+
*/
|
|
2988
|
+
static createDeduction(params, baseAmount) {
|
|
2989
|
+
const amount = params.isPercentage && baseAmount ? applyPercentage(baseAmount, params.value) : params.value;
|
|
2990
|
+
return {
|
|
2991
|
+
type: params.type,
|
|
2992
|
+
name: params.name || params.type,
|
|
2993
|
+
amount,
|
|
2994
|
+
isPercentage: params.isPercentage ?? false,
|
|
2995
|
+
value: params.isPercentage ? params.value : void 0,
|
|
2996
|
+
auto: params.auto ?? false,
|
|
2997
|
+
recurring: true,
|
|
2998
|
+
effectiveFrom: /* @__PURE__ */ new Date()
|
|
2999
|
+
};
|
|
3000
|
+
}
|
|
3001
|
+
/**
|
|
3002
|
+
* Update base amount (immutable)
|
|
3003
|
+
*/
|
|
3004
|
+
static updateBaseAmount(compensation, newAmount, effectiveFrom = /* @__PURE__ */ new Date()) {
|
|
3005
|
+
return {
|
|
3006
|
+
...compensation,
|
|
3007
|
+
baseAmount: newAmount,
|
|
3008
|
+
lastModified: effectiveFrom
|
|
3009
|
+
};
|
|
3010
|
+
}
|
|
3011
|
+
/**
|
|
3012
|
+
* Add allowance (immutable)
|
|
3013
|
+
*/
|
|
3014
|
+
static addAllowance(compensation, allowance) {
|
|
3015
|
+
return {
|
|
3016
|
+
...compensation,
|
|
3017
|
+
allowances: [
|
|
3018
|
+
...compensation.allowances,
|
|
3019
|
+
this.createAllowance(allowance, compensation.baseAmount)
|
|
3020
|
+
],
|
|
3021
|
+
lastModified: /* @__PURE__ */ new Date()
|
|
3022
|
+
};
|
|
3023
|
+
}
|
|
3024
|
+
/**
|
|
3025
|
+
* Remove allowance (immutable)
|
|
3026
|
+
*/
|
|
3027
|
+
static removeAllowance(compensation, allowanceType) {
|
|
3028
|
+
return {
|
|
3029
|
+
...compensation,
|
|
3030
|
+
allowances: compensation.allowances.filter((a) => a.type !== allowanceType),
|
|
3031
|
+
lastModified: /* @__PURE__ */ new Date()
|
|
3032
|
+
};
|
|
3033
|
+
}
|
|
3034
|
+
/**
|
|
3035
|
+
* Add deduction (immutable)
|
|
3036
|
+
*/
|
|
3037
|
+
static addDeduction(compensation, deduction) {
|
|
3038
|
+
return {
|
|
3039
|
+
...compensation,
|
|
3040
|
+
deductions: [
|
|
3041
|
+
...compensation.deductions,
|
|
3042
|
+
this.createDeduction(deduction, compensation.baseAmount)
|
|
3043
|
+
],
|
|
3044
|
+
lastModified: /* @__PURE__ */ new Date()
|
|
3045
|
+
};
|
|
3046
|
+
}
|
|
3047
|
+
/**
|
|
3048
|
+
* Remove deduction (immutable)
|
|
3049
|
+
*/
|
|
3050
|
+
static removeDeduction(compensation, deductionType) {
|
|
3051
|
+
return {
|
|
3052
|
+
...compensation,
|
|
3053
|
+
deductions: compensation.deductions.filter((d) => d.type !== deductionType),
|
|
3054
|
+
lastModified: /* @__PURE__ */ new Date()
|
|
3055
|
+
};
|
|
3056
|
+
}
|
|
3057
|
+
/**
|
|
3058
|
+
* Calculate compensation breakdown
|
|
3059
|
+
*/
|
|
3060
|
+
static calculateBreakdown(compensation) {
|
|
3061
|
+
const { baseAmount, allowances, deductions } = compensation;
|
|
3062
|
+
const calculatedAllowances = allowances.map((a) => ({
|
|
3063
|
+
...a,
|
|
3064
|
+
calculatedAmount: a.isPercentage && a.value !== void 0 ? applyPercentage(baseAmount, a.value) : a.amount
|
|
3065
|
+
}));
|
|
3066
|
+
const calculatedDeductions = deductions.map((d) => ({
|
|
3067
|
+
...d,
|
|
3068
|
+
calculatedAmount: d.isPercentage && d.value !== void 0 ? applyPercentage(baseAmount, d.value) : d.amount
|
|
3069
|
+
}));
|
|
3070
|
+
const grossAmount = calculateGross(
|
|
3071
|
+
baseAmount,
|
|
3072
|
+
calculatedAllowances.map((a) => ({ amount: a.calculatedAmount }))
|
|
3073
|
+
);
|
|
3074
|
+
const netAmount = calculateNet(
|
|
3075
|
+
grossAmount,
|
|
3076
|
+
calculatedDeductions.map((d) => ({ amount: d.calculatedAmount }))
|
|
3077
|
+
);
|
|
3078
|
+
return {
|
|
3079
|
+
baseAmount,
|
|
3080
|
+
allowances: calculatedAllowances,
|
|
3081
|
+
deductions: calculatedDeductions,
|
|
3082
|
+
grossAmount,
|
|
3083
|
+
netAmount: Math.max(0, netAmount)
|
|
3084
|
+
};
|
|
3085
|
+
}
|
|
3086
|
+
/**
|
|
3087
|
+
* Apply salary increment (immutable)
|
|
3088
|
+
*/
|
|
3089
|
+
static applyIncrement(compensation, params) {
|
|
3090
|
+
const newBaseAmount = params.amount ? compensation.baseAmount + params.amount : compensation.baseAmount * (1 + (params.percentage || 0) / 100);
|
|
3091
|
+
return this.updateBaseAmount(
|
|
3092
|
+
compensation,
|
|
3093
|
+
Math.round(newBaseAmount),
|
|
3094
|
+
params.effectiveFrom
|
|
3095
|
+
);
|
|
3096
|
+
}
|
|
3097
|
+
};
|
|
3098
|
+
var CompensationBuilder = class {
|
|
3099
|
+
data = {
|
|
3100
|
+
baseAmount: 0,
|
|
3101
|
+
frequency: "monthly",
|
|
3102
|
+
currency: HRM_CONFIG.payroll.defaultCurrency,
|
|
3103
|
+
allowances: [],
|
|
3104
|
+
deductions: []
|
|
3105
|
+
};
|
|
3106
|
+
/**
|
|
3107
|
+
* Set base amount
|
|
3108
|
+
*/
|
|
3109
|
+
withBase(amount, frequency = "monthly", currency = HRM_CONFIG.payroll.defaultCurrency) {
|
|
3110
|
+
this.data.baseAmount = amount;
|
|
3111
|
+
this.data.frequency = frequency;
|
|
3112
|
+
this.data.currency = currency;
|
|
3113
|
+
return this;
|
|
3114
|
+
}
|
|
3115
|
+
/**
|
|
3116
|
+
* Add allowance
|
|
3117
|
+
*/
|
|
3118
|
+
addAllowance(type, value, isPercentage = false, name) {
|
|
3119
|
+
this.data.allowances = [
|
|
3120
|
+
...this.data.allowances || [],
|
|
3121
|
+
{ type, value, isPercentage, name }
|
|
3122
|
+
];
|
|
3123
|
+
return this;
|
|
3124
|
+
}
|
|
3125
|
+
/**
|
|
3126
|
+
* Add deduction
|
|
3127
|
+
*/
|
|
3128
|
+
addDeduction(type, value, isPercentage = false, name) {
|
|
3129
|
+
this.data.deductions = [
|
|
3130
|
+
...this.data.deductions || [],
|
|
3131
|
+
{ type, value, isPercentage, name }
|
|
3132
|
+
];
|
|
3133
|
+
return this;
|
|
3134
|
+
}
|
|
3135
|
+
/**
|
|
3136
|
+
* Set effective date
|
|
3137
|
+
*/
|
|
3138
|
+
effectiveFrom(date) {
|
|
3139
|
+
this.data.effectiveFrom = date;
|
|
3140
|
+
return this;
|
|
3141
|
+
}
|
|
3142
|
+
/**
|
|
3143
|
+
* Build compensation
|
|
3144
|
+
*/
|
|
3145
|
+
build() {
|
|
3146
|
+
if (!this.data.baseAmount) {
|
|
3147
|
+
throw new Error("baseAmount is required");
|
|
3148
|
+
}
|
|
3149
|
+
return CompensationFactory.create(this.data);
|
|
3150
|
+
}
|
|
3151
|
+
};
|
|
3152
|
+
var CompensationPresets = {
|
|
3153
|
+
/**
|
|
3154
|
+
* Basic compensation (base only)
|
|
3155
|
+
*/
|
|
3156
|
+
basic(baseAmount) {
|
|
3157
|
+
return new CompensationBuilder().withBase(baseAmount).build();
|
|
3158
|
+
},
|
|
3159
|
+
/**
|
|
3160
|
+
* With house rent allowance
|
|
3161
|
+
*/
|
|
3162
|
+
withHouseRent(baseAmount, rentPercentage = 50) {
|
|
3163
|
+
return new CompensationBuilder().withBase(baseAmount).addAllowance("housing", rentPercentage, true, "House Rent").build();
|
|
3164
|
+
},
|
|
3165
|
+
/**
|
|
3166
|
+
* With medical allowance
|
|
3167
|
+
*/
|
|
3168
|
+
withMedical(baseAmount, medicalPercentage = 10) {
|
|
3169
|
+
return new CompensationBuilder().withBase(baseAmount).addAllowance("medical", medicalPercentage, true, "Medical Allowance").build();
|
|
3170
|
+
},
|
|
3171
|
+
/**
|
|
3172
|
+
* Standard package (house rent + medical + transport)
|
|
3173
|
+
*/
|
|
3174
|
+
standard(baseAmount) {
|
|
3175
|
+
return new CompensationBuilder().withBase(baseAmount).addAllowance("housing", 50, true, "House Rent").addAllowance("medical", 10, true, "Medical Allowance").addAllowance("transport", 5, true, "Transport Allowance").build();
|
|
3176
|
+
},
|
|
3177
|
+
/**
|
|
3178
|
+
* With provident fund
|
|
3179
|
+
*/
|
|
3180
|
+
withProvidentFund(baseAmount, pfPercentage = 10) {
|
|
3181
|
+
return new CompensationBuilder().withBase(baseAmount).addAllowance("housing", 50, true, "House Rent").addAllowance("medical", 10, true, "Medical Allowance").addDeduction("provident_fund", pfPercentage, true, "Provident Fund").build();
|
|
3182
|
+
},
|
|
3183
|
+
/**
|
|
3184
|
+
* Executive package
|
|
3185
|
+
*/
|
|
3186
|
+
executive(baseAmount) {
|
|
3187
|
+
return new CompensationBuilder().withBase(baseAmount).addAllowance("housing", 60, true, "House Rent").addAllowance("medical", 15, true, "Medical Allowance").addAllowance("transport", 10, true, "Transport Allowance").addAllowance("mobile", 5, true, "Mobile Allowance").addDeduction("provident_fund", 10, true, "Provident Fund").build();
|
|
3188
|
+
}
|
|
3189
|
+
};
|
|
3190
|
+
|
|
3191
|
+
// src/plugins/employee.plugin.ts
|
|
3192
|
+
function employeePlugin(schema, options = {}) {
|
|
3193
|
+
const {
|
|
3194
|
+
requireBankDetails = false,
|
|
3195
|
+
compensationField = "compensation",
|
|
3196
|
+
statusField = "status",
|
|
3197
|
+
autoCalculateSalary = true
|
|
3198
|
+
} = options;
|
|
3199
|
+
schema.virtual("currentSalary").get(function() {
|
|
3200
|
+
const compensation = this[compensationField];
|
|
3201
|
+
return compensation?.netSalary || 0;
|
|
3202
|
+
});
|
|
3203
|
+
schema.virtual("isActive").get(function() {
|
|
3204
|
+
return isActive({ status: this[statusField] });
|
|
3205
|
+
});
|
|
3206
|
+
schema.virtual("isTerminated").get(function() {
|
|
3207
|
+
return isTerminated({ status: this[statusField] });
|
|
3208
|
+
});
|
|
3209
|
+
schema.virtual("yearsOfService").get(function() {
|
|
3210
|
+
const hireDate = this.hireDate;
|
|
3211
|
+
const terminationDate = this.terminationDate;
|
|
3212
|
+
if (!hireDate) return 0;
|
|
3213
|
+
const end = terminationDate || /* @__PURE__ */ new Date();
|
|
3214
|
+
const days = diffInDays(hireDate, end);
|
|
3215
|
+
return Math.max(0, Math.floor(days / 365.25 * 10) / 10);
|
|
3216
|
+
});
|
|
3217
|
+
schema.virtual("isOnProbation").get(function() {
|
|
3218
|
+
const probationEndDate = this.probationEndDate;
|
|
3219
|
+
if (!probationEndDate) return false;
|
|
3220
|
+
return /* @__PURE__ */ new Date() < new Date(probationEndDate);
|
|
3221
|
+
});
|
|
3222
|
+
schema.methods.calculateSalary = function() {
|
|
3223
|
+
const compensation = this[compensationField];
|
|
3224
|
+
if (!compensation) {
|
|
3225
|
+
return { gross: 0, deductions: 0, net: 0 };
|
|
3226
|
+
}
|
|
3227
|
+
const breakdown = CompensationFactory.calculateBreakdown(compensation);
|
|
3228
|
+
return {
|
|
3229
|
+
gross: breakdown.grossAmount,
|
|
3230
|
+
deductions: sumDeductions(
|
|
3231
|
+
breakdown.deductions.map((d) => ({ amount: d.calculatedAmount }))
|
|
3232
|
+
),
|
|
3233
|
+
net: breakdown.netAmount
|
|
3234
|
+
};
|
|
3235
|
+
};
|
|
3236
|
+
schema.methods.updateSalaryCalculations = function() {
|
|
3237
|
+
const compensation = this[compensationField];
|
|
3238
|
+
if (!compensation) return;
|
|
3239
|
+
const calculated = this.calculateSalary();
|
|
3240
|
+
this[compensationField].grossSalary = calculated.gross;
|
|
3241
|
+
this[compensationField].netSalary = calculated.net;
|
|
3242
|
+
this[compensationField].lastModified = /* @__PURE__ */ new Date();
|
|
3243
|
+
};
|
|
3244
|
+
schema.methods.canReceiveSalary = function() {
|
|
3245
|
+
const status = this[statusField];
|
|
3246
|
+
const compensation = this[compensationField];
|
|
3247
|
+
const bankDetails = this.bankDetails;
|
|
3248
|
+
return status === "active" && (compensation?.baseAmount ?? 0) > 0 && (!requireBankDetails || !!bankDetails?.accountNumber);
|
|
3249
|
+
};
|
|
3250
|
+
schema.methods.addAllowance = function(type, amount, taxable = true) {
|
|
3251
|
+
const compensation = this[compensationField];
|
|
3252
|
+
if (!compensation.allowances) {
|
|
3253
|
+
compensation.allowances = [];
|
|
3254
|
+
}
|
|
3255
|
+
compensation.allowances.push({
|
|
3256
|
+
type,
|
|
3257
|
+
name: type,
|
|
3258
|
+
amount,
|
|
3259
|
+
taxable,
|
|
3260
|
+
recurring: true,
|
|
3261
|
+
effectiveFrom: /* @__PURE__ */ new Date()
|
|
3262
|
+
});
|
|
3263
|
+
this.updateSalaryCalculations();
|
|
3264
|
+
};
|
|
3265
|
+
schema.methods.addDeduction = function(type, amount, auto = false, description = "") {
|
|
3266
|
+
const compensation = this[compensationField];
|
|
3267
|
+
if (!compensation.deductions) {
|
|
3268
|
+
compensation.deductions = [];
|
|
3269
|
+
}
|
|
3270
|
+
compensation.deductions.push({
|
|
3271
|
+
type,
|
|
3272
|
+
name: type,
|
|
3273
|
+
amount,
|
|
3274
|
+
auto,
|
|
3275
|
+
recurring: true,
|
|
3276
|
+
description,
|
|
3277
|
+
effectiveFrom: /* @__PURE__ */ new Date()
|
|
3278
|
+
});
|
|
3279
|
+
this.updateSalaryCalculations();
|
|
3280
|
+
};
|
|
3281
|
+
schema.methods.removeAllowance = function(type) {
|
|
3282
|
+
const compensation = this[compensationField];
|
|
3283
|
+
if (!compensation.allowances) return;
|
|
3284
|
+
compensation.allowances = compensation.allowances.filter((a) => a.type !== type);
|
|
3285
|
+
this.updateSalaryCalculations();
|
|
3286
|
+
};
|
|
3287
|
+
schema.methods.removeDeduction = function(type) {
|
|
3288
|
+
const compensation = this[compensationField];
|
|
3289
|
+
if (!compensation.deductions) return;
|
|
3290
|
+
compensation.deductions = compensation.deductions.filter((d) => d.type !== type);
|
|
3291
|
+
this.updateSalaryCalculations();
|
|
3292
|
+
};
|
|
3293
|
+
schema.methods.terminate = function(reason, terminationDate = /* @__PURE__ */ new Date()) {
|
|
3294
|
+
const status = this[statusField];
|
|
3295
|
+
if (status === "terminated") {
|
|
3296
|
+
throw new Error("Employee already terminated");
|
|
3297
|
+
}
|
|
3298
|
+
const compensation = this[compensationField];
|
|
3299
|
+
const employmentHistory = this.employmentHistory || [];
|
|
3300
|
+
employmentHistory.push({
|
|
3301
|
+
hireDate: this.hireDate,
|
|
3302
|
+
terminationDate,
|
|
3303
|
+
reason,
|
|
3304
|
+
finalSalary: compensation?.netSalary || 0,
|
|
3305
|
+
position: this.position,
|
|
3306
|
+
department: this.department
|
|
3307
|
+
});
|
|
3308
|
+
this[statusField] = "terminated";
|
|
3309
|
+
this.terminationDate = terminationDate;
|
|
3310
|
+
this.employmentHistory = employmentHistory;
|
|
3311
|
+
logger.info("Employee terminated", {
|
|
3312
|
+
employeeId: this.employeeId,
|
|
3313
|
+
organizationId: this.organizationId?.toString(),
|
|
3314
|
+
reason
|
|
3315
|
+
});
|
|
3316
|
+
};
|
|
3317
|
+
schema.methods.reHire = function(hireDate = /* @__PURE__ */ new Date(), position, department) {
|
|
3318
|
+
const status = this[statusField];
|
|
3319
|
+
if (status !== "terminated") {
|
|
3320
|
+
throw new Error("Can only re-hire terminated employees");
|
|
3321
|
+
}
|
|
3322
|
+
this[statusField] = "active";
|
|
3323
|
+
this.hireDate = hireDate;
|
|
3324
|
+
this.terminationDate = null;
|
|
3325
|
+
if (position) this.position = position;
|
|
3326
|
+
if (department) this.department = department;
|
|
3327
|
+
logger.info("Employee re-hired", {
|
|
3328
|
+
employeeId: this.employeeId,
|
|
3329
|
+
organizationId: this.organizationId?.toString()
|
|
3330
|
+
});
|
|
3331
|
+
};
|
|
3332
|
+
if (autoCalculateSalary) {
|
|
3333
|
+
schema.pre("save", async function() {
|
|
3334
|
+
if (this.isModified(compensationField)) {
|
|
3335
|
+
this.updateSalaryCalculations();
|
|
3336
|
+
}
|
|
3337
|
+
});
|
|
3338
|
+
}
|
|
3339
|
+
schema.index({ organizationId: 1, employeeId: 1 }, { unique: true });
|
|
3340
|
+
schema.index({ userId: 1, organizationId: 1 }, { unique: true });
|
|
3341
|
+
schema.index({ organizationId: 1, status: 1 });
|
|
3342
|
+
schema.index({ organizationId: 1, department: 1 });
|
|
3343
|
+
schema.index({ organizationId: 1, "compensation.netSalary": -1 });
|
|
3344
|
+
logger.debug("Employee plugin applied", {
|
|
3345
|
+
requireBankDetails,
|
|
3346
|
+
autoCalculateSalary
|
|
3347
|
+
});
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
// src/core/result.ts
|
|
3351
|
+
function ok(value) {
|
|
3352
|
+
return { ok: true, value };
|
|
3353
|
+
}
|
|
3354
|
+
function err(error) {
|
|
3355
|
+
return { ok: false, error };
|
|
3356
|
+
}
|
|
3357
|
+
function isOk(result) {
|
|
3358
|
+
return result.ok === true;
|
|
3359
|
+
}
|
|
3360
|
+
function isErr(result) {
|
|
3361
|
+
return result.ok === false;
|
|
3362
|
+
}
|
|
3363
|
+
function unwrap(result) {
|
|
3364
|
+
if (isOk(result)) {
|
|
3365
|
+
return result.value;
|
|
3366
|
+
}
|
|
3367
|
+
throw result.error;
|
|
3368
|
+
}
|
|
3369
|
+
function unwrapOr(result, defaultValue) {
|
|
3370
|
+
if (isOk(result)) {
|
|
3371
|
+
return result.value;
|
|
3372
|
+
}
|
|
3373
|
+
return defaultValue;
|
|
3374
|
+
}
|
|
3375
|
+
function map(result, fn) {
|
|
3376
|
+
if (isOk(result)) {
|
|
3377
|
+
return ok(fn(result.value));
|
|
3378
|
+
}
|
|
3379
|
+
return result;
|
|
3380
|
+
}
|
|
3381
|
+
function mapErr(result, fn) {
|
|
3382
|
+
if (isErr(result)) {
|
|
3383
|
+
return err(fn(result.error));
|
|
3384
|
+
}
|
|
3385
|
+
return result;
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
// src/factories/payroll.factory.ts
|
|
3389
|
+
var PayrollFactory = class {
|
|
3390
|
+
/**
|
|
3391
|
+
* Create payroll data object
|
|
3392
|
+
*/
|
|
3393
|
+
static create(params) {
|
|
3394
|
+
const {
|
|
3395
|
+
employeeId,
|
|
3396
|
+
organizationId,
|
|
3397
|
+
baseAmount,
|
|
3398
|
+
allowances = [],
|
|
3399
|
+
deductions = [],
|
|
3400
|
+
period = {},
|
|
3401
|
+
metadata = {}
|
|
3402
|
+
} = params;
|
|
3403
|
+
const calculatedAllowances = this.calculateAllowances(baseAmount, allowances);
|
|
3404
|
+
const calculatedDeductions = this.calculateDeductions(baseAmount, deductions);
|
|
3405
|
+
const gross = calculateGross(baseAmount, calculatedAllowances);
|
|
3406
|
+
const net = calculateNet(gross, calculatedDeductions);
|
|
3407
|
+
return {
|
|
3408
|
+
employeeId,
|
|
3409
|
+
organizationId,
|
|
3410
|
+
period: this.createPeriod(period),
|
|
3411
|
+
breakdown: {
|
|
3412
|
+
baseAmount,
|
|
3413
|
+
allowances: calculatedAllowances,
|
|
3414
|
+
deductions: calculatedDeductions,
|
|
3415
|
+
grossSalary: gross,
|
|
3416
|
+
netSalary: net
|
|
3417
|
+
},
|
|
3418
|
+
status: "pending",
|
|
3419
|
+
processedAt: null,
|
|
3420
|
+
paidAt: null,
|
|
3421
|
+
metadata: {
|
|
3422
|
+
currency: metadata.currency || HRM_CONFIG.payroll.defaultCurrency,
|
|
3423
|
+
paymentMethod: metadata.paymentMethod,
|
|
3424
|
+
notes: metadata.notes
|
|
3425
|
+
}
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
3428
|
+
/**
|
|
3429
|
+
* Create pay period
|
|
3430
|
+
*/
|
|
3431
|
+
static createPeriod(params) {
|
|
3432
|
+
const now = /* @__PURE__ */ new Date();
|
|
3433
|
+
const month = params.month || now.getMonth() + 1;
|
|
3434
|
+
const year = params.year || now.getFullYear();
|
|
3435
|
+
const period = getPayPeriod(month, year);
|
|
3436
|
+
return {
|
|
3437
|
+
...period,
|
|
3438
|
+
payDate: params.payDate || /* @__PURE__ */ new Date()
|
|
3439
|
+
};
|
|
3440
|
+
}
|
|
3441
|
+
/**
|
|
3442
|
+
* Calculate allowances from base amount
|
|
3443
|
+
*/
|
|
3444
|
+
static calculateAllowances(baseAmount, allowances) {
|
|
3445
|
+
return allowances.map((allowance) => {
|
|
3446
|
+
const amount = allowance.isPercentage && allowance.value !== void 0 ? Math.round(baseAmount * allowance.value / 100) : allowance.amount;
|
|
3447
|
+
return {
|
|
3448
|
+
type: allowance.type,
|
|
3449
|
+
amount,
|
|
3450
|
+
taxable: allowance.taxable ?? true
|
|
3451
|
+
};
|
|
3452
|
+
});
|
|
3453
|
+
}
|
|
3454
|
+
/**
|
|
3455
|
+
* Calculate deductions from base amount
|
|
3456
|
+
*/
|
|
3457
|
+
static calculateDeductions(baseAmount, deductions) {
|
|
3458
|
+
return deductions.map((deduction) => {
|
|
3459
|
+
const amount = deduction.isPercentage && deduction.value !== void 0 ? Math.round(baseAmount * deduction.value / 100) : deduction.amount;
|
|
3460
|
+
return {
|
|
3461
|
+
type: deduction.type,
|
|
3462
|
+
amount,
|
|
3463
|
+
description: deduction.description
|
|
3464
|
+
};
|
|
3465
|
+
});
|
|
3466
|
+
}
|
|
3467
|
+
/**
|
|
3468
|
+
* Create bonus object
|
|
3469
|
+
*/
|
|
3470
|
+
static createBonus(params) {
|
|
3471
|
+
return {
|
|
3472
|
+
type: params.type,
|
|
3473
|
+
amount: params.amount,
|
|
3474
|
+
reason: params.reason,
|
|
3475
|
+
approvedBy: params.approvedBy,
|
|
3476
|
+
approvedAt: /* @__PURE__ */ new Date()
|
|
3477
|
+
};
|
|
3478
|
+
}
|
|
3479
|
+
/**
|
|
3480
|
+
* Mark payroll as paid (immutable)
|
|
3481
|
+
* Sets both top-level transactionId and metadata for compatibility
|
|
3482
|
+
*/
|
|
3483
|
+
static markAsPaid(payroll3, params = {}) {
|
|
3484
|
+
return {
|
|
3485
|
+
...payroll3,
|
|
3486
|
+
status: "paid",
|
|
3487
|
+
paidAt: params.paidAt || /* @__PURE__ */ new Date(),
|
|
3488
|
+
processedAt: payroll3.processedAt || params.paidAt || /* @__PURE__ */ new Date(),
|
|
3489
|
+
transactionId: params.transactionId || payroll3.transactionId,
|
|
3490
|
+
metadata: {
|
|
3491
|
+
...payroll3.metadata,
|
|
3492
|
+
transactionId: params.transactionId,
|
|
3493
|
+
paymentMethod: params.paymentMethod || payroll3.metadata?.paymentMethod
|
|
3494
|
+
}
|
|
3495
|
+
};
|
|
3496
|
+
}
|
|
3497
|
+
/**
|
|
3498
|
+
* Mark payroll as processed (immutable)
|
|
3499
|
+
*/
|
|
3500
|
+
static markAsProcessed(payroll3, params = {}) {
|
|
3501
|
+
return {
|
|
3502
|
+
...payroll3,
|
|
3503
|
+
status: "processing",
|
|
3504
|
+
processedAt: params.processedAt || /* @__PURE__ */ new Date()
|
|
3505
|
+
};
|
|
3506
|
+
}
|
|
3507
|
+
};
|
|
3508
|
+
var BatchPayrollFactory = class {
|
|
3509
|
+
/**
|
|
3510
|
+
* Create payroll records for multiple employees
|
|
3511
|
+
*/
|
|
3512
|
+
static createBatch(employees, params) {
|
|
3513
|
+
return employees.map(
|
|
3514
|
+
(employee2) => PayrollFactory.create({
|
|
3515
|
+
employeeId: employee2._id,
|
|
3516
|
+
organizationId: params.organizationId || employee2.organizationId,
|
|
3517
|
+
baseAmount: employee2.compensation.baseAmount,
|
|
3518
|
+
allowances: employee2.compensation.allowances || [],
|
|
3519
|
+
deductions: employee2.compensation.deductions || [],
|
|
3520
|
+
period: { month: params.month, year: params.year },
|
|
3521
|
+
metadata: { currency: employee2.compensation.currency }
|
|
3522
|
+
})
|
|
3523
|
+
);
|
|
3524
|
+
}
|
|
3525
|
+
/**
|
|
3526
|
+
* Calculate total payroll amounts
|
|
3527
|
+
*/
|
|
3528
|
+
static calculateTotalPayroll(payrolls) {
|
|
3529
|
+
return payrolls.reduce(
|
|
3530
|
+
(totals, payroll3) => ({
|
|
3531
|
+
count: totals.count + 1,
|
|
3532
|
+
totalGross: totals.totalGross + payroll3.breakdown.grossSalary,
|
|
3533
|
+
totalNet: totals.totalNet + payroll3.breakdown.netSalary,
|
|
3534
|
+
totalAllowances: totals.totalAllowances + sumAllowances(payroll3.breakdown.allowances),
|
|
3535
|
+
totalDeductions: totals.totalDeductions + sumDeductions(payroll3.breakdown.deductions)
|
|
3536
|
+
}),
|
|
3537
|
+
{ count: 0, totalGross: 0, totalNet: 0, totalAllowances: 0, totalDeductions: 0 }
|
|
3538
|
+
);
|
|
3539
|
+
}
|
|
3540
|
+
};
|
|
3541
|
+
|
|
3542
|
+
// src/services/employee.service.ts
|
|
3543
|
+
var EmployeeService = class {
|
|
3544
|
+
constructor(EmployeeModel) {
|
|
3545
|
+
this.EmployeeModel = EmployeeModel;
|
|
3546
|
+
}
|
|
3547
|
+
/**
|
|
3548
|
+
* Find employee by ID
|
|
3549
|
+
*/
|
|
3550
|
+
async findById(employeeId, options = {}) {
|
|
3551
|
+
let query = this.EmployeeModel.findById(toObjectId(employeeId));
|
|
3552
|
+
if (options.session) {
|
|
3553
|
+
query = query.session(options.session);
|
|
3554
|
+
}
|
|
3555
|
+
if (options.populate) {
|
|
3556
|
+
query = query.populate("userId", "name email phone");
|
|
3557
|
+
}
|
|
3558
|
+
return query.exec();
|
|
3559
|
+
}
|
|
3560
|
+
/**
|
|
3561
|
+
* Find employee by user and organization
|
|
3562
|
+
*/
|
|
3563
|
+
async findByUserId(userId, organizationId, options = {}) {
|
|
3564
|
+
const query = employee().forUser(userId).forOrganization(organizationId).build();
|
|
3565
|
+
let mongooseQuery = this.EmployeeModel.findOne(query);
|
|
3566
|
+
if (options.session) {
|
|
3567
|
+
mongooseQuery = mongooseQuery.session(options.session);
|
|
3568
|
+
}
|
|
3569
|
+
return mongooseQuery.exec();
|
|
3570
|
+
}
|
|
3571
|
+
/**
|
|
3572
|
+
* Find active employees in organization
|
|
3573
|
+
*/
|
|
3574
|
+
async findActive(organizationId, options = {}) {
|
|
3575
|
+
const query = employee().forOrganization(organizationId).active().build();
|
|
3576
|
+
let mongooseQuery = this.EmployeeModel.find(query, options.projection);
|
|
3577
|
+
if (options.session) {
|
|
3578
|
+
mongooseQuery = mongooseQuery.session(options.session);
|
|
3579
|
+
}
|
|
3580
|
+
return mongooseQuery.exec();
|
|
3581
|
+
}
|
|
3582
|
+
/**
|
|
3583
|
+
* Find employed employees (not terminated)
|
|
3584
|
+
*/
|
|
3585
|
+
async findEmployed(organizationId, options = {}) {
|
|
3586
|
+
const query = employee().forOrganization(organizationId).employed().build();
|
|
3587
|
+
let mongooseQuery = this.EmployeeModel.find(query, options.projection);
|
|
3588
|
+
if (options.session) {
|
|
3589
|
+
mongooseQuery = mongooseQuery.session(options.session);
|
|
3590
|
+
}
|
|
3591
|
+
return mongooseQuery.exec();
|
|
3592
|
+
}
|
|
3593
|
+
/**
|
|
3594
|
+
* Find employees by department
|
|
3595
|
+
*/
|
|
3596
|
+
async findByDepartment(organizationId, department, options = {}) {
|
|
3597
|
+
const query = employee().forOrganization(organizationId).inDepartment(department).active().build();
|
|
3598
|
+
let mongooseQuery = this.EmployeeModel.find(query);
|
|
3599
|
+
if (options.session) {
|
|
3600
|
+
mongooseQuery = mongooseQuery.session(options.session);
|
|
3601
|
+
}
|
|
3602
|
+
return mongooseQuery.exec();
|
|
3603
|
+
}
|
|
3604
|
+
/**
|
|
3605
|
+
* Find employees eligible for payroll
|
|
3606
|
+
*/
|
|
3607
|
+
async findEligibleForPayroll(organizationId, options = {}) {
|
|
3608
|
+
const query = employee().forOrganization(organizationId).employed().build();
|
|
3609
|
+
let mongooseQuery = this.EmployeeModel.find(query);
|
|
3610
|
+
if (options.session) {
|
|
3611
|
+
mongooseQuery = mongooseQuery.session(options.session);
|
|
3612
|
+
}
|
|
3613
|
+
const employees = await mongooseQuery.exec();
|
|
3614
|
+
return employees.filter((emp) => canReceiveSalary(emp));
|
|
3615
|
+
}
|
|
3616
|
+
/**
|
|
3617
|
+
* Create new employee
|
|
3618
|
+
*/
|
|
3619
|
+
async create(params, options = {}) {
|
|
3620
|
+
const employeeData = EmployeeFactory.create(params);
|
|
3621
|
+
const [employee2] = await this.EmployeeModel.create([employeeData], {
|
|
3622
|
+
session: options.session
|
|
3623
|
+
});
|
|
3624
|
+
logger.info("Employee created", {
|
|
3625
|
+
employeeId: employee2.employeeId,
|
|
3626
|
+
organizationId: employee2.organizationId.toString()
|
|
3627
|
+
});
|
|
3628
|
+
return employee2;
|
|
3629
|
+
}
|
|
3630
|
+
/**
|
|
3631
|
+
* Update employee status
|
|
3632
|
+
*/
|
|
3633
|
+
async updateStatus(employeeId, status, context = {}, options = {}) {
|
|
3634
|
+
const employee2 = await this.findById(employeeId, options);
|
|
3635
|
+
if (!employee2) {
|
|
3636
|
+
throw new Error("Employee not found");
|
|
3637
|
+
}
|
|
3638
|
+
employee2.status = status;
|
|
3639
|
+
await employee2.save({ session: options.session });
|
|
3640
|
+
logger.info("Employee status updated", {
|
|
3641
|
+
employeeId: employee2.employeeId,
|
|
3642
|
+
newStatus: status
|
|
3643
|
+
});
|
|
3644
|
+
return employee2;
|
|
3645
|
+
}
|
|
3646
|
+
/**
|
|
3647
|
+
* Update employee compensation
|
|
3648
|
+
*
|
|
3649
|
+
* NOTE: This merges the compensation fields rather than replacing the entire object.
|
|
3650
|
+
* To update allowances/deductions, use addAllowance/removeAllowance methods.
|
|
3651
|
+
*/
|
|
3652
|
+
async updateCompensation(employeeId, compensation, options = {}) {
|
|
3653
|
+
const currentEmployee = await this.EmployeeModel.findById(toObjectId(employeeId)).session(options.session || null);
|
|
3654
|
+
if (!currentEmployee) {
|
|
3655
|
+
throw new Error("Employee not found");
|
|
3656
|
+
}
|
|
3657
|
+
const updateFields = {
|
|
3658
|
+
"compensation.lastModified": /* @__PURE__ */ new Date()
|
|
3659
|
+
};
|
|
3660
|
+
if (compensation.baseAmount !== void 0) {
|
|
3661
|
+
updateFields["compensation.baseAmount"] = compensation.baseAmount;
|
|
3662
|
+
}
|
|
3663
|
+
if (compensation.currency !== void 0) {
|
|
3664
|
+
updateFields["compensation.currency"] = compensation.currency;
|
|
3665
|
+
}
|
|
3666
|
+
if (compensation.frequency !== void 0) {
|
|
3667
|
+
updateFields["compensation.frequency"] = compensation.frequency;
|
|
3668
|
+
}
|
|
3669
|
+
if (compensation.effectiveFrom !== void 0) {
|
|
3670
|
+
updateFields["compensation.effectiveFrom"] = compensation.effectiveFrom;
|
|
3671
|
+
}
|
|
3672
|
+
const employee2 = await this.EmployeeModel.findByIdAndUpdate(
|
|
3673
|
+
toObjectId(employeeId),
|
|
3674
|
+
{ $set: updateFields },
|
|
3675
|
+
{ new: true, runValidators: true, session: options.session }
|
|
3676
|
+
);
|
|
3677
|
+
if (!employee2) {
|
|
3678
|
+
throw new Error("Employee not found");
|
|
3679
|
+
}
|
|
3680
|
+
return employee2;
|
|
3681
|
+
}
|
|
3682
|
+
/**
|
|
3683
|
+
* Get employee statistics for organization
|
|
3684
|
+
*/
|
|
3685
|
+
async getEmployeeStats(organizationId, options = {}) {
|
|
3686
|
+
const query = employee().forOrganization(organizationId).build();
|
|
3687
|
+
let mongooseQuery = this.EmployeeModel.find(query);
|
|
3688
|
+
if (options.session) {
|
|
3689
|
+
mongooseQuery = mongooseQuery.session(options.session);
|
|
3690
|
+
}
|
|
3691
|
+
const employees = await mongooseQuery.exec();
|
|
3692
|
+
return {
|
|
3693
|
+
total: employees.length,
|
|
3694
|
+
active: employees.filter(isActive).length,
|
|
3695
|
+
employed: employees.filter(isEmployed).length,
|
|
3696
|
+
canReceiveSalary: employees.filter(canReceiveSalary).length,
|
|
3697
|
+
byStatus: this.groupByStatus(employees),
|
|
3698
|
+
byDepartment: this.groupByDepartment(employees)
|
|
3699
|
+
};
|
|
3700
|
+
}
|
|
3701
|
+
/**
|
|
3702
|
+
* Group employees by status
|
|
3703
|
+
*/
|
|
3704
|
+
groupByStatus(employees) {
|
|
3705
|
+
return employees.reduce(
|
|
3706
|
+
(acc, emp) => {
|
|
3707
|
+
acc[emp.status] = (acc[emp.status] || 0) + 1;
|
|
3708
|
+
return acc;
|
|
3709
|
+
},
|
|
3710
|
+
{}
|
|
3711
|
+
);
|
|
3712
|
+
}
|
|
3713
|
+
/**
|
|
3714
|
+
* Group employees by department
|
|
3715
|
+
*/
|
|
3716
|
+
groupByDepartment(employees) {
|
|
3717
|
+
return employees.reduce(
|
|
3718
|
+
(acc, emp) => {
|
|
3719
|
+
const dept = emp.department || "unassigned";
|
|
3720
|
+
acc[dept] = (acc[dept] || 0) + 1;
|
|
3721
|
+
return acc;
|
|
3722
|
+
},
|
|
3723
|
+
{}
|
|
3724
|
+
);
|
|
3725
|
+
}
|
|
3726
|
+
/**
|
|
3727
|
+
* Check if employee is active
|
|
3728
|
+
*/
|
|
3729
|
+
isActive(employee2) {
|
|
3730
|
+
return isActive(employee2);
|
|
3731
|
+
}
|
|
3732
|
+
/**
|
|
3733
|
+
* Check if employee is employed
|
|
3734
|
+
*/
|
|
3735
|
+
isEmployed(employee2) {
|
|
3736
|
+
return isEmployed(employee2);
|
|
3737
|
+
}
|
|
3738
|
+
/**
|
|
3739
|
+
* Check if employee can receive salary
|
|
3740
|
+
*/
|
|
3741
|
+
canReceiveSalary(employee2) {
|
|
3742
|
+
return canReceiveSalary(employee2);
|
|
3743
|
+
}
|
|
3744
|
+
};
|
|
3745
|
+
|
|
3746
|
+
// src/services/payroll.service.ts
|
|
3747
|
+
var PayrollService = class {
|
|
3748
|
+
constructor(PayrollModel, employeeService) {
|
|
3749
|
+
this.PayrollModel = PayrollModel;
|
|
3750
|
+
this.employeeService = employeeService;
|
|
3751
|
+
}
|
|
3752
|
+
/**
|
|
3753
|
+
* Find payroll by ID
|
|
3754
|
+
*/
|
|
3755
|
+
async findById(payrollId, options = {}) {
|
|
3756
|
+
let query = this.PayrollModel.findById(toObjectId(payrollId));
|
|
3757
|
+
if (options.session) {
|
|
3758
|
+
query = query.session(options.session);
|
|
3759
|
+
}
|
|
3760
|
+
return query.exec();
|
|
3761
|
+
}
|
|
3762
|
+
/**
|
|
3763
|
+
* Find payrolls by employee
|
|
3764
|
+
*/
|
|
3765
|
+
async findByEmployee(employeeId, organizationId, options = {}) {
|
|
3766
|
+
const query = payroll().forEmployee(employeeId).forOrganization(organizationId).build();
|
|
3767
|
+
let mongooseQuery = this.PayrollModel.find(query).sort({ "period.year": -1, "period.month": -1 }).limit(options.limit || 12);
|
|
3768
|
+
if (options.session) {
|
|
3769
|
+
mongooseQuery = mongooseQuery.session(options.session);
|
|
3770
|
+
}
|
|
3771
|
+
return mongooseQuery.exec();
|
|
3772
|
+
}
|
|
3773
|
+
/**
|
|
3774
|
+
* Find payrolls for a period
|
|
3775
|
+
*/
|
|
3776
|
+
async findForPeriod(organizationId, month, year, options = {}) {
|
|
3777
|
+
const query = payroll().forOrganization(organizationId).forPeriod(month, year).build();
|
|
3778
|
+
let mongooseQuery = this.PayrollModel.find(query);
|
|
3779
|
+
if (options.session) {
|
|
3780
|
+
mongooseQuery = mongooseQuery.session(options.session);
|
|
3781
|
+
}
|
|
3782
|
+
return mongooseQuery.exec();
|
|
3783
|
+
}
|
|
3784
|
+
/**
|
|
3785
|
+
* Find pending payrolls
|
|
3786
|
+
*/
|
|
3787
|
+
async findPending(organizationId, month, year, options = {}) {
|
|
3788
|
+
const query = payroll().forOrganization(organizationId).forPeriod(month, year).pending().build();
|
|
3789
|
+
let mongooseQuery = this.PayrollModel.find(query);
|
|
3790
|
+
if (options.session) {
|
|
3791
|
+
mongooseQuery = mongooseQuery.session(options.session);
|
|
3792
|
+
}
|
|
3793
|
+
return mongooseQuery.exec();
|
|
3794
|
+
}
|
|
3795
|
+
/**
|
|
3796
|
+
* Find payroll by employee and period
|
|
3797
|
+
*/
|
|
3798
|
+
async findByEmployeeAndPeriod(employeeId, organizationId, month, year, options = {}) {
|
|
3799
|
+
const query = payroll().forEmployee(employeeId).forOrganization(organizationId).forPeriod(month, year).build();
|
|
3800
|
+
let mongooseQuery = this.PayrollModel.findOne(query);
|
|
3801
|
+
if (options.session) {
|
|
3802
|
+
mongooseQuery = mongooseQuery.session(options.session);
|
|
3803
|
+
}
|
|
3804
|
+
return mongooseQuery.exec();
|
|
3805
|
+
}
|
|
3806
|
+
/**
|
|
3807
|
+
* Create payroll record
|
|
3808
|
+
*/
|
|
3809
|
+
async create(data, options = {}) {
|
|
3810
|
+
const [payroll3] = await this.PayrollModel.create([data], {
|
|
3811
|
+
session: options.session
|
|
3812
|
+
});
|
|
3813
|
+
logger.info("Payroll record created", {
|
|
3814
|
+
payrollId: payroll3._id.toString(),
|
|
3815
|
+
employeeId: payroll3.employeeId.toString()
|
|
3816
|
+
});
|
|
3817
|
+
return payroll3;
|
|
3818
|
+
}
|
|
3819
|
+
/**
|
|
3820
|
+
* Generate payroll for employee
|
|
3821
|
+
*/
|
|
3822
|
+
async generateForEmployee(employeeId, organizationId, month, year, options = {}) {
|
|
3823
|
+
const employee2 = await this.employeeService.findById(employeeId, options);
|
|
3824
|
+
if (!employee2) {
|
|
3825
|
+
throw new Error("Employee not found");
|
|
3826
|
+
}
|
|
3827
|
+
if (!canReceiveSalary(employee2)) {
|
|
3828
|
+
throw new Error("Employee not eligible for payroll");
|
|
3829
|
+
}
|
|
3830
|
+
const existing = await this.findByEmployeeAndPeriod(
|
|
3831
|
+
employeeId,
|
|
3832
|
+
organizationId,
|
|
3833
|
+
month,
|
|
3834
|
+
year,
|
|
3835
|
+
options
|
|
3836
|
+
);
|
|
3837
|
+
if (existing) {
|
|
3838
|
+
throw new Error("Payroll already exists for this period");
|
|
3839
|
+
}
|
|
3840
|
+
const payrollData = PayrollFactory.create({
|
|
3841
|
+
employeeId,
|
|
3842
|
+
organizationId,
|
|
3843
|
+
baseAmount: employee2.compensation.baseAmount,
|
|
3844
|
+
allowances: employee2.compensation.allowances || [],
|
|
3845
|
+
deductions: employee2.compensation.deductions || [],
|
|
3846
|
+
period: { month, year },
|
|
3847
|
+
metadata: { currency: employee2.compensation.currency }
|
|
3848
|
+
});
|
|
3849
|
+
return this.create(payrollData, options);
|
|
3850
|
+
}
|
|
3851
|
+
/**
|
|
3852
|
+
* Generate batch payroll
|
|
3853
|
+
*/
|
|
3854
|
+
async generateBatch(organizationId, month, year, options = {}) {
|
|
3855
|
+
const employees = await this.employeeService.findEligibleForPayroll(
|
|
3856
|
+
organizationId,
|
|
3857
|
+
options
|
|
3858
|
+
);
|
|
3859
|
+
if (employees.length === 0) {
|
|
3860
|
+
return {
|
|
3861
|
+
success: true,
|
|
3862
|
+
generated: 0,
|
|
3863
|
+
skipped: 0,
|
|
3864
|
+
payrolls: [],
|
|
3865
|
+
message: "No eligible employees"
|
|
3866
|
+
};
|
|
3867
|
+
}
|
|
3868
|
+
const existingPayrolls = await this.findForPeriod(
|
|
3869
|
+
organizationId,
|
|
3870
|
+
month,
|
|
3871
|
+
year,
|
|
3872
|
+
options
|
|
3873
|
+
);
|
|
3874
|
+
const existingEmployeeIds = new Set(
|
|
3875
|
+
existingPayrolls.map((p) => p.employeeId.toString())
|
|
3876
|
+
);
|
|
3877
|
+
const eligibleEmployees = employees.filter(
|
|
3878
|
+
(emp) => !existingEmployeeIds.has(emp._id.toString())
|
|
3879
|
+
);
|
|
3880
|
+
if (eligibleEmployees.length === 0) {
|
|
3881
|
+
return {
|
|
3882
|
+
success: true,
|
|
3883
|
+
generated: 0,
|
|
3884
|
+
skipped: employees.length,
|
|
3885
|
+
payrolls: [],
|
|
3886
|
+
message: "Payrolls already exist for all employees"
|
|
3887
|
+
};
|
|
3888
|
+
}
|
|
3889
|
+
const payrollsData = BatchPayrollFactory.createBatch(eligibleEmployees, {
|
|
3890
|
+
month,
|
|
3891
|
+
year,
|
|
3892
|
+
organizationId
|
|
3893
|
+
});
|
|
3894
|
+
const created = await this.PayrollModel.insertMany(payrollsData, {
|
|
3895
|
+
session: options.session
|
|
3896
|
+
});
|
|
3897
|
+
logger.info("Batch payroll generated", {
|
|
3898
|
+
organizationId: organizationId.toString(),
|
|
3899
|
+
month,
|
|
3900
|
+
year,
|
|
3901
|
+
count: created.length
|
|
3902
|
+
});
|
|
3903
|
+
return {
|
|
3904
|
+
success: true,
|
|
3905
|
+
generated: created.length,
|
|
3906
|
+
skipped: existingEmployeeIds.size,
|
|
3907
|
+
payrolls: created,
|
|
3908
|
+
message: `Generated ${created.length} payrolls`
|
|
3909
|
+
};
|
|
3910
|
+
}
|
|
3911
|
+
/**
|
|
3912
|
+
* Mark payroll as paid
|
|
3913
|
+
*/
|
|
3914
|
+
async markAsPaid(payrollId, paymentDetails = {}, options = {}) {
|
|
3915
|
+
const payroll3 = await this.findById(payrollId, options);
|
|
3916
|
+
if (!payroll3) {
|
|
3917
|
+
throw new Error("Payroll not found");
|
|
3918
|
+
}
|
|
3919
|
+
if (payroll3.status === "paid") {
|
|
3920
|
+
throw new Error("Payroll already paid");
|
|
3921
|
+
}
|
|
3922
|
+
const payrollObj = payroll3.toObject();
|
|
3923
|
+
const updatedData = PayrollFactory.markAsPaid(payrollObj, paymentDetails);
|
|
3924
|
+
const updated = await this.PayrollModel.findByIdAndUpdate(
|
|
3925
|
+
payrollId,
|
|
3926
|
+
updatedData,
|
|
3927
|
+
{ new: true, runValidators: true, session: options.session }
|
|
3928
|
+
);
|
|
3929
|
+
if (!updated) {
|
|
3930
|
+
throw new Error("Failed to update payroll");
|
|
3931
|
+
}
|
|
3932
|
+
logger.info("Payroll marked as paid", {
|
|
3933
|
+
payrollId: payrollId.toString()
|
|
3934
|
+
});
|
|
3935
|
+
return updated;
|
|
3936
|
+
}
|
|
3937
|
+
/**
|
|
3938
|
+
* Mark payroll as processed
|
|
3939
|
+
*/
|
|
3940
|
+
async markAsProcessed(payrollId, options = {}) {
|
|
3941
|
+
const payroll3 = await this.findById(payrollId, options);
|
|
3942
|
+
if (!payroll3) {
|
|
3943
|
+
throw new Error("Payroll not found");
|
|
3944
|
+
}
|
|
3945
|
+
const payrollObj = payroll3.toObject();
|
|
3946
|
+
const updatedData = PayrollFactory.markAsProcessed(payrollObj);
|
|
3947
|
+
const updated = await this.PayrollModel.findByIdAndUpdate(
|
|
3948
|
+
payrollId,
|
|
3949
|
+
updatedData,
|
|
3950
|
+
{ new: true, runValidators: true, session: options.session }
|
|
3951
|
+
);
|
|
3952
|
+
if (!updated) {
|
|
3953
|
+
throw new Error("Failed to update payroll");
|
|
3954
|
+
}
|
|
3955
|
+
return updated;
|
|
3956
|
+
}
|
|
3957
|
+
/**
|
|
3958
|
+
* Calculate period summary
|
|
3959
|
+
*/
|
|
3960
|
+
async calculatePeriodSummary(organizationId, month, year, options = {}) {
|
|
3961
|
+
const payrolls = await this.findForPeriod(organizationId, month, year, options);
|
|
3962
|
+
const summary = BatchPayrollFactory.calculateTotalPayroll(payrolls);
|
|
3963
|
+
return {
|
|
3964
|
+
period: { month, year },
|
|
3965
|
+
...summary,
|
|
3966
|
+
byStatus: this.groupByStatus(payrolls)
|
|
3967
|
+
};
|
|
3968
|
+
}
|
|
3969
|
+
/**
|
|
3970
|
+
* Get employee payroll history
|
|
3971
|
+
*/
|
|
3972
|
+
async getEmployeePayrollHistory(employeeId, organizationId, limit = 12, options = {}) {
|
|
3973
|
+
return this.findByEmployee(employeeId, organizationId, { ...options, limit });
|
|
3974
|
+
}
|
|
3975
|
+
/**
|
|
3976
|
+
* Get overview stats
|
|
3977
|
+
*/
|
|
3978
|
+
async getOverviewStats(organizationId, options = {}) {
|
|
3979
|
+
const { month, year } = getCurrentPeriod();
|
|
3980
|
+
const result = await this.calculatePeriodSummary(organizationId, month, year, options);
|
|
3981
|
+
return {
|
|
3982
|
+
currentPeriod: result.period,
|
|
3983
|
+
count: result.count,
|
|
3984
|
+
totalGross: result.totalGross,
|
|
3985
|
+
totalNet: result.totalNet,
|
|
3986
|
+
totalAllowances: result.totalAllowances,
|
|
3987
|
+
totalDeductions: result.totalDeductions,
|
|
3988
|
+
byStatus: result.byStatus
|
|
3989
|
+
};
|
|
3990
|
+
}
|
|
3991
|
+
/**
|
|
3992
|
+
* Group payrolls by status
|
|
3993
|
+
*/
|
|
3994
|
+
groupByStatus(payrolls) {
|
|
3995
|
+
return payrolls.reduce(
|
|
3996
|
+
(acc, payroll3) => {
|
|
3997
|
+
acc[payroll3.status] = (acc[payroll3.status] || 0) + 1;
|
|
3998
|
+
return acc;
|
|
3999
|
+
},
|
|
4000
|
+
{}
|
|
4001
|
+
);
|
|
4002
|
+
}
|
|
4003
|
+
};
|
|
4004
|
+
|
|
4005
|
+
// src/services/compensation.service.ts
|
|
4006
|
+
var CompensationService = class {
|
|
4007
|
+
constructor(EmployeeModel) {
|
|
4008
|
+
this.EmployeeModel = EmployeeModel;
|
|
4009
|
+
}
|
|
4010
|
+
/**
|
|
4011
|
+
* Get employee compensation
|
|
4012
|
+
*/
|
|
4013
|
+
async getEmployeeCompensation(employeeId, options = {}) {
|
|
4014
|
+
let query = this.EmployeeModel.findById(toObjectId(employeeId));
|
|
4015
|
+
if (options.session) {
|
|
4016
|
+
query = query.session(options.session);
|
|
4017
|
+
}
|
|
4018
|
+
const employee2 = await query.exec();
|
|
4019
|
+
if (!employee2) {
|
|
4020
|
+
throw new Error("Employee not found");
|
|
4021
|
+
}
|
|
4022
|
+
return employee2.compensation;
|
|
4023
|
+
}
|
|
4024
|
+
/**
|
|
4025
|
+
* Calculate compensation breakdown
|
|
4026
|
+
*/
|
|
4027
|
+
async calculateBreakdown(employeeId, options = {}) {
|
|
4028
|
+
const compensation = await this.getEmployeeCompensation(employeeId, options);
|
|
4029
|
+
return CompensationFactory.calculateBreakdown(compensation);
|
|
4030
|
+
}
|
|
4031
|
+
/**
|
|
4032
|
+
* Update base amount
|
|
4033
|
+
*/
|
|
4034
|
+
async updateBaseAmount(employeeId, newAmount, effectiveFrom = /* @__PURE__ */ new Date(), options = {}) {
|
|
4035
|
+
const employee2 = await this.findEmployee(employeeId, options);
|
|
4036
|
+
const updatedCompensation = CompensationFactory.updateBaseAmount(
|
|
4037
|
+
employee2.compensation,
|
|
4038
|
+
newAmount,
|
|
4039
|
+
effectiveFrom
|
|
4040
|
+
);
|
|
4041
|
+
employee2.compensation = updatedCompensation;
|
|
4042
|
+
await employee2.save({ session: options.session });
|
|
4043
|
+
logger.info("Compensation base amount updated", {
|
|
4044
|
+
employeeId: employee2.employeeId,
|
|
4045
|
+
newAmount
|
|
4046
|
+
});
|
|
4047
|
+
return this.calculateBreakdown(employeeId, options);
|
|
4048
|
+
}
|
|
4049
|
+
/**
|
|
4050
|
+
* Apply salary increment
|
|
4051
|
+
*/
|
|
4052
|
+
async applyIncrement(employeeId, params, options = {}) {
|
|
4053
|
+
const employee2 = await this.findEmployee(employeeId, options);
|
|
4054
|
+
const previousAmount = employee2.compensation.baseAmount;
|
|
4055
|
+
const updatedCompensation = CompensationFactory.applyIncrement(
|
|
4056
|
+
employee2.compensation,
|
|
4057
|
+
params
|
|
4058
|
+
);
|
|
4059
|
+
employee2.compensation = updatedCompensation;
|
|
4060
|
+
await employee2.save({ session: options.session });
|
|
4061
|
+
logger.info("Salary increment applied", {
|
|
4062
|
+
employeeId: employee2.employeeId,
|
|
4063
|
+
previousAmount,
|
|
4064
|
+
newAmount: updatedCompensation.baseAmount,
|
|
4065
|
+
percentage: params.percentage
|
|
4066
|
+
});
|
|
4067
|
+
return this.calculateBreakdown(employeeId, options);
|
|
4068
|
+
}
|
|
4069
|
+
/**
|
|
4070
|
+
* Add allowance
|
|
4071
|
+
*/
|
|
4072
|
+
async addAllowance(employeeId, allowance, options = {}) {
|
|
4073
|
+
const employee2 = await this.findEmployee(employeeId, options);
|
|
4074
|
+
const updatedCompensation = CompensationFactory.addAllowance(
|
|
4075
|
+
employee2.compensation,
|
|
4076
|
+
allowance
|
|
4077
|
+
);
|
|
4078
|
+
employee2.compensation = updatedCompensation;
|
|
4079
|
+
await employee2.save({ session: options.session });
|
|
4080
|
+
logger.info("Allowance added", {
|
|
4081
|
+
employeeId: employee2.employeeId,
|
|
4082
|
+
type: allowance.type,
|
|
4083
|
+
value: allowance.value
|
|
4084
|
+
});
|
|
4085
|
+
return this.calculateBreakdown(employeeId, options);
|
|
4086
|
+
}
|
|
4087
|
+
/**
|
|
4088
|
+
* Remove allowance
|
|
4089
|
+
*/
|
|
4090
|
+
async removeAllowance(employeeId, allowanceType, options = {}) {
|
|
4091
|
+
const employee2 = await this.findEmployee(employeeId, options);
|
|
4092
|
+
const updatedCompensation = CompensationFactory.removeAllowance(
|
|
4093
|
+
employee2.compensation,
|
|
4094
|
+
allowanceType
|
|
4095
|
+
);
|
|
4096
|
+
employee2.compensation = updatedCompensation;
|
|
4097
|
+
await employee2.save({ session: options.session });
|
|
4098
|
+
logger.info("Allowance removed", {
|
|
4099
|
+
employeeId: employee2.employeeId,
|
|
4100
|
+
type: allowanceType
|
|
4101
|
+
});
|
|
4102
|
+
return this.calculateBreakdown(employeeId, options);
|
|
4103
|
+
}
|
|
4104
|
+
/**
|
|
4105
|
+
* Add deduction
|
|
4106
|
+
*/
|
|
4107
|
+
async addDeduction(employeeId, deduction, options = {}) {
|
|
4108
|
+
const employee2 = await this.findEmployee(employeeId, options);
|
|
4109
|
+
const updatedCompensation = CompensationFactory.addDeduction(
|
|
4110
|
+
employee2.compensation,
|
|
4111
|
+
deduction
|
|
4112
|
+
);
|
|
4113
|
+
employee2.compensation = updatedCompensation;
|
|
4114
|
+
await employee2.save({ session: options.session });
|
|
4115
|
+
logger.info("Deduction added", {
|
|
4116
|
+
employeeId: employee2.employeeId,
|
|
4117
|
+
type: deduction.type,
|
|
4118
|
+
value: deduction.value
|
|
4119
|
+
});
|
|
4120
|
+
return this.calculateBreakdown(employeeId, options);
|
|
4121
|
+
}
|
|
4122
|
+
/**
|
|
4123
|
+
* Remove deduction
|
|
4124
|
+
*/
|
|
4125
|
+
async removeDeduction(employeeId, deductionType, options = {}) {
|
|
4126
|
+
const employee2 = await this.findEmployee(employeeId, options);
|
|
4127
|
+
const updatedCompensation = CompensationFactory.removeDeduction(
|
|
4128
|
+
employee2.compensation,
|
|
4129
|
+
deductionType
|
|
4130
|
+
);
|
|
4131
|
+
employee2.compensation = updatedCompensation;
|
|
4132
|
+
await employee2.save({ session: options.session });
|
|
4133
|
+
logger.info("Deduction removed", {
|
|
4134
|
+
employeeId: employee2.employeeId,
|
|
4135
|
+
type: deductionType
|
|
4136
|
+
});
|
|
4137
|
+
return this.calculateBreakdown(employeeId, options);
|
|
4138
|
+
}
|
|
4139
|
+
/**
|
|
4140
|
+
* Set standard compensation
|
|
4141
|
+
*/
|
|
4142
|
+
async setStandardCompensation(employeeId, baseAmount, options = {}) {
|
|
4143
|
+
const employee2 = await this.findEmployee(employeeId, options);
|
|
4144
|
+
employee2.compensation = CompensationPresets.standard(baseAmount);
|
|
4145
|
+
await employee2.save({ session: options.session });
|
|
4146
|
+
logger.info("Standard compensation set", {
|
|
4147
|
+
employeeId: employee2.employeeId,
|
|
4148
|
+
baseAmount
|
|
4149
|
+
});
|
|
4150
|
+
return this.calculateBreakdown(employeeId, options);
|
|
4151
|
+
}
|
|
4152
|
+
/**
|
|
4153
|
+
* Compare compensation between two employees
|
|
4154
|
+
*/
|
|
4155
|
+
async compareCompensation(employeeId1, employeeId2, options = {}) {
|
|
4156
|
+
const breakdown1 = await this.calculateBreakdown(employeeId1, options);
|
|
4157
|
+
const breakdown2 = await this.calculateBreakdown(employeeId2, options);
|
|
4158
|
+
return {
|
|
4159
|
+
employee1: breakdown1,
|
|
4160
|
+
employee2: breakdown2,
|
|
4161
|
+
difference: {
|
|
4162
|
+
base: breakdown2.baseAmount - breakdown1.baseAmount,
|
|
4163
|
+
gross: breakdown2.grossAmount - breakdown1.grossAmount,
|
|
4164
|
+
net: breakdown2.netAmount - breakdown1.netAmount
|
|
4165
|
+
},
|
|
4166
|
+
ratio: {
|
|
4167
|
+
base: breakdown1.baseAmount > 0 ? breakdown2.baseAmount / breakdown1.baseAmount : 0,
|
|
4168
|
+
gross: breakdown1.grossAmount > 0 ? breakdown2.grossAmount / breakdown1.grossAmount : 0,
|
|
4169
|
+
net: breakdown1.netAmount > 0 ? breakdown2.netAmount / breakdown1.netAmount : 0
|
|
4170
|
+
}
|
|
4171
|
+
};
|
|
4172
|
+
}
|
|
4173
|
+
/**
|
|
4174
|
+
* Get department compensation stats
|
|
4175
|
+
*/
|
|
4176
|
+
async getDepartmentCompensationStats(organizationId, department, options = {}) {
|
|
4177
|
+
let query = this.EmployeeModel.find({
|
|
4178
|
+
organizationId: toObjectId(organizationId),
|
|
4179
|
+
department,
|
|
4180
|
+
status: { $in: ["active", "on_leave"] }
|
|
4181
|
+
});
|
|
4182
|
+
if (options.session) {
|
|
4183
|
+
query = query.session(options.session);
|
|
4184
|
+
}
|
|
4185
|
+
const employees = await query.exec();
|
|
4186
|
+
const breakdowns = employees.map(
|
|
4187
|
+
(emp) => CompensationFactory.calculateBreakdown(emp.compensation)
|
|
4188
|
+
);
|
|
4189
|
+
const totals = breakdowns.reduce(
|
|
4190
|
+
(acc, breakdown) => ({
|
|
4191
|
+
totalBase: acc.totalBase + breakdown.baseAmount,
|
|
4192
|
+
totalGross: acc.totalGross + breakdown.grossAmount,
|
|
4193
|
+
totalNet: acc.totalNet + breakdown.netAmount
|
|
4194
|
+
}),
|
|
4195
|
+
{ totalBase: 0, totalGross: 0, totalNet: 0 }
|
|
4196
|
+
);
|
|
4197
|
+
const count = employees.length || 1;
|
|
4198
|
+
return {
|
|
4199
|
+
department,
|
|
4200
|
+
employeeCount: employees.length,
|
|
4201
|
+
...totals,
|
|
4202
|
+
averageBase: Math.round(totals.totalBase / count),
|
|
4203
|
+
averageGross: Math.round(totals.totalGross / count),
|
|
4204
|
+
averageNet: Math.round(totals.totalNet / count)
|
|
4205
|
+
};
|
|
4206
|
+
}
|
|
4207
|
+
/**
|
|
4208
|
+
* Get organization compensation stats
|
|
4209
|
+
*/
|
|
4210
|
+
async getOrganizationCompensationStats(organizationId, options = {}) {
|
|
4211
|
+
let query = this.EmployeeModel.find({
|
|
4212
|
+
organizationId: toObjectId(organizationId),
|
|
4213
|
+
status: { $in: ["active", "on_leave"] }
|
|
4214
|
+
});
|
|
4215
|
+
if (options.session) {
|
|
4216
|
+
query = query.session(options.session);
|
|
4217
|
+
}
|
|
4218
|
+
const employees = await query.exec();
|
|
4219
|
+
const breakdowns = employees.map(
|
|
4220
|
+
(emp) => CompensationFactory.calculateBreakdown(emp.compensation)
|
|
4221
|
+
);
|
|
4222
|
+
const totals = breakdowns.reduce(
|
|
4223
|
+
(acc, breakdown) => ({
|
|
4224
|
+
totalBase: acc.totalBase + breakdown.baseAmount,
|
|
4225
|
+
totalGross: acc.totalGross + breakdown.grossAmount,
|
|
4226
|
+
totalNet: acc.totalNet + breakdown.netAmount
|
|
4227
|
+
}),
|
|
4228
|
+
{ totalBase: 0, totalGross: 0, totalNet: 0 }
|
|
4229
|
+
);
|
|
4230
|
+
const byDepartment = {};
|
|
4231
|
+
employees.forEach((emp, i) => {
|
|
4232
|
+
const dept = emp.department || "unassigned";
|
|
4233
|
+
if (!byDepartment[dept]) {
|
|
4234
|
+
byDepartment[dept] = { count: 0, totalNet: 0 };
|
|
4235
|
+
}
|
|
4236
|
+
byDepartment[dept].count++;
|
|
4237
|
+
byDepartment[dept].totalNet += breakdowns[i].netAmount;
|
|
4238
|
+
});
|
|
4239
|
+
const count = employees.length || 1;
|
|
4240
|
+
return {
|
|
4241
|
+
employeeCount: employees.length,
|
|
4242
|
+
...totals,
|
|
4243
|
+
averageBase: Math.round(totals.totalBase / count),
|
|
4244
|
+
averageGross: Math.round(totals.totalGross / count),
|
|
4245
|
+
averageNet: Math.round(totals.totalNet / count),
|
|
4246
|
+
byDepartment
|
|
4247
|
+
};
|
|
4248
|
+
}
|
|
4249
|
+
/**
|
|
4250
|
+
* Find employee helper
|
|
4251
|
+
*/
|
|
4252
|
+
async findEmployee(employeeId, options = {}) {
|
|
4253
|
+
let query = this.EmployeeModel.findById(toObjectId(employeeId));
|
|
4254
|
+
if (options.session) {
|
|
4255
|
+
query = query.session(options.session);
|
|
4256
|
+
}
|
|
4257
|
+
const employee2 = await query.exec();
|
|
4258
|
+
if (!employee2) {
|
|
4259
|
+
throw new Error("Employee not found");
|
|
4260
|
+
}
|
|
4261
|
+
return employee2;
|
|
4262
|
+
}
|
|
4263
|
+
};
|
|
4264
|
+
|
|
4265
|
+
// src/attendance.ts
|
|
4266
|
+
async function getAttendance(AttendanceModel, params) {
|
|
4267
|
+
const record = await AttendanceModel.findOne({
|
|
4268
|
+
tenantId: params.organizationId,
|
|
4269
|
+
targetId: params.employeeId,
|
|
4270
|
+
targetModel: "Employee",
|
|
4271
|
+
year: params.year,
|
|
4272
|
+
month: params.month
|
|
4273
|
+
}).lean();
|
|
4274
|
+
if (!record) return null;
|
|
4275
|
+
const fullDays = record.fullDaysCount || 0;
|
|
4276
|
+
const halfDays = (record.halfDaysCount || 0) * 0.5;
|
|
4277
|
+
const paidLeave = record.paidLeaveDaysCount || 0;
|
|
4278
|
+
const actualDays = Math.round(fullDays + halfDays + paidLeave);
|
|
4279
|
+
const absentDays = Math.max(0, params.expectedDays - actualDays);
|
|
4280
|
+
const overtimeDays = record.overtimeDaysCount || 0;
|
|
4281
|
+
return {
|
|
4282
|
+
expectedDays: params.expectedDays,
|
|
4283
|
+
actualDays,
|
|
4284
|
+
absentDays,
|
|
4285
|
+
overtimeDays
|
|
4286
|
+
};
|
|
4287
|
+
}
|
|
4288
|
+
async function batchGetAttendance(AttendanceModel, params) {
|
|
4289
|
+
const records = await AttendanceModel.find({
|
|
4290
|
+
tenantId: params.organizationId,
|
|
4291
|
+
targetId: { $in: params.employeeIds },
|
|
4292
|
+
targetModel: "Employee",
|
|
4293
|
+
year: params.year,
|
|
4294
|
+
month: params.month
|
|
4295
|
+
}).lean();
|
|
4296
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
4297
|
+
for (const record of records) {
|
|
4298
|
+
const fullDays = record.fullDaysCount || 0;
|
|
4299
|
+
const halfDays = (record.halfDaysCount || 0) * 0.5;
|
|
4300
|
+
const paidLeave = record.paidLeaveDaysCount || 0;
|
|
4301
|
+
const actualDays = Math.round(fullDays + halfDays + paidLeave);
|
|
4302
|
+
map2.set(record.targetId.toString(), {
|
|
4303
|
+
expectedDays: params.expectedDays,
|
|
4304
|
+
actualDays
|
|
4305
|
+
});
|
|
4306
|
+
}
|
|
4307
|
+
return map2;
|
|
4308
|
+
}
|
|
4309
|
+
function createHolidaySchema(options = {}) {
|
|
4310
|
+
const fields = {
|
|
4311
|
+
date: { type: Date, required: true, index: true },
|
|
4312
|
+
name: { type: String, required: true },
|
|
4313
|
+
type: {
|
|
4314
|
+
type: String,
|
|
4315
|
+
enum: ["public", "company", "religious"],
|
|
4316
|
+
default: "company"
|
|
4317
|
+
},
|
|
4318
|
+
paid: { type: Boolean, default: true }
|
|
4319
|
+
};
|
|
4320
|
+
if (!options.singleTenant) {
|
|
4321
|
+
fields.organizationId = {
|
|
4322
|
+
type: Schema.Types.ObjectId,
|
|
4323
|
+
ref: "Organization",
|
|
4324
|
+
required: true,
|
|
4325
|
+
index: true
|
|
4326
|
+
};
|
|
4327
|
+
}
|
|
4328
|
+
const schema = new Schema(fields, { timestamps: true });
|
|
4329
|
+
if (!options.singleTenant) {
|
|
4330
|
+
schema.index({ organizationId: 1, date: 1 });
|
|
4331
|
+
} else {
|
|
4332
|
+
schema.index({ date: 1 });
|
|
4333
|
+
}
|
|
4334
|
+
return schema;
|
|
4335
|
+
}
|
|
4336
|
+
async function getHolidays(HolidayModel, params) {
|
|
4337
|
+
const query = {
|
|
4338
|
+
date: { $gte: params.startDate, $lte: params.endDate }
|
|
4339
|
+
};
|
|
4340
|
+
if (params.organizationId) {
|
|
4341
|
+
query.organizationId = params.organizationId;
|
|
4342
|
+
}
|
|
4343
|
+
const holidays = await HolidayModel.find(query).select("date").lean();
|
|
4344
|
+
return holidays.map((h) => h.date);
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4347
|
+
// src/index.ts
|
|
4348
|
+
var index_default = payroll2;
|
|
4349
|
+
|
|
4350
|
+
export { ALLOWANCE_TYPE, BatchPayrollFactory, CompensationBuilder, CompensationFactory, CompensationService, Container, DEDUCTION_TYPE, DEPARTMENT, DuplicatePayrollError, EMPLOYEE_STATUS, EMPLOYMENT_TYPE, EmployeeBuilder, EmployeeFactory, EmployeeNotFoundError, EmployeeQueryBuilder, EmployeeService, EmployeeTerminatedError, HRM_CONFIG, HRM_TRANSACTION_CATEGORIES, NotEligibleError, NotInitializedError, PAYMENT_FREQUENCY, PAYROLL_STATUS, Payroll, PayrollBuilder, PayrollError, PayrollFactory, PayrollQueryBuilder, PayrollService, PluginManager, QueryBuilder, TERMINATION_REASON, ValidationError, addDays, addMonths, addYears, allowanceSchema, applyPercentage, bankDetailsSchema, batchGetAttendance, calculateGross, calculateNet, calculateProRating, calculateTax, canReceiveSalary, compensationSchema, compose, createEventBus, createHolidaySchema, createPayrollInstance, createValidator, deductionSchema, index_default as default, determineOrgRole, diffInDays, diffInMonths, disableLogging, employee, employeePlugin, employmentFields, employmentHistorySchema, enableLogging, endOfMonth, endOfYear, err, formatDateForDB, getAttendance, getCurrentPeriod, getHolidays, getLogger, getPayPeriod, getPayroll, getPayrollRecordModel, getWorkingDaysInMonth, inRange, initializeContainer, isActive, isDateInRange, isEmployed, isErr, isLoggingEnabled, isOk, isOnProbation, isTerminated, logger, map, mapErr, max, mergeConfig, min, ok, oneOf, payroll, payroll2 as payrollInstance, payrollRecordSchema, payrollStatsSchema, pipe, required, resetPayroll, setLogger, startOfMonth, startOfYear, sum, sumAllowances, sumBy, sumDeductions, toObjectId, unwrap, unwrapOr, workScheduleSchema };
|
|
4351
|
+
//# sourceMappingURL=index.js.map
|
|
4352
|
+
//# sourceMappingURL=index.js.map
|