@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.
- 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
|
@@ -0,0 +1,1696 @@
|
|
|
1
|
+
import { Types } from 'mongoose';
|
|
2
|
+
|
|
3
|
+
// src/utils/date.ts
|
|
4
|
+
function addMonths(date, months) {
|
|
5
|
+
const result = new Date(date);
|
|
6
|
+
result.setMonth(result.getMonth() + months);
|
|
7
|
+
return result;
|
|
8
|
+
}
|
|
9
|
+
function startOfMonth(date) {
|
|
10
|
+
const result = new Date(date);
|
|
11
|
+
result.setDate(1);
|
|
12
|
+
result.setHours(0, 0, 0, 0);
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
function endOfMonth(date) {
|
|
16
|
+
const result = new Date(date);
|
|
17
|
+
result.setMonth(result.getMonth() + 1, 0);
|
|
18
|
+
result.setHours(23, 59, 59, 999);
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
function getPayPeriod(month, year) {
|
|
22
|
+
const startDate = new Date(year, month - 1, 1);
|
|
23
|
+
return {
|
|
24
|
+
month,
|
|
25
|
+
year,
|
|
26
|
+
startDate: startOfMonth(startDate),
|
|
27
|
+
endDate: endOfMonth(startDate)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function getCurrentPeriod(date = /* @__PURE__ */ new Date()) {
|
|
31
|
+
const d = new Date(date);
|
|
32
|
+
return {
|
|
33
|
+
year: d.getFullYear(),
|
|
34
|
+
month: d.getMonth() + 1
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function calculateProbationEnd(hireDate, probationMonths) {
|
|
38
|
+
if (!probationMonths || probationMonths <= 0) return null;
|
|
39
|
+
return addMonths(hireDate, probationMonths);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/config.ts
|
|
43
|
+
var HRM_CONFIG = {
|
|
44
|
+
payroll: {
|
|
45
|
+
defaultCurrency: "BDT"},
|
|
46
|
+
employment: {
|
|
47
|
+
defaultProbationMonths: 3}};
|
|
48
|
+
var ORG_ROLES = {
|
|
49
|
+
OWNER: {
|
|
50
|
+
key: "owner",
|
|
51
|
+
label: "Owner",
|
|
52
|
+
description: "Full organization access (set by Organization model)"
|
|
53
|
+
},
|
|
54
|
+
MANAGER: {
|
|
55
|
+
key: "manager",
|
|
56
|
+
label: "Manager",
|
|
57
|
+
description: "Management and administrative features"
|
|
58
|
+
},
|
|
59
|
+
TRAINER: {
|
|
60
|
+
key: "trainer",
|
|
61
|
+
label: "Trainer",
|
|
62
|
+
description: "Training and coaching features"
|
|
63
|
+
},
|
|
64
|
+
STAFF: {
|
|
65
|
+
key: "staff",
|
|
66
|
+
label: "Staff",
|
|
67
|
+
description: "General staff access to basic features"
|
|
68
|
+
},
|
|
69
|
+
INTERN: {
|
|
70
|
+
key: "intern",
|
|
71
|
+
label: "Intern",
|
|
72
|
+
description: "Limited access for interns"
|
|
73
|
+
},
|
|
74
|
+
CONSULTANT: {
|
|
75
|
+
key: "consultant",
|
|
76
|
+
label: "Consultant",
|
|
77
|
+
description: "Project-based consultant access"
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
Object.values(ORG_ROLES).map((role) => role.key);
|
|
81
|
+
|
|
82
|
+
// src/factories/employee.factory.ts
|
|
83
|
+
var EmployeeFactory = class {
|
|
84
|
+
/**
|
|
85
|
+
* Create employee data object
|
|
86
|
+
*/
|
|
87
|
+
static create(params) {
|
|
88
|
+
const { userId, organizationId, employment, compensation, bankDetails } = params;
|
|
89
|
+
const hireDate = employment.hireDate || /* @__PURE__ */ new Date();
|
|
90
|
+
return {
|
|
91
|
+
userId,
|
|
92
|
+
organizationId,
|
|
93
|
+
employeeId: employment.employeeId || `EMP-${Date.now()}-${Math.random().toString(36).substr(2, 6).toUpperCase()}`,
|
|
94
|
+
employmentType: employment.type || "full_time",
|
|
95
|
+
status: "active",
|
|
96
|
+
department: employment.department,
|
|
97
|
+
position: employment.position,
|
|
98
|
+
hireDate,
|
|
99
|
+
probationEndDate: calculateProbationEnd(
|
|
100
|
+
hireDate,
|
|
101
|
+
employment.probationMonths ?? HRM_CONFIG.employment.defaultProbationMonths
|
|
102
|
+
),
|
|
103
|
+
compensation: this.createCompensation(compensation),
|
|
104
|
+
workSchedule: employment.workSchedule || this.defaultWorkSchedule(),
|
|
105
|
+
bankDetails: bankDetails || {},
|
|
106
|
+
payrollStats: {
|
|
107
|
+
totalPaid: 0,
|
|
108
|
+
paymentsThisYear: 0,
|
|
109
|
+
averageMonthly: 0
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Create compensation object
|
|
115
|
+
*/
|
|
116
|
+
static createCompensation(params) {
|
|
117
|
+
return {
|
|
118
|
+
baseAmount: params.baseAmount,
|
|
119
|
+
frequency: params.frequency || "monthly",
|
|
120
|
+
currency: params.currency || HRM_CONFIG.payroll.defaultCurrency,
|
|
121
|
+
allowances: (params.allowances || []).map((a) => ({
|
|
122
|
+
type: a.type || "other",
|
|
123
|
+
name: a.name || a.type || "other",
|
|
124
|
+
amount: a.amount || 0,
|
|
125
|
+
taxable: a.taxable,
|
|
126
|
+
recurring: a.recurring,
|
|
127
|
+
effectiveFrom: a.effectiveFrom,
|
|
128
|
+
effectiveTo: a.effectiveTo
|
|
129
|
+
})),
|
|
130
|
+
deductions: (params.deductions || []).map((d) => ({
|
|
131
|
+
type: d.type || "other",
|
|
132
|
+
name: d.name || d.type || "other",
|
|
133
|
+
amount: d.amount || 0,
|
|
134
|
+
auto: d.auto,
|
|
135
|
+
recurring: d.recurring,
|
|
136
|
+
description: d.description,
|
|
137
|
+
effectiveFrom: d.effectiveFrom,
|
|
138
|
+
effectiveTo: d.effectiveTo
|
|
139
|
+
})),
|
|
140
|
+
grossSalary: 0,
|
|
141
|
+
netSalary: 0,
|
|
142
|
+
effectiveFrom: /* @__PURE__ */ new Date(),
|
|
143
|
+
lastModified: /* @__PURE__ */ new Date()
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Create allowance object
|
|
148
|
+
*/
|
|
149
|
+
static createAllowance(params) {
|
|
150
|
+
return {
|
|
151
|
+
type: params.type,
|
|
152
|
+
name: params.name || params.type,
|
|
153
|
+
amount: params.amount,
|
|
154
|
+
isPercentage: params.isPercentage ?? false,
|
|
155
|
+
taxable: params.taxable ?? true,
|
|
156
|
+
recurring: params.recurring ?? true,
|
|
157
|
+
effectiveFrom: /* @__PURE__ */ new Date()
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Create deduction object
|
|
162
|
+
*/
|
|
163
|
+
static createDeduction(params) {
|
|
164
|
+
return {
|
|
165
|
+
type: params.type,
|
|
166
|
+
name: params.name || params.type,
|
|
167
|
+
amount: params.amount,
|
|
168
|
+
isPercentage: params.isPercentage ?? false,
|
|
169
|
+
auto: params.auto ?? false,
|
|
170
|
+
recurring: params.recurring ?? true,
|
|
171
|
+
description: params.description,
|
|
172
|
+
effectiveFrom: /* @__PURE__ */ new Date()
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Default work schedule
|
|
177
|
+
*/
|
|
178
|
+
static defaultWorkSchedule() {
|
|
179
|
+
return {
|
|
180
|
+
hoursPerWeek: 40,
|
|
181
|
+
hoursPerDay: 8,
|
|
182
|
+
workingDays: [1, 2, 3, 4, 5],
|
|
183
|
+
// Mon-Fri
|
|
184
|
+
shiftStart: "09:00",
|
|
185
|
+
shiftEnd: "17:00"
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Create termination data
|
|
190
|
+
*/
|
|
191
|
+
static createTermination(params) {
|
|
192
|
+
return {
|
|
193
|
+
terminatedAt: params.date || /* @__PURE__ */ new Date(),
|
|
194
|
+
terminationReason: params.reason,
|
|
195
|
+
terminationNotes: params.notes,
|
|
196
|
+
terminatedBy: {
|
|
197
|
+
userId: params.context?.userId,
|
|
198
|
+
name: params.context?.userName,
|
|
199
|
+
role: params.context?.userRole
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
function toObjectId(id) {
|
|
205
|
+
if (id instanceof Types.ObjectId) return id;
|
|
206
|
+
return new Types.ObjectId(id);
|
|
207
|
+
}
|
|
208
|
+
var QueryBuilder = class {
|
|
209
|
+
query;
|
|
210
|
+
constructor(initialQuery = {}) {
|
|
211
|
+
this.query = { ...initialQuery };
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Add where condition
|
|
215
|
+
*/
|
|
216
|
+
where(field, value) {
|
|
217
|
+
this.query[field] = value;
|
|
218
|
+
return this;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Add $in condition
|
|
222
|
+
*/
|
|
223
|
+
whereIn(field, values) {
|
|
224
|
+
this.query[field] = { $in: values };
|
|
225
|
+
return this;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Add $nin condition
|
|
229
|
+
*/
|
|
230
|
+
whereNotIn(field, values) {
|
|
231
|
+
this.query[field] = { $nin: values };
|
|
232
|
+
return this;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Add $gte condition
|
|
236
|
+
*/
|
|
237
|
+
whereGte(field, value) {
|
|
238
|
+
const existing = this.query[field] || {};
|
|
239
|
+
this.query[field] = { ...existing, $gte: value };
|
|
240
|
+
return this;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Add $lte condition
|
|
244
|
+
*/
|
|
245
|
+
whereLte(field, value) {
|
|
246
|
+
const existing = this.query[field] || {};
|
|
247
|
+
this.query[field] = { ...existing, $lte: value };
|
|
248
|
+
return this;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Add $gt condition
|
|
252
|
+
*/
|
|
253
|
+
whereGt(field, value) {
|
|
254
|
+
const existing = this.query[field] || {};
|
|
255
|
+
this.query[field] = { ...existing, $gt: value };
|
|
256
|
+
return this;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Add $lt condition
|
|
260
|
+
*/
|
|
261
|
+
whereLt(field, value) {
|
|
262
|
+
const existing = this.query[field] || {};
|
|
263
|
+
this.query[field] = { ...existing, $lt: value };
|
|
264
|
+
return this;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Add between condition
|
|
268
|
+
*/
|
|
269
|
+
whereBetween(field, start, end) {
|
|
270
|
+
this.query[field] = { $gte: start, $lte: end };
|
|
271
|
+
return this;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Add $exists condition
|
|
275
|
+
*/
|
|
276
|
+
whereExists(field) {
|
|
277
|
+
this.query[field] = { $exists: true };
|
|
278
|
+
return this;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Add $exists: false condition
|
|
282
|
+
*/
|
|
283
|
+
whereNotExists(field) {
|
|
284
|
+
this.query[field] = { $exists: false };
|
|
285
|
+
return this;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Add $ne condition
|
|
289
|
+
*/
|
|
290
|
+
whereNot(field, value) {
|
|
291
|
+
this.query[field] = { $ne: value };
|
|
292
|
+
return this;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Add regex condition
|
|
296
|
+
*/
|
|
297
|
+
whereRegex(field, pattern, flags = "i") {
|
|
298
|
+
this.query[field] = { $regex: pattern, $options: flags };
|
|
299
|
+
return this;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Merge another query
|
|
303
|
+
*/
|
|
304
|
+
merge(otherQuery) {
|
|
305
|
+
this.query = { ...this.query, ...otherQuery };
|
|
306
|
+
return this;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Build and return the query
|
|
310
|
+
*/
|
|
311
|
+
build() {
|
|
312
|
+
return { ...this.query };
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
var EmployeeQueryBuilder = class extends QueryBuilder {
|
|
316
|
+
/**
|
|
317
|
+
* Filter by organization
|
|
318
|
+
*/
|
|
319
|
+
forOrganization(organizationId) {
|
|
320
|
+
return this.where("organizationId", toObjectId(organizationId));
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Filter by user
|
|
324
|
+
*/
|
|
325
|
+
forUser(userId) {
|
|
326
|
+
return this.where("userId", toObjectId(userId));
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Filter by status(es)
|
|
330
|
+
*/
|
|
331
|
+
withStatus(...statuses) {
|
|
332
|
+
if (statuses.length === 1) {
|
|
333
|
+
return this.where("status", statuses[0]);
|
|
334
|
+
}
|
|
335
|
+
return this.whereIn("status", statuses);
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Filter active employees
|
|
339
|
+
*/
|
|
340
|
+
active() {
|
|
341
|
+
return this.withStatus("active");
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Filter employed employees (not terminated)
|
|
345
|
+
*/
|
|
346
|
+
employed() {
|
|
347
|
+
return this.whereIn("status", ["active", "on_leave", "suspended"]);
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Filter terminated employees
|
|
351
|
+
*/
|
|
352
|
+
terminated() {
|
|
353
|
+
return this.withStatus("terminated");
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Filter by department
|
|
357
|
+
*/
|
|
358
|
+
inDepartment(department) {
|
|
359
|
+
return this.where("department", department);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Filter by position
|
|
363
|
+
*/
|
|
364
|
+
inPosition(position) {
|
|
365
|
+
return this.where("position", position);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Filter by employment type
|
|
369
|
+
*/
|
|
370
|
+
withEmploymentType(type) {
|
|
371
|
+
return this.where("employmentType", type);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Filter by hire date (after)
|
|
375
|
+
*/
|
|
376
|
+
hiredAfter(date) {
|
|
377
|
+
return this.whereGte("hireDate", date);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Filter by hire date (before)
|
|
381
|
+
*/
|
|
382
|
+
hiredBefore(date) {
|
|
383
|
+
return this.whereLte("hireDate", date);
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Filter by minimum salary
|
|
387
|
+
*/
|
|
388
|
+
withMinSalary(amount) {
|
|
389
|
+
return this.whereGte("compensation.netSalary", amount);
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Filter by maximum salary
|
|
393
|
+
*/
|
|
394
|
+
withMaxSalary(amount) {
|
|
395
|
+
return this.whereLte("compensation.netSalary", amount);
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Filter by salary range
|
|
399
|
+
*/
|
|
400
|
+
withSalaryRange(min, max) {
|
|
401
|
+
return this.whereBetween("compensation.netSalary", min, max);
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
var PayrollQueryBuilder = class extends QueryBuilder {
|
|
405
|
+
/**
|
|
406
|
+
* Filter by organization
|
|
407
|
+
*/
|
|
408
|
+
forOrganization(organizationId) {
|
|
409
|
+
return this.where("organizationId", toObjectId(organizationId));
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Filter by employee
|
|
413
|
+
*/
|
|
414
|
+
forEmployee(employeeId) {
|
|
415
|
+
return this.where("employeeId", toObjectId(employeeId));
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Filter by period
|
|
419
|
+
*/
|
|
420
|
+
forPeriod(month, year) {
|
|
421
|
+
if (month !== void 0) {
|
|
422
|
+
this.where("period.month", month);
|
|
423
|
+
}
|
|
424
|
+
if (year !== void 0) {
|
|
425
|
+
this.where("period.year", year);
|
|
426
|
+
}
|
|
427
|
+
return this;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Filter by status(es)
|
|
431
|
+
*/
|
|
432
|
+
withStatus(...statuses) {
|
|
433
|
+
if (statuses.length === 1) {
|
|
434
|
+
return this.where("status", statuses[0]);
|
|
435
|
+
}
|
|
436
|
+
return this.whereIn("status", statuses);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Filter paid records
|
|
440
|
+
*/
|
|
441
|
+
paid() {
|
|
442
|
+
return this.withStatus("paid");
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Filter pending records
|
|
446
|
+
*/
|
|
447
|
+
pending() {
|
|
448
|
+
return this.whereIn("status", ["pending", "processing"]);
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Filter by date range
|
|
452
|
+
*/
|
|
453
|
+
inDateRange(start, end) {
|
|
454
|
+
return this.whereBetween("period.payDate", start, end);
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Filter exported records
|
|
458
|
+
*/
|
|
459
|
+
exported() {
|
|
460
|
+
return this.where("exported", true);
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Filter not exported records
|
|
464
|
+
*/
|
|
465
|
+
notExported() {
|
|
466
|
+
return this.where("exported", false);
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
function employee() {
|
|
470
|
+
return new EmployeeQueryBuilder();
|
|
471
|
+
}
|
|
472
|
+
function payroll() {
|
|
473
|
+
return new PayrollQueryBuilder();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/utils/validation.ts
|
|
477
|
+
function isActive(employee2) {
|
|
478
|
+
return employee2?.status === "active";
|
|
479
|
+
}
|
|
480
|
+
function isOnLeave(employee2) {
|
|
481
|
+
return employee2?.status === "on_leave";
|
|
482
|
+
}
|
|
483
|
+
function isSuspended(employee2) {
|
|
484
|
+
return employee2?.status === "suspended";
|
|
485
|
+
}
|
|
486
|
+
function isEmployed(employee2) {
|
|
487
|
+
return isActive(employee2) || isOnLeave(employee2) || isSuspended(employee2);
|
|
488
|
+
}
|
|
489
|
+
function canReceiveSalary(employee2) {
|
|
490
|
+
return (isActive(employee2) || isOnLeave(employee2)) && (employee2.compensation?.baseAmount ?? 0) > 0;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/utils/logger.ts
|
|
494
|
+
var createConsoleLogger = () => ({
|
|
495
|
+
info: (message, meta) => {
|
|
496
|
+
if (meta) {
|
|
497
|
+
console.log(`[Payroll] INFO: ${message}`, meta);
|
|
498
|
+
} else {
|
|
499
|
+
console.log(`[Payroll] INFO: ${message}`);
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
error: (message, meta) => {
|
|
503
|
+
if (meta) {
|
|
504
|
+
console.error(`[Payroll] ERROR: ${message}`, meta);
|
|
505
|
+
} else {
|
|
506
|
+
console.error(`[Payroll] ERROR: ${message}`);
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
warn: (message, meta) => {
|
|
510
|
+
if (meta) {
|
|
511
|
+
console.warn(`[Payroll] WARN: ${message}`, meta);
|
|
512
|
+
} else {
|
|
513
|
+
console.warn(`[Payroll] WARN: ${message}`);
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
debug: (message, meta) => {
|
|
517
|
+
if (process.env.NODE_ENV !== "production") {
|
|
518
|
+
if (meta) {
|
|
519
|
+
console.log(`[Payroll] DEBUG: ${message}`, meta);
|
|
520
|
+
} else {
|
|
521
|
+
console.log(`[Payroll] DEBUG: ${message}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
var currentLogger = createConsoleLogger();
|
|
527
|
+
var logger = {
|
|
528
|
+
info: (message, meta) => {
|
|
529
|
+
currentLogger.info(message, meta);
|
|
530
|
+
},
|
|
531
|
+
error: (message, meta) => {
|
|
532
|
+
currentLogger.error(message, meta);
|
|
533
|
+
},
|
|
534
|
+
warn: (message, meta) => {
|
|
535
|
+
currentLogger.warn(message, meta);
|
|
536
|
+
},
|
|
537
|
+
debug: (message, meta) => {
|
|
538
|
+
currentLogger.debug(message, meta);
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
// src/services/employee.service.ts
|
|
543
|
+
var EmployeeService = class {
|
|
544
|
+
constructor(EmployeeModel) {
|
|
545
|
+
this.EmployeeModel = EmployeeModel;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Find employee by ID
|
|
549
|
+
*/
|
|
550
|
+
async findById(employeeId, options = {}) {
|
|
551
|
+
let query = this.EmployeeModel.findById(toObjectId(employeeId));
|
|
552
|
+
if (options.session) {
|
|
553
|
+
query = query.session(options.session);
|
|
554
|
+
}
|
|
555
|
+
if (options.populate) {
|
|
556
|
+
query = query.populate("userId", "name email phone");
|
|
557
|
+
}
|
|
558
|
+
return query.exec();
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Find employee by user and organization
|
|
562
|
+
*/
|
|
563
|
+
async findByUserId(userId, organizationId, options = {}) {
|
|
564
|
+
const query = employee().forUser(userId).forOrganization(organizationId).build();
|
|
565
|
+
let mongooseQuery = this.EmployeeModel.findOne(query);
|
|
566
|
+
if (options.session) {
|
|
567
|
+
mongooseQuery = mongooseQuery.session(options.session);
|
|
568
|
+
}
|
|
569
|
+
return mongooseQuery.exec();
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Find active employees in organization
|
|
573
|
+
*/
|
|
574
|
+
async findActive(organizationId, options = {}) {
|
|
575
|
+
const query = employee().forOrganization(organizationId).active().build();
|
|
576
|
+
let mongooseQuery = this.EmployeeModel.find(query, options.projection);
|
|
577
|
+
if (options.session) {
|
|
578
|
+
mongooseQuery = mongooseQuery.session(options.session);
|
|
579
|
+
}
|
|
580
|
+
return mongooseQuery.exec();
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Find employed employees (not terminated)
|
|
584
|
+
*/
|
|
585
|
+
async findEmployed(organizationId, options = {}) {
|
|
586
|
+
const query = employee().forOrganization(organizationId).employed().build();
|
|
587
|
+
let mongooseQuery = this.EmployeeModel.find(query, options.projection);
|
|
588
|
+
if (options.session) {
|
|
589
|
+
mongooseQuery = mongooseQuery.session(options.session);
|
|
590
|
+
}
|
|
591
|
+
return mongooseQuery.exec();
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Find employees by department
|
|
595
|
+
*/
|
|
596
|
+
async findByDepartment(organizationId, department, options = {}) {
|
|
597
|
+
const query = employee().forOrganization(organizationId).inDepartment(department).active().build();
|
|
598
|
+
let mongooseQuery = this.EmployeeModel.find(query);
|
|
599
|
+
if (options.session) {
|
|
600
|
+
mongooseQuery = mongooseQuery.session(options.session);
|
|
601
|
+
}
|
|
602
|
+
return mongooseQuery.exec();
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Find employees eligible for payroll
|
|
606
|
+
*/
|
|
607
|
+
async findEligibleForPayroll(organizationId, options = {}) {
|
|
608
|
+
const query = employee().forOrganization(organizationId).employed().build();
|
|
609
|
+
let mongooseQuery = this.EmployeeModel.find(query);
|
|
610
|
+
if (options.session) {
|
|
611
|
+
mongooseQuery = mongooseQuery.session(options.session);
|
|
612
|
+
}
|
|
613
|
+
const employees = await mongooseQuery.exec();
|
|
614
|
+
return employees.filter((emp) => canReceiveSalary(emp));
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Create new employee
|
|
618
|
+
*/
|
|
619
|
+
async create(params, options = {}) {
|
|
620
|
+
const employeeData = EmployeeFactory.create(params);
|
|
621
|
+
const [employee2] = await this.EmployeeModel.create([employeeData], {
|
|
622
|
+
session: options.session
|
|
623
|
+
});
|
|
624
|
+
logger.info("Employee created", {
|
|
625
|
+
employeeId: employee2.employeeId,
|
|
626
|
+
organizationId: employee2.organizationId.toString()
|
|
627
|
+
});
|
|
628
|
+
return employee2;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Update employee status
|
|
632
|
+
*/
|
|
633
|
+
async updateStatus(employeeId, status, context = {}, options = {}) {
|
|
634
|
+
const employee2 = await this.findById(employeeId, options);
|
|
635
|
+
if (!employee2) {
|
|
636
|
+
throw new Error("Employee not found");
|
|
637
|
+
}
|
|
638
|
+
employee2.status = status;
|
|
639
|
+
await employee2.save({ session: options.session });
|
|
640
|
+
logger.info("Employee status updated", {
|
|
641
|
+
employeeId: employee2.employeeId,
|
|
642
|
+
newStatus: status
|
|
643
|
+
});
|
|
644
|
+
return employee2;
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Update employee compensation
|
|
648
|
+
*
|
|
649
|
+
* NOTE: This merges the compensation fields rather than replacing the entire object.
|
|
650
|
+
* To update allowances/deductions, use addAllowance/removeAllowance methods.
|
|
651
|
+
*/
|
|
652
|
+
async updateCompensation(employeeId, compensation, options = {}) {
|
|
653
|
+
const currentEmployee = await this.EmployeeModel.findById(toObjectId(employeeId)).session(options.session || null);
|
|
654
|
+
if (!currentEmployee) {
|
|
655
|
+
throw new Error("Employee not found");
|
|
656
|
+
}
|
|
657
|
+
const updateFields = {
|
|
658
|
+
"compensation.lastModified": /* @__PURE__ */ new Date()
|
|
659
|
+
};
|
|
660
|
+
if (compensation.baseAmount !== void 0) {
|
|
661
|
+
updateFields["compensation.baseAmount"] = compensation.baseAmount;
|
|
662
|
+
}
|
|
663
|
+
if (compensation.currency !== void 0) {
|
|
664
|
+
updateFields["compensation.currency"] = compensation.currency;
|
|
665
|
+
}
|
|
666
|
+
if (compensation.frequency !== void 0) {
|
|
667
|
+
updateFields["compensation.frequency"] = compensation.frequency;
|
|
668
|
+
}
|
|
669
|
+
if (compensation.effectiveFrom !== void 0) {
|
|
670
|
+
updateFields["compensation.effectiveFrom"] = compensation.effectiveFrom;
|
|
671
|
+
}
|
|
672
|
+
const employee2 = await this.EmployeeModel.findByIdAndUpdate(
|
|
673
|
+
toObjectId(employeeId),
|
|
674
|
+
{ $set: updateFields },
|
|
675
|
+
{ new: true, runValidators: true, session: options.session }
|
|
676
|
+
);
|
|
677
|
+
if (!employee2) {
|
|
678
|
+
throw new Error("Employee not found");
|
|
679
|
+
}
|
|
680
|
+
return employee2;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Get employee statistics for organization
|
|
684
|
+
*/
|
|
685
|
+
async getEmployeeStats(organizationId, options = {}) {
|
|
686
|
+
const query = employee().forOrganization(organizationId).build();
|
|
687
|
+
let mongooseQuery = this.EmployeeModel.find(query);
|
|
688
|
+
if (options.session) {
|
|
689
|
+
mongooseQuery = mongooseQuery.session(options.session);
|
|
690
|
+
}
|
|
691
|
+
const employees = await mongooseQuery.exec();
|
|
692
|
+
return {
|
|
693
|
+
total: employees.length,
|
|
694
|
+
active: employees.filter(isActive).length,
|
|
695
|
+
employed: employees.filter(isEmployed).length,
|
|
696
|
+
canReceiveSalary: employees.filter(canReceiveSalary).length,
|
|
697
|
+
byStatus: this.groupByStatus(employees),
|
|
698
|
+
byDepartment: this.groupByDepartment(employees)
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Group employees by status
|
|
703
|
+
*/
|
|
704
|
+
groupByStatus(employees) {
|
|
705
|
+
return employees.reduce(
|
|
706
|
+
(acc, emp) => {
|
|
707
|
+
acc[emp.status] = (acc[emp.status] || 0) + 1;
|
|
708
|
+
return acc;
|
|
709
|
+
},
|
|
710
|
+
{}
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Group employees by department
|
|
715
|
+
*/
|
|
716
|
+
groupByDepartment(employees) {
|
|
717
|
+
return employees.reduce(
|
|
718
|
+
(acc, emp) => {
|
|
719
|
+
const dept = emp.department || "unassigned";
|
|
720
|
+
acc[dept] = (acc[dept] || 0) + 1;
|
|
721
|
+
return acc;
|
|
722
|
+
},
|
|
723
|
+
{}
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Check if employee is active
|
|
728
|
+
*/
|
|
729
|
+
isActive(employee2) {
|
|
730
|
+
return isActive(employee2);
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Check if employee is employed
|
|
734
|
+
*/
|
|
735
|
+
isEmployed(employee2) {
|
|
736
|
+
return isEmployed(employee2);
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Check if employee can receive salary
|
|
740
|
+
*/
|
|
741
|
+
canReceiveSalary(employee2) {
|
|
742
|
+
return canReceiveSalary(employee2);
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
function createEmployeeService(EmployeeModel) {
|
|
746
|
+
return new EmployeeService(EmployeeModel);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// src/utils/calculation.ts
|
|
750
|
+
function sumBy(items, getter) {
|
|
751
|
+
return items.reduce((total, item) => total + getter(item), 0);
|
|
752
|
+
}
|
|
753
|
+
function sumAllowances(allowances) {
|
|
754
|
+
return sumBy(allowances, (a) => a.amount);
|
|
755
|
+
}
|
|
756
|
+
function sumDeductions(deductions) {
|
|
757
|
+
return sumBy(deductions, (d) => d.amount);
|
|
758
|
+
}
|
|
759
|
+
function applyPercentage(amount, percentage) {
|
|
760
|
+
return Math.round(amount * (percentage / 100));
|
|
761
|
+
}
|
|
762
|
+
function calculateGross(baseAmount, allowances) {
|
|
763
|
+
return baseAmount + sumAllowances(allowances);
|
|
764
|
+
}
|
|
765
|
+
function calculateNet(gross, deductions) {
|
|
766
|
+
return Math.max(0, gross - sumDeductions(deductions));
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// src/factories/payroll.factory.ts
|
|
770
|
+
var PayrollFactory = class {
|
|
771
|
+
/**
|
|
772
|
+
* Create payroll data object
|
|
773
|
+
*/
|
|
774
|
+
static create(params) {
|
|
775
|
+
const {
|
|
776
|
+
employeeId,
|
|
777
|
+
organizationId,
|
|
778
|
+
baseAmount,
|
|
779
|
+
allowances = [],
|
|
780
|
+
deductions = [],
|
|
781
|
+
period = {},
|
|
782
|
+
metadata = {}
|
|
783
|
+
} = params;
|
|
784
|
+
const calculatedAllowances = this.calculateAllowances(baseAmount, allowances);
|
|
785
|
+
const calculatedDeductions = this.calculateDeductions(baseAmount, deductions);
|
|
786
|
+
const gross = calculateGross(baseAmount, calculatedAllowances);
|
|
787
|
+
const net = calculateNet(gross, calculatedDeductions);
|
|
788
|
+
return {
|
|
789
|
+
employeeId,
|
|
790
|
+
organizationId,
|
|
791
|
+
period: this.createPeriod(period),
|
|
792
|
+
breakdown: {
|
|
793
|
+
baseAmount,
|
|
794
|
+
allowances: calculatedAllowances,
|
|
795
|
+
deductions: calculatedDeductions,
|
|
796
|
+
grossSalary: gross,
|
|
797
|
+
netSalary: net
|
|
798
|
+
},
|
|
799
|
+
status: "pending",
|
|
800
|
+
processedAt: null,
|
|
801
|
+
paidAt: null,
|
|
802
|
+
metadata: {
|
|
803
|
+
currency: metadata.currency || HRM_CONFIG.payroll.defaultCurrency,
|
|
804
|
+
paymentMethod: metadata.paymentMethod,
|
|
805
|
+
notes: metadata.notes
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Create pay period
|
|
811
|
+
*/
|
|
812
|
+
static createPeriod(params) {
|
|
813
|
+
const now = /* @__PURE__ */ new Date();
|
|
814
|
+
const month = params.month || now.getMonth() + 1;
|
|
815
|
+
const year = params.year || now.getFullYear();
|
|
816
|
+
const period = getPayPeriod(month, year);
|
|
817
|
+
return {
|
|
818
|
+
...period,
|
|
819
|
+
payDate: params.payDate || /* @__PURE__ */ new Date()
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Calculate allowances from base amount
|
|
824
|
+
*/
|
|
825
|
+
static calculateAllowances(baseAmount, allowances) {
|
|
826
|
+
return allowances.map((allowance) => {
|
|
827
|
+
const amount = allowance.isPercentage && allowance.value !== void 0 ? Math.round(baseAmount * allowance.value / 100) : allowance.amount;
|
|
828
|
+
return {
|
|
829
|
+
type: allowance.type,
|
|
830
|
+
amount,
|
|
831
|
+
taxable: allowance.taxable ?? true
|
|
832
|
+
};
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Calculate deductions from base amount
|
|
837
|
+
*/
|
|
838
|
+
static calculateDeductions(baseAmount, deductions) {
|
|
839
|
+
return deductions.map((deduction) => {
|
|
840
|
+
const amount = deduction.isPercentage && deduction.value !== void 0 ? Math.round(baseAmount * deduction.value / 100) : deduction.amount;
|
|
841
|
+
return {
|
|
842
|
+
type: deduction.type,
|
|
843
|
+
amount,
|
|
844
|
+
description: deduction.description
|
|
845
|
+
};
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Create bonus object
|
|
850
|
+
*/
|
|
851
|
+
static createBonus(params) {
|
|
852
|
+
return {
|
|
853
|
+
type: params.type,
|
|
854
|
+
amount: params.amount,
|
|
855
|
+
reason: params.reason,
|
|
856
|
+
approvedBy: params.approvedBy,
|
|
857
|
+
approvedAt: /* @__PURE__ */ new Date()
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Mark payroll as paid (immutable)
|
|
862
|
+
* Sets both top-level transactionId and metadata for compatibility
|
|
863
|
+
*/
|
|
864
|
+
static markAsPaid(payroll2, params = {}) {
|
|
865
|
+
return {
|
|
866
|
+
...payroll2,
|
|
867
|
+
status: "paid",
|
|
868
|
+
paidAt: params.paidAt || /* @__PURE__ */ new Date(),
|
|
869
|
+
processedAt: payroll2.processedAt || params.paidAt || /* @__PURE__ */ new Date(),
|
|
870
|
+
transactionId: params.transactionId || payroll2.transactionId,
|
|
871
|
+
metadata: {
|
|
872
|
+
...payroll2.metadata,
|
|
873
|
+
transactionId: params.transactionId,
|
|
874
|
+
paymentMethod: params.paymentMethod || payroll2.metadata?.paymentMethod
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Mark payroll as processed (immutable)
|
|
880
|
+
*/
|
|
881
|
+
static markAsProcessed(payroll2, params = {}) {
|
|
882
|
+
return {
|
|
883
|
+
...payroll2,
|
|
884
|
+
status: "processing",
|
|
885
|
+
processedAt: params.processedAt || /* @__PURE__ */ new Date()
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
var BatchPayrollFactory = class {
|
|
890
|
+
/**
|
|
891
|
+
* Create payroll records for multiple employees
|
|
892
|
+
*/
|
|
893
|
+
static createBatch(employees, params) {
|
|
894
|
+
return employees.map(
|
|
895
|
+
(employee2) => PayrollFactory.create({
|
|
896
|
+
employeeId: employee2._id,
|
|
897
|
+
organizationId: params.organizationId || employee2.organizationId,
|
|
898
|
+
baseAmount: employee2.compensation.baseAmount,
|
|
899
|
+
allowances: employee2.compensation.allowances || [],
|
|
900
|
+
deductions: employee2.compensation.deductions || [],
|
|
901
|
+
period: { month: params.month, year: params.year },
|
|
902
|
+
metadata: { currency: employee2.compensation.currency }
|
|
903
|
+
})
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Calculate total payroll amounts
|
|
908
|
+
*/
|
|
909
|
+
static calculateTotalPayroll(payrolls) {
|
|
910
|
+
return payrolls.reduce(
|
|
911
|
+
(totals, payroll2) => ({
|
|
912
|
+
count: totals.count + 1,
|
|
913
|
+
totalGross: totals.totalGross + payroll2.breakdown.grossSalary,
|
|
914
|
+
totalNet: totals.totalNet + payroll2.breakdown.netSalary,
|
|
915
|
+
totalAllowances: totals.totalAllowances + sumAllowances(payroll2.breakdown.allowances),
|
|
916
|
+
totalDeductions: totals.totalDeductions + sumDeductions(payroll2.breakdown.deductions)
|
|
917
|
+
}),
|
|
918
|
+
{ count: 0, totalGross: 0, totalNet: 0, totalAllowances: 0, totalDeductions: 0 }
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
// src/services/payroll.service.ts
|
|
924
|
+
var PayrollService = class {
|
|
925
|
+
constructor(PayrollModel, employeeService) {
|
|
926
|
+
this.PayrollModel = PayrollModel;
|
|
927
|
+
this.employeeService = employeeService;
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Find payroll by ID
|
|
931
|
+
*/
|
|
932
|
+
async findById(payrollId, options = {}) {
|
|
933
|
+
let query = this.PayrollModel.findById(toObjectId(payrollId));
|
|
934
|
+
if (options.session) {
|
|
935
|
+
query = query.session(options.session);
|
|
936
|
+
}
|
|
937
|
+
return query.exec();
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Find payrolls by employee
|
|
941
|
+
*/
|
|
942
|
+
async findByEmployee(employeeId, organizationId, options = {}) {
|
|
943
|
+
const query = payroll().forEmployee(employeeId).forOrganization(organizationId).build();
|
|
944
|
+
let mongooseQuery = this.PayrollModel.find(query).sort({ "period.year": -1, "period.month": -1 }).limit(options.limit || 12);
|
|
945
|
+
if (options.session) {
|
|
946
|
+
mongooseQuery = mongooseQuery.session(options.session);
|
|
947
|
+
}
|
|
948
|
+
return mongooseQuery.exec();
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Find payrolls for a period
|
|
952
|
+
*/
|
|
953
|
+
async findForPeriod(organizationId, month, year, options = {}) {
|
|
954
|
+
const query = payroll().forOrganization(organizationId).forPeriod(month, year).build();
|
|
955
|
+
let mongooseQuery = this.PayrollModel.find(query);
|
|
956
|
+
if (options.session) {
|
|
957
|
+
mongooseQuery = mongooseQuery.session(options.session);
|
|
958
|
+
}
|
|
959
|
+
return mongooseQuery.exec();
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Find pending payrolls
|
|
963
|
+
*/
|
|
964
|
+
async findPending(organizationId, month, year, options = {}) {
|
|
965
|
+
const query = payroll().forOrganization(organizationId).forPeriod(month, year).pending().build();
|
|
966
|
+
let mongooseQuery = this.PayrollModel.find(query);
|
|
967
|
+
if (options.session) {
|
|
968
|
+
mongooseQuery = mongooseQuery.session(options.session);
|
|
969
|
+
}
|
|
970
|
+
return mongooseQuery.exec();
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Find payroll by employee and period
|
|
974
|
+
*/
|
|
975
|
+
async findByEmployeeAndPeriod(employeeId, organizationId, month, year, options = {}) {
|
|
976
|
+
const query = payroll().forEmployee(employeeId).forOrganization(organizationId).forPeriod(month, year).build();
|
|
977
|
+
let mongooseQuery = this.PayrollModel.findOne(query);
|
|
978
|
+
if (options.session) {
|
|
979
|
+
mongooseQuery = mongooseQuery.session(options.session);
|
|
980
|
+
}
|
|
981
|
+
return mongooseQuery.exec();
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Create payroll record
|
|
985
|
+
*/
|
|
986
|
+
async create(data, options = {}) {
|
|
987
|
+
const [payroll2] = await this.PayrollModel.create([data], {
|
|
988
|
+
session: options.session
|
|
989
|
+
});
|
|
990
|
+
logger.info("Payroll record created", {
|
|
991
|
+
payrollId: payroll2._id.toString(),
|
|
992
|
+
employeeId: payroll2.employeeId.toString()
|
|
993
|
+
});
|
|
994
|
+
return payroll2;
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Generate payroll for employee
|
|
998
|
+
*/
|
|
999
|
+
async generateForEmployee(employeeId, organizationId, month, year, options = {}) {
|
|
1000
|
+
const employee2 = await this.employeeService.findById(employeeId, options);
|
|
1001
|
+
if (!employee2) {
|
|
1002
|
+
throw new Error("Employee not found");
|
|
1003
|
+
}
|
|
1004
|
+
if (!canReceiveSalary(employee2)) {
|
|
1005
|
+
throw new Error("Employee not eligible for payroll");
|
|
1006
|
+
}
|
|
1007
|
+
const existing = await this.findByEmployeeAndPeriod(
|
|
1008
|
+
employeeId,
|
|
1009
|
+
organizationId,
|
|
1010
|
+
month,
|
|
1011
|
+
year,
|
|
1012
|
+
options
|
|
1013
|
+
);
|
|
1014
|
+
if (existing) {
|
|
1015
|
+
throw new Error("Payroll already exists for this period");
|
|
1016
|
+
}
|
|
1017
|
+
const payrollData = PayrollFactory.create({
|
|
1018
|
+
employeeId,
|
|
1019
|
+
organizationId,
|
|
1020
|
+
baseAmount: employee2.compensation.baseAmount,
|
|
1021
|
+
allowances: employee2.compensation.allowances || [],
|
|
1022
|
+
deductions: employee2.compensation.deductions || [],
|
|
1023
|
+
period: { month, year },
|
|
1024
|
+
metadata: { currency: employee2.compensation.currency }
|
|
1025
|
+
});
|
|
1026
|
+
return this.create(payrollData, options);
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Generate batch payroll
|
|
1030
|
+
*/
|
|
1031
|
+
async generateBatch(organizationId, month, year, options = {}) {
|
|
1032
|
+
const employees = await this.employeeService.findEligibleForPayroll(
|
|
1033
|
+
organizationId,
|
|
1034
|
+
options
|
|
1035
|
+
);
|
|
1036
|
+
if (employees.length === 0) {
|
|
1037
|
+
return {
|
|
1038
|
+
success: true,
|
|
1039
|
+
generated: 0,
|
|
1040
|
+
skipped: 0,
|
|
1041
|
+
payrolls: [],
|
|
1042
|
+
message: "No eligible employees"
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
const existingPayrolls = await this.findForPeriod(
|
|
1046
|
+
organizationId,
|
|
1047
|
+
month,
|
|
1048
|
+
year,
|
|
1049
|
+
options
|
|
1050
|
+
);
|
|
1051
|
+
const existingEmployeeIds = new Set(
|
|
1052
|
+
existingPayrolls.map((p) => p.employeeId.toString())
|
|
1053
|
+
);
|
|
1054
|
+
const eligibleEmployees = employees.filter(
|
|
1055
|
+
(emp) => !existingEmployeeIds.has(emp._id.toString())
|
|
1056
|
+
);
|
|
1057
|
+
if (eligibleEmployees.length === 0) {
|
|
1058
|
+
return {
|
|
1059
|
+
success: true,
|
|
1060
|
+
generated: 0,
|
|
1061
|
+
skipped: employees.length,
|
|
1062
|
+
payrolls: [],
|
|
1063
|
+
message: "Payrolls already exist for all employees"
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
const payrollsData = BatchPayrollFactory.createBatch(eligibleEmployees, {
|
|
1067
|
+
month,
|
|
1068
|
+
year,
|
|
1069
|
+
organizationId
|
|
1070
|
+
});
|
|
1071
|
+
const created = await this.PayrollModel.insertMany(payrollsData, {
|
|
1072
|
+
session: options.session
|
|
1073
|
+
});
|
|
1074
|
+
logger.info("Batch payroll generated", {
|
|
1075
|
+
organizationId: organizationId.toString(),
|
|
1076
|
+
month,
|
|
1077
|
+
year,
|
|
1078
|
+
count: created.length
|
|
1079
|
+
});
|
|
1080
|
+
return {
|
|
1081
|
+
success: true,
|
|
1082
|
+
generated: created.length,
|
|
1083
|
+
skipped: existingEmployeeIds.size,
|
|
1084
|
+
payrolls: created,
|
|
1085
|
+
message: `Generated ${created.length} payrolls`
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Mark payroll as paid
|
|
1090
|
+
*/
|
|
1091
|
+
async markAsPaid(payrollId, paymentDetails = {}, options = {}) {
|
|
1092
|
+
const payroll2 = await this.findById(payrollId, options);
|
|
1093
|
+
if (!payroll2) {
|
|
1094
|
+
throw new Error("Payroll not found");
|
|
1095
|
+
}
|
|
1096
|
+
if (payroll2.status === "paid") {
|
|
1097
|
+
throw new Error("Payroll already paid");
|
|
1098
|
+
}
|
|
1099
|
+
const payrollObj = payroll2.toObject();
|
|
1100
|
+
const updatedData = PayrollFactory.markAsPaid(payrollObj, paymentDetails);
|
|
1101
|
+
const updated = await this.PayrollModel.findByIdAndUpdate(
|
|
1102
|
+
payrollId,
|
|
1103
|
+
updatedData,
|
|
1104
|
+
{ new: true, runValidators: true, session: options.session }
|
|
1105
|
+
);
|
|
1106
|
+
if (!updated) {
|
|
1107
|
+
throw new Error("Failed to update payroll");
|
|
1108
|
+
}
|
|
1109
|
+
logger.info("Payroll marked as paid", {
|
|
1110
|
+
payrollId: payrollId.toString()
|
|
1111
|
+
});
|
|
1112
|
+
return updated;
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Mark payroll as processed
|
|
1116
|
+
*/
|
|
1117
|
+
async markAsProcessed(payrollId, options = {}) {
|
|
1118
|
+
const payroll2 = await this.findById(payrollId, options);
|
|
1119
|
+
if (!payroll2) {
|
|
1120
|
+
throw new Error("Payroll not found");
|
|
1121
|
+
}
|
|
1122
|
+
const payrollObj = payroll2.toObject();
|
|
1123
|
+
const updatedData = PayrollFactory.markAsProcessed(payrollObj);
|
|
1124
|
+
const updated = await this.PayrollModel.findByIdAndUpdate(
|
|
1125
|
+
payrollId,
|
|
1126
|
+
updatedData,
|
|
1127
|
+
{ new: true, runValidators: true, session: options.session }
|
|
1128
|
+
);
|
|
1129
|
+
if (!updated) {
|
|
1130
|
+
throw new Error("Failed to update payroll");
|
|
1131
|
+
}
|
|
1132
|
+
return updated;
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Calculate period summary
|
|
1136
|
+
*/
|
|
1137
|
+
async calculatePeriodSummary(organizationId, month, year, options = {}) {
|
|
1138
|
+
const payrolls = await this.findForPeriod(organizationId, month, year, options);
|
|
1139
|
+
const summary = BatchPayrollFactory.calculateTotalPayroll(payrolls);
|
|
1140
|
+
return {
|
|
1141
|
+
period: { month, year },
|
|
1142
|
+
...summary,
|
|
1143
|
+
byStatus: this.groupByStatus(payrolls)
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Get employee payroll history
|
|
1148
|
+
*/
|
|
1149
|
+
async getEmployeePayrollHistory(employeeId, organizationId, limit = 12, options = {}) {
|
|
1150
|
+
return this.findByEmployee(employeeId, organizationId, { ...options, limit });
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Get overview stats
|
|
1154
|
+
*/
|
|
1155
|
+
async getOverviewStats(organizationId, options = {}) {
|
|
1156
|
+
const { month, year } = getCurrentPeriod();
|
|
1157
|
+
const result = await this.calculatePeriodSummary(organizationId, month, year, options);
|
|
1158
|
+
return {
|
|
1159
|
+
currentPeriod: result.period,
|
|
1160
|
+
count: result.count,
|
|
1161
|
+
totalGross: result.totalGross,
|
|
1162
|
+
totalNet: result.totalNet,
|
|
1163
|
+
totalAllowances: result.totalAllowances,
|
|
1164
|
+
totalDeductions: result.totalDeductions,
|
|
1165
|
+
byStatus: result.byStatus
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Group payrolls by status
|
|
1170
|
+
*/
|
|
1171
|
+
groupByStatus(payrolls) {
|
|
1172
|
+
return payrolls.reduce(
|
|
1173
|
+
(acc, payroll2) => {
|
|
1174
|
+
acc[payroll2.status] = (acc[payroll2.status] || 0) + 1;
|
|
1175
|
+
return acc;
|
|
1176
|
+
},
|
|
1177
|
+
{}
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
function createPayrollService(PayrollModel, employeeService) {
|
|
1182
|
+
return new PayrollService(PayrollModel, employeeService);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// src/factories/compensation.factory.ts
|
|
1186
|
+
var CompensationFactory = class {
|
|
1187
|
+
/**
|
|
1188
|
+
* Create compensation object
|
|
1189
|
+
*/
|
|
1190
|
+
static create(params) {
|
|
1191
|
+
const {
|
|
1192
|
+
baseAmount,
|
|
1193
|
+
frequency = "monthly",
|
|
1194
|
+
currency = HRM_CONFIG.payroll.defaultCurrency,
|
|
1195
|
+
allowances = [],
|
|
1196
|
+
deductions = [],
|
|
1197
|
+
effectiveFrom = /* @__PURE__ */ new Date()
|
|
1198
|
+
} = params;
|
|
1199
|
+
return {
|
|
1200
|
+
baseAmount,
|
|
1201
|
+
frequency,
|
|
1202
|
+
currency,
|
|
1203
|
+
allowances: allowances.map((a) => this.createAllowance(a, baseAmount)),
|
|
1204
|
+
deductions: deductions.map((d) => this.createDeduction(d, baseAmount)),
|
|
1205
|
+
effectiveFrom,
|
|
1206
|
+
lastModified: /* @__PURE__ */ new Date()
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Create allowance
|
|
1211
|
+
*/
|
|
1212
|
+
static createAllowance(params, baseAmount) {
|
|
1213
|
+
const amount = params.isPercentage && baseAmount ? applyPercentage(baseAmount, params.value) : params.value;
|
|
1214
|
+
return {
|
|
1215
|
+
type: params.type,
|
|
1216
|
+
name: params.name || params.type,
|
|
1217
|
+
amount,
|
|
1218
|
+
isPercentage: params.isPercentage ?? false,
|
|
1219
|
+
value: params.isPercentage ? params.value : void 0,
|
|
1220
|
+
taxable: params.taxable ?? true,
|
|
1221
|
+
recurring: true,
|
|
1222
|
+
effectiveFrom: /* @__PURE__ */ new Date()
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Create deduction
|
|
1227
|
+
*/
|
|
1228
|
+
static createDeduction(params, baseAmount) {
|
|
1229
|
+
const amount = params.isPercentage && baseAmount ? applyPercentage(baseAmount, params.value) : params.value;
|
|
1230
|
+
return {
|
|
1231
|
+
type: params.type,
|
|
1232
|
+
name: params.name || params.type,
|
|
1233
|
+
amount,
|
|
1234
|
+
isPercentage: params.isPercentage ?? false,
|
|
1235
|
+
value: params.isPercentage ? params.value : void 0,
|
|
1236
|
+
auto: params.auto ?? false,
|
|
1237
|
+
recurring: true,
|
|
1238
|
+
effectiveFrom: /* @__PURE__ */ new Date()
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Update base amount (immutable)
|
|
1243
|
+
*/
|
|
1244
|
+
static updateBaseAmount(compensation, newAmount, effectiveFrom = /* @__PURE__ */ new Date()) {
|
|
1245
|
+
return {
|
|
1246
|
+
...compensation,
|
|
1247
|
+
baseAmount: newAmount,
|
|
1248
|
+
lastModified: effectiveFrom
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Add allowance (immutable)
|
|
1253
|
+
*/
|
|
1254
|
+
static addAllowance(compensation, allowance) {
|
|
1255
|
+
return {
|
|
1256
|
+
...compensation,
|
|
1257
|
+
allowances: [
|
|
1258
|
+
...compensation.allowances,
|
|
1259
|
+
this.createAllowance(allowance, compensation.baseAmount)
|
|
1260
|
+
],
|
|
1261
|
+
lastModified: /* @__PURE__ */ new Date()
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Remove allowance (immutable)
|
|
1266
|
+
*/
|
|
1267
|
+
static removeAllowance(compensation, allowanceType) {
|
|
1268
|
+
return {
|
|
1269
|
+
...compensation,
|
|
1270
|
+
allowances: compensation.allowances.filter((a) => a.type !== allowanceType),
|
|
1271
|
+
lastModified: /* @__PURE__ */ new Date()
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Add deduction (immutable)
|
|
1276
|
+
*/
|
|
1277
|
+
static addDeduction(compensation, deduction) {
|
|
1278
|
+
return {
|
|
1279
|
+
...compensation,
|
|
1280
|
+
deductions: [
|
|
1281
|
+
...compensation.deductions,
|
|
1282
|
+
this.createDeduction(deduction, compensation.baseAmount)
|
|
1283
|
+
],
|
|
1284
|
+
lastModified: /* @__PURE__ */ new Date()
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Remove deduction (immutable)
|
|
1289
|
+
*/
|
|
1290
|
+
static removeDeduction(compensation, deductionType) {
|
|
1291
|
+
return {
|
|
1292
|
+
...compensation,
|
|
1293
|
+
deductions: compensation.deductions.filter((d) => d.type !== deductionType),
|
|
1294
|
+
lastModified: /* @__PURE__ */ new Date()
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Calculate compensation breakdown
|
|
1299
|
+
*/
|
|
1300
|
+
static calculateBreakdown(compensation) {
|
|
1301
|
+
const { baseAmount, allowances, deductions } = compensation;
|
|
1302
|
+
const calculatedAllowances = allowances.map((a) => ({
|
|
1303
|
+
...a,
|
|
1304
|
+
calculatedAmount: a.isPercentage && a.value !== void 0 ? applyPercentage(baseAmount, a.value) : a.amount
|
|
1305
|
+
}));
|
|
1306
|
+
const calculatedDeductions = deductions.map((d) => ({
|
|
1307
|
+
...d,
|
|
1308
|
+
calculatedAmount: d.isPercentage && d.value !== void 0 ? applyPercentage(baseAmount, d.value) : d.amount
|
|
1309
|
+
}));
|
|
1310
|
+
const grossAmount = calculateGross(
|
|
1311
|
+
baseAmount,
|
|
1312
|
+
calculatedAllowances.map((a) => ({ amount: a.calculatedAmount }))
|
|
1313
|
+
);
|
|
1314
|
+
const netAmount = calculateNet(
|
|
1315
|
+
grossAmount,
|
|
1316
|
+
calculatedDeductions.map((d) => ({ amount: d.calculatedAmount }))
|
|
1317
|
+
);
|
|
1318
|
+
return {
|
|
1319
|
+
baseAmount,
|
|
1320
|
+
allowances: calculatedAllowances,
|
|
1321
|
+
deductions: calculatedDeductions,
|
|
1322
|
+
grossAmount,
|
|
1323
|
+
netAmount: Math.max(0, netAmount)
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Apply salary increment (immutable)
|
|
1328
|
+
*/
|
|
1329
|
+
static applyIncrement(compensation, params) {
|
|
1330
|
+
const newBaseAmount = params.amount ? compensation.baseAmount + params.amount : compensation.baseAmount * (1 + (params.percentage || 0) / 100);
|
|
1331
|
+
return this.updateBaseAmount(
|
|
1332
|
+
compensation,
|
|
1333
|
+
Math.round(newBaseAmount),
|
|
1334
|
+
params.effectiveFrom
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
var CompensationBuilder = class {
|
|
1339
|
+
data = {
|
|
1340
|
+
baseAmount: 0,
|
|
1341
|
+
frequency: "monthly",
|
|
1342
|
+
currency: HRM_CONFIG.payroll.defaultCurrency,
|
|
1343
|
+
allowances: [],
|
|
1344
|
+
deductions: []
|
|
1345
|
+
};
|
|
1346
|
+
/**
|
|
1347
|
+
* Set base amount
|
|
1348
|
+
*/
|
|
1349
|
+
withBase(amount, frequency = "monthly", currency = HRM_CONFIG.payroll.defaultCurrency) {
|
|
1350
|
+
this.data.baseAmount = amount;
|
|
1351
|
+
this.data.frequency = frequency;
|
|
1352
|
+
this.data.currency = currency;
|
|
1353
|
+
return this;
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Add allowance
|
|
1357
|
+
*/
|
|
1358
|
+
addAllowance(type, value, isPercentage = false, name) {
|
|
1359
|
+
this.data.allowances = [
|
|
1360
|
+
...this.data.allowances || [],
|
|
1361
|
+
{ type, value, isPercentage, name }
|
|
1362
|
+
];
|
|
1363
|
+
return this;
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Add deduction
|
|
1367
|
+
*/
|
|
1368
|
+
addDeduction(type, value, isPercentage = false, name) {
|
|
1369
|
+
this.data.deductions = [
|
|
1370
|
+
...this.data.deductions || [],
|
|
1371
|
+
{ type, value, isPercentage, name }
|
|
1372
|
+
];
|
|
1373
|
+
return this;
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Set effective date
|
|
1377
|
+
*/
|
|
1378
|
+
effectiveFrom(date) {
|
|
1379
|
+
this.data.effectiveFrom = date;
|
|
1380
|
+
return this;
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* Build compensation
|
|
1384
|
+
*/
|
|
1385
|
+
build() {
|
|
1386
|
+
if (!this.data.baseAmount) {
|
|
1387
|
+
throw new Error("baseAmount is required");
|
|
1388
|
+
}
|
|
1389
|
+
return CompensationFactory.create(this.data);
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
var CompensationPresets = {
|
|
1393
|
+
/**
|
|
1394
|
+
* Basic compensation (base only)
|
|
1395
|
+
*/
|
|
1396
|
+
basic(baseAmount) {
|
|
1397
|
+
return new CompensationBuilder().withBase(baseAmount).build();
|
|
1398
|
+
},
|
|
1399
|
+
/**
|
|
1400
|
+
* With house rent allowance
|
|
1401
|
+
*/
|
|
1402
|
+
withHouseRent(baseAmount, rentPercentage = 50) {
|
|
1403
|
+
return new CompensationBuilder().withBase(baseAmount).addAllowance("housing", rentPercentage, true, "House Rent").build();
|
|
1404
|
+
},
|
|
1405
|
+
/**
|
|
1406
|
+
* With medical allowance
|
|
1407
|
+
*/
|
|
1408
|
+
withMedical(baseAmount, medicalPercentage = 10) {
|
|
1409
|
+
return new CompensationBuilder().withBase(baseAmount).addAllowance("medical", medicalPercentage, true, "Medical Allowance").build();
|
|
1410
|
+
},
|
|
1411
|
+
/**
|
|
1412
|
+
* Standard package (house rent + medical + transport)
|
|
1413
|
+
*/
|
|
1414
|
+
standard(baseAmount) {
|
|
1415
|
+
return new CompensationBuilder().withBase(baseAmount).addAllowance("housing", 50, true, "House Rent").addAllowance("medical", 10, true, "Medical Allowance").addAllowance("transport", 5, true, "Transport Allowance").build();
|
|
1416
|
+
},
|
|
1417
|
+
/**
|
|
1418
|
+
* With provident fund
|
|
1419
|
+
*/
|
|
1420
|
+
withProvidentFund(baseAmount, pfPercentage = 10) {
|
|
1421
|
+
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();
|
|
1422
|
+
},
|
|
1423
|
+
/**
|
|
1424
|
+
* Executive package
|
|
1425
|
+
*/
|
|
1426
|
+
executive(baseAmount) {
|
|
1427
|
+
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();
|
|
1428
|
+
}
|
|
1429
|
+
};
|
|
1430
|
+
|
|
1431
|
+
// src/services/compensation.service.ts
|
|
1432
|
+
var CompensationService = class {
|
|
1433
|
+
constructor(EmployeeModel) {
|
|
1434
|
+
this.EmployeeModel = EmployeeModel;
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Get employee compensation
|
|
1438
|
+
*/
|
|
1439
|
+
async getEmployeeCompensation(employeeId, options = {}) {
|
|
1440
|
+
let query = this.EmployeeModel.findById(toObjectId(employeeId));
|
|
1441
|
+
if (options.session) {
|
|
1442
|
+
query = query.session(options.session);
|
|
1443
|
+
}
|
|
1444
|
+
const employee2 = await query.exec();
|
|
1445
|
+
if (!employee2) {
|
|
1446
|
+
throw new Error("Employee not found");
|
|
1447
|
+
}
|
|
1448
|
+
return employee2.compensation;
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Calculate compensation breakdown
|
|
1452
|
+
*/
|
|
1453
|
+
async calculateBreakdown(employeeId, options = {}) {
|
|
1454
|
+
const compensation = await this.getEmployeeCompensation(employeeId, options);
|
|
1455
|
+
return CompensationFactory.calculateBreakdown(compensation);
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* Update base amount
|
|
1459
|
+
*/
|
|
1460
|
+
async updateBaseAmount(employeeId, newAmount, effectiveFrom = /* @__PURE__ */ new Date(), options = {}) {
|
|
1461
|
+
const employee2 = await this.findEmployee(employeeId, options);
|
|
1462
|
+
const updatedCompensation = CompensationFactory.updateBaseAmount(
|
|
1463
|
+
employee2.compensation,
|
|
1464
|
+
newAmount,
|
|
1465
|
+
effectiveFrom
|
|
1466
|
+
);
|
|
1467
|
+
employee2.compensation = updatedCompensation;
|
|
1468
|
+
await employee2.save({ session: options.session });
|
|
1469
|
+
logger.info("Compensation base amount updated", {
|
|
1470
|
+
employeeId: employee2.employeeId,
|
|
1471
|
+
newAmount
|
|
1472
|
+
});
|
|
1473
|
+
return this.calculateBreakdown(employeeId, options);
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Apply salary increment
|
|
1477
|
+
*/
|
|
1478
|
+
async applyIncrement(employeeId, params, options = {}) {
|
|
1479
|
+
const employee2 = await this.findEmployee(employeeId, options);
|
|
1480
|
+
const previousAmount = employee2.compensation.baseAmount;
|
|
1481
|
+
const updatedCompensation = CompensationFactory.applyIncrement(
|
|
1482
|
+
employee2.compensation,
|
|
1483
|
+
params
|
|
1484
|
+
);
|
|
1485
|
+
employee2.compensation = updatedCompensation;
|
|
1486
|
+
await employee2.save({ session: options.session });
|
|
1487
|
+
logger.info("Salary increment applied", {
|
|
1488
|
+
employeeId: employee2.employeeId,
|
|
1489
|
+
previousAmount,
|
|
1490
|
+
newAmount: updatedCompensation.baseAmount,
|
|
1491
|
+
percentage: params.percentage
|
|
1492
|
+
});
|
|
1493
|
+
return this.calculateBreakdown(employeeId, options);
|
|
1494
|
+
}
|
|
1495
|
+
/**
|
|
1496
|
+
* Add allowance
|
|
1497
|
+
*/
|
|
1498
|
+
async addAllowance(employeeId, allowance, options = {}) {
|
|
1499
|
+
const employee2 = await this.findEmployee(employeeId, options);
|
|
1500
|
+
const updatedCompensation = CompensationFactory.addAllowance(
|
|
1501
|
+
employee2.compensation,
|
|
1502
|
+
allowance
|
|
1503
|
+
);
|
|
1504
|
+
employee2.compensation = updatedCompensation;
|
|
1505
|
+
await employee2.save({ session: options.session });
|
|
1506
|
+
logger.info("Allowance added", {
|
|
1507
|
+
employeeId: employee2.employeeId,
|
|
1508
|
+
type: allowance.type,
|
|
1509
|
+
value: allowance.value
|
|
1510
|
+
});
|
|
1511
|
+
return this.calculateBreakdown(employeeId, options);
|
|
1512
|
+
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Remove allowance
|
|
1515
|
+
*/
|
|
1516
|
+
async removeAllowance(employeeId, allowanceType, options = {}) {
|
|
1517
|
+
const employee2 = await this.findEmployee(employeeId, options);
|
|
1518
|
+
const updatedCompensation = CompensationFactory.removeAllowance(
|
|
1519
|
+
employee2.compensation,
|
|
1520
|
+
allowanceType
|
|
1521
|
+
);
|
|
1522
|
+
employee2.compensation = updatedCompensation;
|
|
1523
|
+
await employee2.save({ session: options.session });
|
|
1524
|
+
logger.info("Allowance removed", {
|
|
1525
|
+
employeeId: employee2.employeeId,
|
|
1526
|
+
type: allowanceType
|
|
1527
|
+
});
|
|
1528
|
+
return this.calculateBreakdown(employeeId, options);
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Add deduction
|
|
1532
|
+
*/
|
|
1533
|
+
async addDeduction(employeeId, deduction, options = {}) {
|
|
1534
|
+
const employee2 = await this.findEmployee(employeeId, options);
|
|
1535
|
+
const updatedCompensation = CompensationFactory.addDeduction(
|
|
1536
|
+
employee2.compensation,
|
|
1537
|
+
deduction
|
|
1538
|
+
);
|
|
1539
|
+
employee2.compensation = updatedCompensation;
|
|
1540
|
+
await employee2.save({ session: options.session });
|
|
1541
|
+
logger.info("Deduction added", {
|
|
1542
|
+
employeeId: employee2.employeeId,
|
|
1543
|
+
type: deduction.type,
|
|
1544
|
+
value: deduction.value
|
|
1545
|
+
});
|
|
1546
|
+
return this.calculateBreakdown(employeeId, options);
|
|
1547
|
+
}
|
|
1548
|
+
/**
|
|
1549
|
+
* Remove deduction
|
|
1550
|
+
*/
|
|
1551
|
+
async removeDeduction(employeeId, deductionType, options = {}) {
|
|
1552
|
+
const employee2 = await this.findEmployee(employeeId, options);
|
|
1553
|
+
const updatedCompensation = CompensationFactory.removeDeduction(
|
|
1554
|
+
employee2.compensation,
|
|
1555
|
+
deductionType
|
|
1556
|
+
);
|
|
1557
|
+
employee2.compensation = updatedCompensation;
|
|
1558
|
+
await employee2.save({ session: options.session });
|
|
1559
|
+
logger.info("Deduction removed", {
|
|
1560
|
+
employeeId: employee2.employeeId,
|
|
1561
|
+
type: deductionType
|
|
1562
|
+
});
|
|
1563
|
+
return this.calculateBreakdown(employeeId, options);
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Set standard compensation
|
|
1567
|
+
*/
|
|
1568
|
+
async setStandardCompensation(employeeId, baseAmount, options = {}) {
|
|
1569
|
+
const employee2 = await this.findEmployee(employeeId, options);
|
|
1570
|
+
employee2.compensation = CompensationPresets.standard(baseAmount);
|
|
1571
|
+
await employee2.save({ session: options.session });
|
|
1572
|
+
logger.info("Standard compensation set", {
|
|
1573
|
+
employeeId: employee2.employeeId,
|
|
1574
|
+
baseAmount
|
|
1575
|
+
});
|
|
1576
|
+
return this.calculateBreakdown(employeeId, options);
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* Compare compensation between two employees
|
|
1580
|
+
*/
|
|
1581
|
+
async compareCompensation(employeeId1, employeeId2, options = {}) {
|
|
1582
|
+
const breakdown1 = await this.calculateBreakdown(employeeId1, options);
|
|
1583
|
+
const breakdown2 = await this.calculateBreakdown(employeeId2, options);
|
|
1584
|
+
return {
|
|
1585
|
+
employee1: breakdown1,
|
|
1586
|
+
employee2: breakdown2,
|
|
1587
|
+
difference: {
|
|
1588
|
+
base: breakdown2.baseAmount - breakdown1.baseAmount,
|
|
1589
|
+
gross: breakdown2.grossAmount - breakdown1.grossAmount,
|
|
1590
|
+
net: breakdown2.netAmount - breakdown1.netAmount
|
|
1591
|
+
},
|
|
1592
|
+
ratio: {
|
|
1593
|
+
base: breakdown1.baseAmount > 0 ? breakdown2.baseAmount / breakdown1.baseAmount : 0,
|
|
1594
|
+
gross: breakdown1.grossAmount > 0 ? breakdown2.grossAmount / breakdown1.grossAmount : 0,
|
|
1595
|
+
net: breakdown1.netAmount > 0 ? breakdown2.netAmount / breakdown1.netAmount : 0
|
|
1596
|
+
}
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Get department compensation stats
|
|
1601
|
+
*/
|
|
1602
|
+
async getDepartmentCompensationStats(organizationId, department, options = {}) {
|
|
1603
|
+
let query = this.EmployeeModel.find({
|
|
1604
|
+
organizationId: toObjectId(organizationId),
|
|
1605
|
+
department,
|
|
1606
|
+
status: { $in: ["active", "on_leave"] }
|
|
1607
|
+
});
|
|
1608
|
+
if (options.session) {
|
|
1609
|
+
query = query.session(options.session);
|
|
1610
|
+
}
|
|
1611
|
+
const employees = await query.exec();
|
|
1612
|
+
const breakdowns = employees.map(
|
|
1613
|
+
(emp) => CompensationFactory.calculateBreakdown(emp.compensation)
|
|
1614
|
+
);
|
|
1615
|
+
const totals = breakdowns.reduce(
|
|
1616
|
+
(acc, breakdown) => ({
|
|
1617
|
+
totalBase: acc.totalBase + breakdown.baseAmount,
|
|
1618
|
+
totalGross: acc.totalGross + breakdown.grossAmount,
|
|
1619
|
+
totalNet: acc.totalNet + breakdown.netAmount
|
|
1620
|
+
}),
|
|
1621
|
+
{ totalBase: 0, totalGross: 0, totalNet: 0 }
|
|
1622
|
+
);
|
|
1623
|
+
const count = employees.length || 1;
|
|
1624
|
+
return {
|
|
1625
|
+
department,
|
|
1626
|
+
employeeCount: employees.length,
|
|
1627
|
+
...totals,
|
|
1628
|
+
averageBase: Math.round(totals.totalBase / count),
|
|
1629
|
+
averageGross: Math.round(totals.totalGross / count),
|
|
1630
|
+
averageNet: Math.round(totals.totalNet / count)
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Get organization compensation stats
|
|
1635
|
+
*/
|
|
1636
|
+
async getOrganizationCompensationStats(organizationId, options = {}) {
|
|
1637
|
+
let query = this.EmployeeModel.find({
|
|
1638
|
+
organizationId: toObjectId(organizationId),
|
|
1639
|
+
status: { $in: ["active", "on_leave"] }
|
|
1640
|
+
});
|
|
1641
|
+
if (options.session) {
|
|
1642
|
+
query = query.session(options.session);
|
|
1643
|
+
}
|
|
1644
|
+
const employees = await query.exec();
|
|
1645
|
+
const breakdowns = employees.map(
|
|
1646
|
+
(emp) => CompensationFactory.calculateBreakdown(emp.compensation)
|
|
1647
|
+
);
|
|
1648
|
+
const totals = breakdowns.reduce(
|
|
1649
|
+
(acc, breakdown) => ({
|
|
1650
|
+
totalBase: acc.totalBase + breakdown.baseAmount,
|
|
1651
|
+
totalGross: acc.totalGross + breakdown.grossAmount,
|
|
1652
|
+
totalNet: acc.totalNet + breakdown.netAmount
|
|
1653
|
+
}),
|
|
1654
|
+
{ totalBase: 0, totalGross: 0, totalNet: 0 }
|
|
1655
|
+
);
|
|
1656
|
+
const byDepartment = {};
|
|
1657
|
+
employees.forEach((emp, i) => {
|
|
1658
|
+
const dept = emp.department || "unassigned";
|
|
1659
|
+
if (!byDepartment[dept]) {
|
|
1660
|
+
byDepartment[dept] = { count: 0, totalNet: 0 };
|
|
1661
|
+
}
|
|
1662
|
+
byDepartment[dept].count++;
|
|
1663
|
+
byDepartment[dept].totalNet += breakdowns[i].netAmount;
|
|
1664
|
+
});
|
|
1665
|
+
const count = employees.length || 1;
|
|
1666
|
+
return {
|
|
1667
|
+
employeeCount: employees.length,
|
|
1668
|
+
...totals,
|
|
1669
|
+
averageBase: Math.round(totals.totalBase / count),
|
|
1670
|
+
averageGross: Math.round(totals.totalGross / count),
|
|
1671
|
+
averageNet: Math.round(totals.totalNet / count),
|
|
1672
|
+
byDepartment
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* Find employee helper
|
|
1677
|
+
*/
|
|
1678
|
+
async findEmployee(employeeId, options = {}) {
|
|
1679
|
+
let query = this.EmployeeModel.findById(toObjectId(employeeId));
|
|
1680
|
+
if (options.session) {
|
|
1681
|
+
query = query.session(options.session);
|
|
1682
|
+
}
|
|
1683
|
+
const employee2 = await query.exec();
|
|
1684
|
+
if (!employee2) {
|
|
1685
|
+
throw new Error("Employee not found");
|
|
1686
|
+
}
|
|
1687
|
+
return employee2;
|
|
1688
|
+
}
|
|
1689
|
+
};
|
|
1690
|
+
function createCompensationService(EmployeeModel) {
|
|
1691
|
+
return new CompensationService(EmployeeModel);
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
export { CompensationService, EmployeeService, PayrollService, createCompensationService, createEmployeeService, createPayrollService };
|
|
1695
|
+
//# sourceMappingURL=index.js.map
|
|
1696
|
+
//# sourceMappingURL=index.js.map
|