@classytic/payroll 2.7.5 → 2.8.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 +333 -323
- package/dist/attendance.calculator-BZcv2iii.d.ts +336 -0
- package/dist/calculators/index.d.ts +3 -299
- package/dist/calculators/index.js +154 -19
- package/dist/calculators/index.js.map +1 -1
- package/dist/core/index.d.ts +321 -0
- package/dist/core/index.js +1962 -0
- package/dist/core/index.js.map +1 -0
- package/dist/{employee-identity-Cq2wo9-2.d.ts → error-helpers-Bm6lMny2.d.ts} +257 -7
- package/dist/{index-DjB72l6e.d.ts → index-BKLkuSAs.d.ts} +248 -132
- package/dist/index.d.ts +418 -658
- package/dist/index.js +1179 -373
- package/dist/index.js.map +1 -1
- package/dist/payroll-states-DBt0XVm-.d.ts +598 -0
- package/dist/{prorating.calculator-C7sdFiG2.d.ts → prorating.calculator-C33fWBQf.d.ts} +2 -2
- package/dist/schemas/index.d.ts +2 -2
- package/dist/schemas/index.js +95 -75
- package/dist/schemas/index.js.map +1 -1
- package/dist/{types-BVDjiVGS.d.ts → types-bZdAJueH.d.ts} +427 -12
- package/dist/utils/index.d.ts +17 -5
- package/dist/utils/index.js +185 -25
- package/dist/utils/index.js.map +1 -1
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import mongoose5, { Schema, Types, isValidObjectId as isValidObjectId$1 } from 'mongoose';
|
|
2
|
+
import { MongoServerError } from 'mongodb';
|
|
2
3
|
import pLimit from 'p-limit';
|
|
3
4
|
import { LRUCache } from 'lru-cache';
|
|
4
5
|
import crypto from 'crypto';
|
|
5
6
|
import { Repository } from '@classytic/mongokit';
|
|
6
|
-
import { MongoServerError } from 'mongodb';
|
|
7
7
|
import { isTransaction } from '@classytic/shared-types';
|
|
8
8
|
|
|
9
9
|
var __defProp = Object.defineProperty;
|
|
10
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
11
10
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
12
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
13
11
|
var __esm = (fn, res) => function __init() {
|
|
14
12
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
15
13
|
};
|
|
@@ -17,15 +15,6 @@ var __export = (target, all) => {
|
|
|
17
15
|
for (var name in all)
|
|
18
16
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
19
17
|
};
|
|
20
|
-
var __copyProps = (to, from, except, desc) => {
|
|
21
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
22
|
-
for (let key of __getOwnPropNames(from))
|
|
23
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
24
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
25
|
-
}
|
|
26
|
-
return to;
|
|
27
|
-
};
|
|
28
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
18
|
|
|
30
19
|
// src/enums.ts
|
|
31
20
|
function isVoidablePayrollStatus(status) {
|
|
@@ -204,19 +193,6 @@ var init_enums = __esm({
|
|
|
204
193
|
});
|
|
205
194
|
|
|
206
195
|
// src/utils/logger.ts
|
|
207
|
-
var logger_exports = {};
|
|
208
|
-
__export(logger_exports, {
|
|
209
|
-
createChildLogger: () => createChildLogger,
|
|
210
|
-
createSilentLogger: () => createSilentLogger,
|
|
211
|
-
default: () => logger_default,
|
|
212
|
-
disableLogging: () => disableLogging,
|
|
213
|
-
enableLogging: () => enableLogging,
|
|
214
|
-
getLogger: () => getLogger,
|
|
215
|
-
isLoggingEnabled: () => isLoggingEnabled,
|
|
216
|
-
logger: () => logger,
|
|
217
|
-
resetLogger: () => resetLogger,
|
|
218
|
-
setLogger: () => setLogger
|
|
219
|
-
});
|
|
220
196
|
function getLogger() {
|
|
221
197
|
return {
|
|
222
198
|
info: (message, meta) => {
|
|
@@ -236,48 +212,7 @@ function getLogger() {
|
|
|
236
212
|
function setLogger(logger2) {
|
|
237
213
|
currentLogger = logger2;
|
|
238
214
|
}
|
|
239
|
-
|
|
240
|
-
currentLogger = createConsoleLogger();
|
|
241
|
-
}
|
|
242
|
-
function createChildLogger(prefix) {
|
|
243
|
-
const parent = currentLogger;
|
|
244
|
-
return {
|
|
245
|
-
info: (message, meta) => {
|
|
246
|
-
if (loggingEnabled) parent.info(`[${prefix}] ${message}`, meta);
|
|
247
|
-
},
|
|
248
|
-
error: (message, meta) => {
|
|
249
|
-
if (loggingEnabled) parent.error(`[${prefix}] ${message}`, meta);
|
|
250
|
-
},
|
|
251
|
-
warn: (message, meta) => {
|
|
252
|
-
if (loggingEnabled) parent.warn(`[${prefix}] ${message}`, meta);
|
|
253
|
-
},
|
|
254
|
-
debug: (message, meta) => {
|
|
255
|
-
if (loggingEnabled) parent.debug(`[${prefix}] ${message}`, meta);
|
|
256
|
-
}
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
function createSilentLogger() {
|
|
260
|
-
return {
|
|
261
|
-
info: () => {
|
|
262
|
-
},
|
|
263
|
-
error: () => {
|
|
264
|
-
},
|
|
265
|
-
warn: () => {
|
|
266
|
-
},
|
|
267
|
-
debug: () => {
|
|
268
|
-
}
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
function enableLogging() {
|
|
272
|
-
loggingEnabled = true;
|
|
273
|
-
}
|
|
274
|
-
function disableLogging() {
|
|
275
|
-
loggingEnabled = false;
|
|
276
|
-
}
|
|
277
|
-
function isLoggingEnabled() {
|
|
278
|
-
return loggingEnabled;
|
|
279
|
-
}
|
|
280
|
-
var createConsoleLogger, currentLogger, loggingEnabled, logger, logger_default;
|
|
215
|
+
var createConsoleLogger, currentLogger, loggingEnabled, logger;
|
|
281
216
|
var init_logger = __esm({
|
|
282
217
|
"src/utils/logger.ts"() {
|
|
283
218
|
createConsoleLogger = () => ({
|
|
@@ -328,7 +263,6 @@ var init_logger = __esm({
|
|
|
328
263
|
if (loggingEnabled) currentLogger.debug(message, meta);
|
|
329
264
|
}
|
|
330
265
|
};
|
|
331
|
-
logger_default = logger;
|
|
332
266
|
}
|
|
333
267
|
});
|
|
334
268
|
|
|
@@ -552,6 +486,11 @@ var init_payroll_states = __esm({
|
|
|
552
486
|
});
|
|
553
487
|
|
|
554
488
|
// src/utils/date.ts
|
|
489
|
+
function addDays(date, days) {
|
|
490
|
+
const result = new Date(date);
|
|
491
|
+
result.setDate(result.getDate() + days);
|
|
492
|
+
return result;
|
|
493
|
+
}
|
|
555
494
|
function addMonths(date, months) {
|
|
556
495
|
const result = new Date(date);
|
|
557
496
|
result.setMonth(result.getMonth() + months);
|
|
@@ -569,10 +508,26 @@ function endOfMonth(date) {
|
|
|
569
508
|
result.setHours(23, 59, 59, 999);
|
|
570
509
|
return result;
|
|
571
510
|
}
|
|
511
|
+
function startOfDay(date) {
|
|
512
|
+
const result = new Date(date);
|
|
513
|
+
result.setHours(0, 0, 0, 0);
|
|
514
|
+
return result;
|
|
515
|
+
}
|
|
516
|
+
function toUTCDateString(date) {
|
|
517
|
+
const d = new Date(date);
|
|
518
|
+
d.setHours(0, 0, 0, 0);
|
|
519
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
520
|
+
}
|
|
572
521
|
function diffInDays(start, end) {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
);
|
|
522
|
+
const s = new Date(start);
|
|
523
|
+
const e = new Date(end);
|
|
524
|
+
s.setHours(0, 0, 0, 0);
|
|
525
|
+
e.setHours(0, 0, 0, 0);
|
|
526
|
+
return Math.ceil((e.getTime() - s.getTime()) / (1e3 * 60 * 60 * 24));
|
|
527
|
+
}
|
|
528
|
+
function isWeekday(date) {
|
|
529
|
+
const day = new Date(date).getDay();
|
|
530
|
+
return day >= 1 && day <= 5;
|
|
576
531
|
}
|
|
577
532
|
function getPayPeriod(month, year) {
|
|
578
533
|
const startDate = new Date(year, month - 1, 1);
|
|
@@ -583,6 +538,59 @@ function getPayPeriod(month, year) {
|
|
|
583
538
|
endDate: endOfMonth(startDate)
|
|
584
539
|
};
|
|
585
540
|
}
|
|
541
|
+
function getPayPeriodForFrequency(frequency, paymentDate, month, year) {
|
|
542
|
+
switch (frequency) {
|
|
543
|
+
case "monthly": {
|
|
544
|
+
const period = getPayPeriod(month, year);
|
|
545
|
+
const workingDays = getWorkingDaysInMonth(year, month);
|
|
546
|
+
return { ...period, workingDays };
|
|
547
|
+
}
|
|
548
|
+
case "bi_weekly": {
|
|
549
|
+
const endDate = startOfDay(paymentDate);
|
|
550
|
+
const startDate = addDays(endDate, -13);
|
|
551
|
+
const workingDays = countWeekdaysInRange(startDate, endDate);
|
|
552
|
+
return { month, year, startDate, endDate, workingDays };
|
|
553
|
+
}
|
|
554
|
+
case "weekly": {
|
|
555
|
+
const endDate = startOfDay(paymentDate);
|
|
556
|
+
const startDate = addDays(endDate, -6);
|
|
557
|
+
const workingDays = countWeekdaysInRange(startDate, endDate);
|
|
558
|
+
return { month, year, startDate, endDate, workingDays };
|
|
559
|
+
}
|
|
560
|
+
case "daily":
|
|
561
|
+
case "hourly": {
|
|
562
|
+
const date = startOfDay(paymentDate);
|
|
563
|
+
const workingDays = isWeekday(date) ? 1 : 0;
|
|
564
|
+
return { month, year, startDate: date, endDate: date, workingDays };
|
|
565
|
+
}
|
|
566
|
+
default:
|
|
567
|
+
return getPayPeriodForFrequency("monthly", paymentDate, month, year);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function countWeekdaysInRange(start, end) {
|
|
571
|
+
let count = 0;
|
|
572
|
+
const current = new Date(start);
|
|
573
|
+
while (current <= end) {
|
|
574
|
+
if (isWeekday(current)) {
|
|
575
|
+
count++;
|
|
576
|
+
}
|
|
577
|
+
current.setDate(current.getDate() + 1);
|
|
578
|
+
}
|
|
579
|
+
return count;
|
|
580
|
+
}
|
|
581
|
+
function getWorkingDaysInMonth(year, month) {
|
|
582
|
+
const start = new Date(year, month - 1, 1);
|
|
583
|
+
const end = endOfMonth(start);
|
|
584
|
+
let count = 0;
|
|
585
|
+
const current = new Date(start);
|
|
586
|
+
while (current <= end) {
|
|
587
|
+
if (isWeekday(current)) {
|
|
588
|
+
count++;
|
|
589
|
+
}
|
|
590
|
+
current.setDate(current.getDate() + 1);
|
|
591
|
+
}
|
|
592
|
+
return count;
|
|
593
|
+
}
|
|
586
594
|
function calculateProbationEnd(hireDate, probationMonths) {
|
|
587
595
|
if (!probationMonths || probationMonths <= 0) return null;
|
|
588
596
|
return addMonths(hireDate, probationMonths);
|
|
@@ -833,11 +841,14 @@ function createError(code, message, context) {
|
|
|
833
841
|
EMPLOYEE_NOT_FOUND: 404,
|
|
834
842
|
INVALID_EMPLOYEE: 400,
|
|
835
843
|
DUPLICATE_PAYROLL: 409,
|
|
844
|
+
VOIDED_PAYROLL_REPROCESS: 409,
|
|
836
845
|
VALIDATION_ERROR: 400,
|
|
837
846
|
EMPLOYEE_TERMINATED: 400,
|
|
838
847
|
ALREADY_PROCESSED: 409,
|
|
839
848
|
NOT_ELIGIBLE: 400,
|
|
840
|
-
SECURITY_ERROR: 403
|
|
849
|
+
SECURITY_ERROR: 403,
|
|
850
|
+
EXPORT_NOT_FOUND: 404,
|
|
851
|
+
EXPORT_ORG_MISMATCH: 403
|
|
841
852
|
};
|
|
842
853
|
return new PayrollError(message, code, statusMap[code] || 500, context ?? {});
|
|
843
854
|
}
|
|
@@ -944,12 +955,13 @@ var init_errors = __esm({
|
|
|
944
955
|
}
|
|
945
956
|
};
|
|
946
957
|
DuplicatePayrollError = class extends PayrollError {
|
|
947
|
-
constructor(employeeId, month, year, context) {
|
|
958
|
+
constructor(employeeId, month, year, payrollRunType, context) {
|
|
959
|
+
const runTypeInfo = payrollRunType ? ` (${payrollRunType})` : "";
|
|
948
960
|
super(
|
|
949
|
-
`Payroll already processed for employee ${employeeId} in ${month}/${year}`,
|
|
961
|
+
`Payroll already processed for employee ${employeeId} in ${month}/${year}${runTypeInfo}`,
|
|
950
962
|
"DUPLICATE_PAYROLL",
|
|
951
963
|
409,
|
|
952
|
-
{ employeeId, month, year, ...context }
|
|
964
|
+
{ employeeId, month, year, payrollRunType, ...context }
|
|
953
965
|
);
|
|
954
966
|
}
|
|
955
967
|
};
|
|
@@ -993,7 +1005,7 @@ var init_errors = __esm({
|
|
|
993
1005
|
function countWorkingDays(startDate, endDate, options = {}) {
|
|
994
1006
|
const workDays = options.workingDays || DEFAULT_WORK_SCHEDULE.workingDays;
|
|
995
1007
|
const holidaySet = new Set(
|
|
996
|
-
(options.holidays || []).map((d) =>
|
|
1008
|
+
(options.holidays || []).map((d) => toUTCDateString(d))
|
|
997
1009
|
);
|
|
998
1010
|
let totalDays = 0;
|
|
999
1011
|
let workingDays = 0;
|
|
@@ -1005,7 +1017,7 @@ function countWorkingDays(startDate, endDate, options = {}) {
|
|
|
1005
1017
|
end.setHours(0, 0, 0, 0);
|
|
1006
1018
|
while (current <= end) {
|
|
1007
1019
|
totalDays++;
|
|
1008
|
-
const isHoliday = holidaySet.has(current
|
|
1020
|
+
const isHoliday = holidaySet.has(toUTCDateString(current));
|
|
1009
1021
|
const isWorkDay = workDays.includes(current.getDay());
|
|
1010
1022
|
if (isHoliday) {
|
|
1011
1023
|
holidays++;
|
|
@@ -1021,6 +1033,7 @@ function countWorkingDays(startDate, endDate, options = {}) {
|
|
|
1021
1033
|
var DEFAULT_WORK_SCHEDULE;
|
|
1022
1034
|
var init_config = __esm({
|
|
1023
1035
|
"src/core/config.ts"() {
|
|
1036
|
+
init_date();
|
|
1024
1037
|
DEFAULT_WORK_SCHEDULE = {
|
|
1025
1038
|
workingDays: [1, 2, 3, 4, 5],
|
|
1026
1039
|
// Monday to Friday
|
|
@@ -1089,7 +1102,7 @@ function calculateProRating(input) {
|
|
|
1089
1102
|
};
|
|
1090
1103
|
}
|
|
1091
1104
|
function applyProRating(baseAmount, ratio) {
|
|
1092
|
-
return
|
|
1105
|
+
return roundMoney(baseAmount * ratio, 2);
|
|
1093
1106
|
}
|
|
1094
1107
|
function shouldProRate(hireDate, terminationDate, periodStart, periodEnd) {
|
|
1095
1108
|
const hire = new Date(hireDate);
|
|
@@ -1101,6 +1114,75 @@ function shouldProRate(hireDate, terminationDate, periodStart, periodEnd) {
|
|
|
1101
1114
|
var init_prorating_calculator = __esm({
|
|
1102
1115
|
"src/calculators/prorating.calculator.ts"() {
|
|
1103
1116
|
init_config();
|
|
1117
|
+
init_money();
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
function isMongoError(error) {
|
|
1121
|
+
return error instanceof MongoServerError;
|
|
1122
|
+
}
|
|
1123
|
+
function isDuplicateKeyError(error) {
|
|
1124
|
+
return isMongoError(error) && error.code === 11e3;
|
|
1125
|
+
}
|
|
1126
|
+
function parseDuplicateKeyError(error) {
|
|
1127
|
+
const message = error.message || "";
|
|
1128
|
+
const match = message.match(/dup key: \{ ([^:]+):/);
|
|
1129
|
+
if (match && match[1]) {
|
|
1130
|
+
return match[1].trim();
|
|
1131
|
+
}
|
|
1132
|
+
const errmsgMatch = message.match(/index: ([^_]+)_/);
|
|
1133
|
+
if (errmsgMatch && errmsgMatch[1]) {
|
|
1134
|
+
return errmsgMatch[1].trim();
|
|
1135
|
+
}
|
|
1136
|
+
return "unknown";
|
|
1137
|
+
}
|
|
1138
|
+
function isTransactionError(error) {
|
|
1139
|
+
if (!isMongoError(error)) return false;
|
|
1140
|
+
const message = (error.message || "").toLowerCase();
|
|
1141
|
+
return message.includes("transaction") || message.includes("session") || message.includes("replica set") || error.code === 251 || // NoSuchTransaction
|
|
1142
|
+
error.code === 225 || // TransactionTooOld
|
|
1143
|
+
error.code === 244 || // TransactionAborted
|
|
1144
|
+
error.code === 256 || // TransactionCommitted
|
|
1145
|
+
error.code === 257;
|
|
1146
|
+
}
|
|
1147
|
+
function isTransactionUnsupportedError(error) {
|
|
1148
|
+
if (!isMongoError(error)) return false;
|
|
1149
|
+
const message = (error.message || "").toLowerCase();
|
|
1150
|
+
return message.includes("transaction numbers are only allowed on a replica set member") || message.includes("transactions are only supported on replica sets") || message.includes("mongos") || error.code === 20;
|
|
1151
|
+
}
|
|
1152
|
+
function isConnectionError(error) {
|
|
1153
|
+
if (!isMongoError(error)) return false;
|
|
1154
|
+
return error.code === 11600 || // InterruptedAtShutdown
|
|
1155
|
+
error.code === 11602 || // InterruptedDueToReplStateChange
|
|
1156
|
+
error.code === 91 || // ShutdownInProgress
|
|
1157
|
+
error.code === 89 || // NetworkTimeout
|
|
1158
|
+
error.code === 6;
|
|
1159
|
+
}
|
|
1160
|
+
function isGuestEmployee(employee2) {
|
|
1161
|
+
return !employee2.userId;
|
|
1162
|
+
}
|
|
1163
|
+
function hasUserId(employee2) {
|
|
1164
|
+
return !!employee2.userId;
|
|
1165
|
+
}
|
|
1166
|
+
function isValidationError(error) {
|
|
1167
|
+
return error instanceof Error && error.name === "ValidationError";
|
|
1168
|
+
}
|
|
1169
|
+
function isError(error) {
|
|
1170
|
+
return error instanceof Error;
|
|
1171
|
+
}
|
|
1172
|
+
function getErrorMessage(error) {
|
|
1173
|
+
if (isError(error)) {
|
|
1174
|
+
return error.message;
|
|
1175
|
+
}
|
|
1176
|
+
if (typeof error === "string") {
|
|
1177
|
+
return error;
|
|
1178
|
+
}
|
|
1179
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
1180
|
+
return String(error.message);
|
|
1181
|
+
}
|
|
1182
|
+
return "Unknown error occurred";
|
|
1183
|
+
}
|
|
1184
|
+
var init_type_guards = __esm({
|
|
1185
|
+
"src/utils/type-guards.ts"() {
|
|
1104
1186
|
}
|
|
1105
1187
|
});
|
|
1106
1188
|
|
|
@@ -1112,7 +1194,7 @@ function calculateLeaveDays(startDate, endDate, options = {}) {
|
|
|
1112
1194
|
holidays = [],
|
|
1113
1195
|
includeEndDate = true
|
|
1114
1196
|
} = options;
|
|
1115
|
-
const holidaySet = new Set(holidays.map((d) =>
|
|
1197
|
+
const holidaySet = new Set(holidays.map((d) => toUTCDateString(d)));
|
|
1116
1198
|
let count = 0;
|
|
1117
1199
|
const current = new Date(startDate);
|
|
1118
1200
|
current.setHours(0, 0, 0, 0);
|
|
@@ -1123,7 +1205,7 @@ function calculateLeaveDays(startDate, endDate, options = {}) {
|
|
|
1123
1205
|
}
|
|
1124
1206
|
while (current <= end) {
|
|
1125
1207
|
const isWorkDay = workingDays.includes(current.getDay());
|
|
1126
|
-
const isHoliday = holidaySet.has(current
|
|
1208
|
+
const isHoliday = holidaySet.has(toUTCDateString(current));
|
|
1127
1209
|
if (isWorkDay && !isHoliday) {
|
|
1128
1210
|
count++;
|
|
1129
1211
|
}
|
|
@@ -1194,8 +1276,8 @@ function initializeLeaveBalances(hireDate, config = {}, year = (/* @__PURE__ */
|
|
|
1194
1276
|
const fiscalYearEnd = new Date(year + 1, fiscalYearStartMonth - 1, 0);
|
|
1195
1277
|
let prorationRatio = 1;
|
|
1196
1278
|
if (proRateNewHires && hireDate > fiscalYearStart) {
|
|
1197
|
-
const totalDays =
|
|
1198
|
-
const remainingDays =
|
|
1279
|
+
const totalDays = diffInDays(fiscalYearStart, fiscalYearEnd);
|
|
1280
|
+
const remainingDays = diffInDays(hireDate, fiscalYearEnd);
|
|
1199
1281
|
prorationRatio = Math.max(0, Math.min(1, remainingDays / totalDays));
|
|
1200
1282
|
}
|
|
1201
1283
|
const balances = [];
|
|
@@ -1219,14 +1301,14 @@ function proRateAllocation(fullAllocation, hireDate, fiscalYearStartMonth = 1, y
|
|
|
1219
1301
|
return fullAllocation;
|
|
1220
1302
|
}
|
|
1221
1303
|
const fiscalYearEnd = new Date(year + 1, fiscalYearStartMonth - 1, 0);
|
|
1222
|
-
const totalDays =
|
|
1223
|
-
const remainingDays = Math.max(0,
|
|
1304
|
+
const totalDays = diffInDays(fiscalYearStart, fiscalYearEnd);
|
|
1305
|
+
const remainingDays = Math.max(0, diffInDays(hireDate, fiscalYearEnd));
|
|
1224
1306
|
return Math.round(fullAllocation * remainingDays / totalDays);
|
|
1225
1307
|
}
|
|
1226
1308
|
function calculateUnpaidLeaveDeduction(baseSalary, unpaidDays, workingDaysInMonth) {
|
|
1227
1309
|
if (unpaidDays <= 0 || workingDaysInMonth <= 0) return 0;
|
|
1228
|
-
const dailyRate = baseSalary / workingDaysInMonth;
|
|
1229
|
-
return
|
|
1310
|
+
const dailyRate = roundMoney(baseSalary / workingDaysInMonth, 2);
|
|
1311
|
+
return roundMoney(dailyRate * unpaidDays, 2);
|
|
1230
1312
|
}
|
|
1231
1313
|
function getUnpaidLeaveDays(leaveRequests, status = "approved") {
|
|
1232
1314
|
return leaveRequests.filter((r) => r.type === "unpaid" && r.status === status).reduce((sum, r) => sum + r.days, 0);
|
|
@@ -1265,16 +1347,11 @@ function accrueLeaveToBalance(balances, type, amount, year = (/* @__PURE__ */ ne
|
|
|
1265
1347
|
}
|
|
1266
1348
|
return balances;
|
|
1267
1349
|
}
|
|
1268
|
-
function diffInDays2(start, end) {
|
|
1269
|
-
const startDate = new Date(start);
|
|
1270
|
-
const endDate = new Date(end);
|
|
1271
|
-
startDate.setHours(0, 0, 0, 0);
|
|
1272
|
-
endDate.setHours(0, 0, 0, 0);
|
|
1273
|
-
return Math.ceil((endDate.getTime() - startDate.getTime()) / (1e3 * 60 * 60 * 24));
|
|
1274
|
-
}
|
|
1275
1350
|
var DEFAULT_LEAVE_ALLOCATIONS, DEFAULT_CARRY_OVER;
|
|
1276
1351
|
var init_leave = __esm({
|
|
1277
1352
|
"src/utils/leave.ts"() {
|
|
1353
|
+
init_date();
|
|
1354
|
+
init_money();
|
|
1278
1355
|
DEFAULT_LEAVE_ALLOCATIONS = {
|
|
1279
1356
|
annual: 20,
|
|
1280
1357
|
sick: 10,
|
|
@@ -1522,6 +1599,85 @@ var init_employee_identity = __esm({
|
|
|
1522
1599
|
}
|
|
1523
1600
|
});
|
|
1524
1601
|
|
|
1602
|
+
// src/utils/error-helpers.ts
|
|
1603
|
+
function handleTransactionError(error, operationName) {
|
|
1604
|
+
const isUnsupported = isTransactionUnsupportedError(error);
|
|
1605
|
+
const isTxError = isTransactionError(error);
|
|
1606
|
+
const isConnError = isConnectionError(error);
|
|
1607
|
+
const retryable = isTxError && !isUnsupported;
|
|
1608
|
+
const wrappedError = toPayrollError(error);
|
|
1609
|
+
wrappedError.context.operationName = operationName;
|
|
1610
|
+
wrappedError.context.transactionError = true;
|
|
1611
|
+
if (isUnsupported) {
|
|
1612
|
+
wrappedError.context.reason = "transactions_unsupported";
|
|
1613
|
+
} else if (isConnError) {
|
|
1614
|
+
wrappedError.context.reason = "connection_error";
|
|
1615
|
+
} else if (isTxError) {
|
|
1616
|
+
wrappedError.context.reason = "transaction_conflict";
|
|
1617
|
+
}
|
|
1618
|
+
return {
|
|
1619
|
+
isTransactionError: isTxError || isUnsupported,
|
|
1620
|
+
isUnsupported,
|
|
1621
|
+
retryable,
|
|
1622
|
+
error: wrappedError,
|
|
1623
|
+
originalError: error
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
function handleDuplicateKeyError(error, context) {
|
|
1627
|
+
if (!isDuplicateKeyError(error)) {
|
|
1628
|
+
return {
|
|
1629
|
+
isDuplicate: false,
|
|
1630
|
+
field: "",
|
|
1631
|
+
error: toPayrollError(error)
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
const field = parseDuplicateKeyError(error);
|
|
1635
|
+
const wrappedError = context?.employeeId && context?.month && context?.year ? new DuplicatePayrollError(context.employeeId, context.month, context.year, void 0, { duplicateField: field }) : toPayrollError(error);
|
|
1636
|
+
return {
|
|
1637
|
+
isDuplicate: true,
|
|
1638
|
+
field,
|
|
1639
|
+
error: wrappedError
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
function handlePayrollError(error, operationName) {
|
|
1643
|
+
const info = extractErrorInfo(error);
|
|
1644
|
+
const retryable = isConnectionError(error) || isTransactionError(error) && !isTransactionUnsupportedError(error);
|
|
1645
|
+
const operational = error instanceof PayrollError ? error.isOperational() : false;
|
|
1646
|
+
return {
|
|
1647
|
+
code: info.code,
|
|
1648
|
+
status: info.status,
|
|
1649
|
+
message: info.message,
|
|
1650
|
+
context: {
|
|
1651
|
+
...info.context,
|
|
1652
|
+
operationName
|
|
1653
|
+
},
|
|
1654
|
+
operational,
|
|
1655
|
+
retryable
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
function formatUserError(error) {
|
|
1659
|
+
if (error instanceof PayrollError) {
|
|
1660
|
+
return error.message;
|
|
1661
|
+
}
|
|
1662
|
+
if (isDuplicateKeyError(error)) {
|
|
1663
|
+
const field = parseDuplicateKeyError(error);
|
|
1664
|
+
return `A record with this ${field} already exists`;
|
|
1665
|
+
}
|
|
1666
|
+
if (isTransactionUnsupportedError(error)) {
|
|
1667
|
+
return "Database does not support transactions. Please use a replica set.";
|
|
1668
|
+
}
|
|
1669
|
+
if (isConnectionError(error)) {
|
|
1670
|
+
return "Database connection error. Please try again.";
|
|
1671
|
+
}
|
|
1672
|
+
return "An unexpected error occurred";
|
|
1673
|
+
}
|
|
1674
|
+
var init_error_helpers = __esm({
|
|
1675
|
+
"src/utils/error-helpers.ts"() {
|
|
1676
|
+
init_errors();
|
|
1677
|
+
init_type_guards();
|
|
1678
|
+
}
|
|
1679
|
+
});
|
|
1680
|
+
|
|
1525
1681
|
// src/utils/index.ts
|
|
1526
1682
|
var init_utils = __esm({
|
|
1527
1683
|
"src/utils/index.ts"() {
|
|
@@ -1562,13 +1718,18 @@ var init_tax_withholding_service = __esm({
|
|
|
1562
1718
|
/**
|
|
1563
1719
|
* Create tax withholding records from payroll breakdown
|
|
1564
1720
|
*
|
|
1565
|
-
* Extracts tax deductions from the breakdown and creates
|
|
1566
|
-
* TaxWithholding records
|
|
1721
|
+
* Extracts tax deductions from the breakdown and creates all
|
|
1722
|
+
* TaxWithholding records in a single batch operation (one DB roundtrip).
|
|
1723
|
+
* Events are emitted for each created withholding after the batch insert.
|
|
1724
|
+
*
|
|
1725
|
+
* @param params - Breakdown data, employee info, and session
|
|
1726
|
+
* @returns Array of created TaxWithholdingDocuments (empty if no tax deductions)
|
|
1567
1727
|
*/
|
|
1568
1728
|
async createFromBreakdown(params) {
|
|
1569
1729
|
const {
|
|
1570
1730
|
organizationId,
|
|
1571
1731
|
employeeId,
|
|
1732
|
+
employeeBusinessId,
|
|
1572
1733
|
userId,
|
|
1573
1734
|
payrollRecordId,
|
|
1574
1735
|
transactionId,
|
|
@@ -1616,8 +1777,7 @@ var init_tax_withholding_service = __esm({
|
|
|
1616
1777
|
},
|
|
1617
1778
|
employee: {
|
|
1618
1779
|
id: employeeId,
|
|
1619
|
-
employeeId:
|
|
1620
|
-
// Will be filled by caller if needed
|
|
1780
|
+
employeeId: employeeBusinessId || employeeId.toString()
|
|
1621
1781
|
},
|
|
1622
1782
|
payrollRecord: {
|
|
1623
1783
|
id: payrollRecordId
|
|
@@ -1698,8 +1858,15 @@ var init_tax_withholding_service = __esm({
|
|
|
1698
1858
|
/**
|
|
1699
1859
|
* Mark tax withholdings as paid
|
|
1700
1860
|
*
|
|
1701
|
-
* Updates status
|
|
1702
|
-
* and emits tax:paid event
|
|
1861
|
+
* Updates status using bulkWrite (single DB roundtrip), optionally creates
|
|
1862
|
+
* a government payment transaction, and emits tax:paid event.
|
|
1863
|
+
*
|
|
1864
|
+
* State transitions are pre-validated in memory before the batch update
|
|
1865
|
+
* to ensure all withholdings can transition to 'paid' status.
|
|
1866
|
+
*
|
|
1867
|
+
* @param params - Mark paid parameters (IDs, reference number, notes)
|
|
1868
|
+
* @returns Updated withholdings and optional government transaction
|
|
1869
|
+
* @throws Error if any withholding cannot transition to 'paid' status
|
|
1703
1870
|
*/
|
|
1704
1871
|
async markPaid(params) {
|
|
1705
1872
|
const {
|
|
@@ -1745,12 +1912,30 @@ var init_tax_withholding_service = __esm({
|
|
|
1745
1912
|
});
|
|
1746
1913
|
}
|
|
1747
1914
|
for (const withholding of withholdings) {
|
|
1748
|
-
withholding.
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1915
|
+
const transition = TaxStatusMachine.validateTransition(withholding.status, TAX_STATUS.PAID);
|
|
1916
|
+
if (!transition.success) {
|
|
1917
|
+
throw new Error(`Cannot mark withholding ${withholding._id} as paid: ${transition.error}`);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
const bulkOps = withholdings.map((w) => ({
|
|
1921
|
+
updateOne: {
|
|
1922
|
+
filter: { _id: w._id },
|
|
1923
|
+
update: {
|
|
1924
|
+
$set: {
|
|
1925
|
+
status: TAX_STATUS.PAID,
|
|
1926
|
+
governmentTransactionId: governmentTransaction?._id,
|
|
1927
|
+
referenceNumber,
|
|
1928
|
+
paidAt
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
}));
|
|
1933
|
+
await this.TaxWithholdingModel.bulkWrite(bulkOps, { session });
|
|
1934
|
+
for (const withholding of withholdings) {
|
|
1935
|
+
withholding.status = TAX_STATUS.PAID;
|
|
1936
|
+
withholding.governmentTransactionId = governmentTransaction?._id;
|
|
1937
|
+
withholding.referenceNumber = referenceNumber;
|
|
1938
|
+
withholding.paidAt = paidAt;
|
|
1754
1939
|
}
|
|
1755
1940
|
if (this.events) {
|
|
1756
1941
|
this.events.emitSync("tax:paid", {
|
|
@@ -1778,7 +1963,7 @@ var init_tax_withholding_service = __esm({
|
|
|
1778
1963
|
});
|
|
1779
1964
|
return {
|
|
1780
1965
|
withholdings,
|
|
1781
|
-
transaction: governmentTransaction
|
|
1966
|
+
transaction: governmentTransaction ?? void 0
|
|
1782
1967
|
};
|
|
1783
1968
|
}
|
|
1784
1969
|
/**
|
|
@@ -2004,13 +2189,13 @@ var HRM_CONFIG = {
|
|
|
2004
2189
|
}
|
|
2005
2190
|
};
|
|
2006
2191
|
var TAX_BRACKETS = {
|
|
2192
|
+
// Bangladesh FY 2024-25 rates (after tax-free threshold)
|
|
2007
2193
|
BDT: [
|
|
2008
|
-
{ min: 0, max:
|
|
2009
|
-
{ min:
|
|
2010
|
-
{ min: 4e5, max:
|
|
2011
|
-
{ min:
|
|
2012
|
-
{ min:
|
|
2013
|
-
{ min: 3e6, max: Infinity, rate: 0.25 }
|
|
2194
|
+
{ min: 0, max: 1e5, rate: 0.05 },
|
|
2195
|
+
{ min: 1e5, max: 4e5, rate: 0.1 },
|
|
2196
|
+
{ min: 4e5, max: 7e5, rate: 0.15 },
|
|
2197
|
+
{ min: 7e5, max: 11e5, rate: 0.2 },
|
|
2198
|
+
{ min: 11e5, max: Infinity, rate: 0.25 }
|
|
2014
2199
|
],
|
|
2015
2200
|
USD: [
|
|
2016
2201
|
{ min: 0, max: 1e4, rate: 0.1 },
|
|
@@ -2087,6 +2272,17 @@ function determineOrgRole(employmentData) {
|
|
|
2087
2272
|
}
|
|
2088
2273
|
return ROLE_MAPPING.default;
|
|
2089
2274
|
}
|
|
2275
|
+
function getPayPeriodsPerYear(frequency) {
|
|
2276
|
+
const periodsMap = {
|
|
2277
|
+
monthly: 12,
|
|
2278
|
+
bi_weekly: 26,
|
|
2279
|
+
weekly: 52,
|
|
2280
|
+
daily: 365,
|
|
2281
|
+
hourly: 2080
|
|
2282
|
+
// Assuming 40 hours/week * 52 weeks
|
|
2283
|
+
};
|
|
2284
|
+
return periodsMap[frequency];
|
|
2285
|
+
}
|
|
2090
2286
|
function mergeConfig(customConfig) {
|
|
2091
2287
|
if (!customConfig) return HRM_CONFIG;
|
|
2092
2288
|
return {
|
|
@@ -2288,6 +2484,7 @@ var Container = class {
|
|
|
2288
2484
|
};
|
|
2289
2485
|
|
|
2290
2486
|
// src/core/events.ts
|
|
2487
|
+
init_logger();
|
|
2291
2488
|
var EventBus = class {
|
|
2292
2489
|
handlers = /* @__PURE__ */ new Map();
|
|
2293
2490
|
/**
|
|
@@ -2333,7 +2530,9 @@ var EventBus = class {
|
|
|
2333
2530
|
try {
|
|
2334
2531
|
await handler(payload);
|
|
2335
2532
|
} catch (error) {
|
|
2336
|
-
|
|
2533
|
+
getLogger().error(`Event handler error for ${event}`, {
|
|
2534
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2535
|
+
});
|
|
2337
2536
|
}
|
|
2338
2537
|
})
|
|
2339
2538
|
);
|
|
@@ -2372,6 +2571,7 @@ function createEventBus() {
|
|
|
2372
2571
|
}
|
|
2373
2572
|
|
|
2374
2573
|
// src/core/plugin.ts
|
|
2574
|
+
init_logger();
|
|
2375
2575
|
var PluginManager = class {
|
|
2376
2576
|
constructor(context) {
|
|
2377
2577
|
this.context = context;
|
|
@@ -2440,8 +2640,7 @@ var PluginManager = class {
|
|
|
2440
2640
|
try {
|
|
2441
2641
|
await errorHandler(error, hookName);
|
|
2442
2642
|
} catch (handlerError) {
|
|
2443
|
-
|
|
2444
|
-
getLogger2().debug("Error handler threw an error", {
|
|
2643
|
+
getLogger().debug("Error handler threw an error", {
|
|
2445
2644
|
hook: hookName,
|
|
2446
2645
|
handlerError: handlerError instanceof Error ? handlerError.message : String(handlerError)
|
|
2447
2646
|
});
|
|
@@ -2464,6 +2663,9 @@ var PluginManager = class {
|
|
|
2464
2663
|
return this.plugins.has(name);
|
|
2465
2664
|
}
|
|
2466
2665
|
};
|
|
2666
|
+
function definePlugin(definition) {
|
|
2667
|
+
return definition;
|
|
2668
|
+
}
|
|
2467
2669
|
|
|
2468
2670
|
// src/core/idempotency.ts
|
|
2469
2671
|
init_logger();
|
|
@@ -2544,12 +2746,22 @@ var IdempotencyManager = class _IdempotencyManager {
|
|
|
2544
2746
|
};
|
|
2545
2747
|
}
|
|
2546
2748
|
};
|
|
2547
|
-
function generatePayrollIdempotencyKey(organizationId, employeeId, month, year) {
|
|
2548
|
-
|
|
2749
|
+
function generatePayrollIdempotencyKey(organizationId, employeeId, month, year, payrollRunType = "regular", periodStartDate) {
|
|
2750
|
+
if (periodStartDate) {
|
|
2751
|
+
const startDateStr = periodStartDate.toISOString().split("T")[0];
|
|
2752
|
+
return `payroll:${organizationId}:${employeeId}:${year}-${month}:${startDateStr}:${payrollRunType}`;
|
|
2753
|
+
}
|
|
2754
|
+
return `payroll:${organizationId}:${employeeId}:${year}-${month}:${payrollRunType}`;
|
|
2549
2755
|
}
|
|
2550
2756
|
var WebhookManager = class {
|
|
2551
2757
|
webhooks = [];
|
|
2552
2758
|
deliveryLog = [];
|
|
2759
|
+
maxLogSize;
|
|
2760
|
+
storePayloads;
|
|
2761
|
+
constructor(options) {
|
|
2762
|
+
this.maxLogSize = options?.maxLogSize ?? 1e3;
|
|
2763
|
+
this.storePayloads = options?.storePayloads ?? false;
|
|
2764
|
+
}
|
|
2553
2765
|
/**
|
|
2554
2766
|
* Register a webhook
|
|
2555
2767
|
*/
|
|
@@ -2585,11 +2797,12 @@ var WebhookManager = class {
|
|
|
2585
2797
|
id: deliveryId,
|
|
2586
2798
|
event,
|
|
2587
2799
|
url: webhook.url,
|
|
2588
|
-
payload,
|
|
2800
|
+
payload: this.storePayloads ? payload : void 0,
|
|
2589
2801
|
attempt: 0,
|
|
2590
2802
|
status: "pending"
|
|
2591
2803
|
};
|
|
2592
2804
|
this.deliveryLog.push(delivery);
|
|
2805
|
+
this.pruneLog();
|
|
2593
2806
|
const maxRetries = webhook.retries || 3;
|
|
2594
2807
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
2595
2808
|
delivery.attempt = attempt;
|
|
@@ -2700,6 +2913,15 @@ var WebhookManager = class {
|
|
|
2700
2913
|
const signature = hmac.digest("hex");
|
|
2701
2914
|
return `t=${timestamp},v1=${signature}`;
|
|
2702
2915
|
}
|
|
2916
|
+
/**
|
|
2917
|
+
* Prune delivery log to stay within maxLogSize.
|
|
2918
|
+
* Removes oldest entries first.
|
|
2919
|
+
*/
|
|
2920
|
+
pruneLog() {
|
|
2921
|
+
if (this.deliveryLog.length > this.maxLogSize) {
|
|
2922
|
+
this.deliveryLog = this.deliveryLog.slice(-this.maxLogSize);
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2703
2925
|
/**
|
|
2704
2926
|
* Sleep for ms
|
|
2705
2927
|
*/
|
|
@@ -3130,6 +3352,7 @@ var TransactionFactory = class {
|
|
|
3130
3352
|
// src/payroll.ts
|
|
3131
3353
|
init_query_builders();
|
|
3132
3354
|
init_date();
|
|
3355
|
+
init_money();
|
|
3133
3356
|
init_logger();
|
|
3134
3357
|
init_errors();
|
|
3135
3358
|
|
|
@@ -3457,6 +3680,7 @@ function createEmployeeService(employeeRepo, config) {
|
|
|
3457
3680
|
// src/factories/payroll.factory.ts
|
|
3458
3681
|
init_date();
|
|
3459
3682
|
init_calculation();
|
|
3683
|
+
init_money();
|
|
3460
3684
|
var PayrollFactory = class {
|
|
3461
3685
|
/**
|
|
3462
3686
|
* Create payroll data object
|
|
@@ -3514,7 +3738,7 @@ var PayrollFactory = class {
|
|
|
3514
3738
|
*/
|
|
3515
3739
|
static calculateAllowances(baseAmount, allowances) {
|
|
3516
3740
|
return allowances.map((allowance) => {
|
|
3517
|
-
const amount = allowance.isPercentage && allowance.value !== void 0 ?
|
|
3741
|
+
const amount = allowance.isPercentage && allowance.value !== void 0 ? roundMoney(baseAmount * allowance.value / 100, 2) : allowance.amount;
|
|
3518
3742
|
return {
|
|
3519
3743
|
type: allowance.type,
|
|
3520
3744
|
amount,
|
|
@@ -3527,7 +3751,7 @@ var PayrollFactory = class {
|
|
|
3527
3751
|
*/
|
|
3528
3752
|
static calculateDeductions(baseAmount, deductions) {
|
|
3529
3753
|
return deductions.map((deduction) => {
|
|
3530
|
-
const amount = deduction.isPercentage && deduction.value !== void 0 ?
|
|
3754
|
+
const amount = deduction.isPercentage && deduction.value !== void 0 ? roundMoney(baseAmount * deduction.value / 100, 2) : deduction.amount;
|
|
3531
3755
|
return {
|
|
3532
3756
|
type: deduction.type,
|
|
3533
3757
|
amount,
|
|
@@ -3866,6 +4090,7 @@ function createPayrollService(payrollRepo, employeeService) {
|
|
|
3866
4090
|
|
|
3867
4091
|
// src/factories/compensation.factory.ts
|
|
3868
4092
|
init_calculation();
|
|
4093
|
+
init_money();
|
|
3869
4094
|
var CompensationFactory = class {
|
|
3870
4095
|
/**
|
|
3871
4096
|
* Create compensation object
|
|
@@ -4013,7 +4238,7 @@ var CompensationFactory = class {
|
|
|
4013
4238
|
const newBaseAmount = params.amount ? compensation.baseAmount + params.amount : compensation.baseAmount * (1 + (params.percentage || 0) / 100);
|
|
4014
4239
|
return this.updateBaseAmount(
|
|
4015
4240
|
compensation,
|
|
4016
|
-
|
|
4241
|
+
roundMoney(newBaseAmount, 2),
|
|
4017
4242
|
params.effectiveFrom
|
|
4018
4243
|
);
|
|
4019
4244
|
}
|
|
@@ -4114,6 +4339,7 @@ var CompensationPresets = {
|
|
|
4114
4339
|
// src/services/compensation.service.ts
|
|
4115
4340
|
init_query_builders();
|
|
4116
4341
|
init_logger();
|
|
4342
|
+
init_money();
|
|
4117
4343
|
var CompensationService = class {
|
|
4118
4344
|
constructor(employeeRepo) {
|
|
4119
4345
|
this.employeeRepo = employeeRepo;
|
|
@@ -4370,12 +4596,12 @@ var CompensationService = class {
|
|
|
4370
4596
|
return {
|
|
4371
4597
|
department,
|
|
4372
4598
|
employeeCount: stats.employeeCount,
|
|
4373
|
-
totalBase:
|
|
4374
|
-
totalGross:
|
|
4375
|
-
totalNet:
|
|
4376
|
-
averageBase:
|
|
4377
|
-
averageGross:
|
|
4378
|
-
averageNet:
|
|
4599
|
+
totalBase: roundMoney(stats.totalBase || 0, 2),
|
|
4600
|
+
totalGross: roundMoney(stats.totalGross || 0, 2),
|
|
4601
|
+
totalNet: roundMoney(stats.totalNet || 0, 2),
|
|
4602
|
+
averageBase: roundMoney(stats.avgBase || 0, 2),
|
|
4603
|
+
averageGross: roundMoney(stats.avgGross || 0, 2),
|
|
4604
|
+
averageNet: roundMoney(stats.avgNet || 0, 2)
|
|
4379
4605
|
};
|
|
4380
4606
|
}
|
|
4381
4607
|
/**
|
|
@@ -4440,17 +4666,17 @@ var CompensationService = class {
|
|
|
4440
4666
|
(results[0]?.byDepartment || []).forEach((dept) => {
|
|
4441
4667
|
byDepartment[dept._id] = {
|
|
4442
4668
|
count: dept.count,
|
|
4443
|
-
totalNet:
|
|
4669
|
+
totalNet: roundMoney(dept.totalNet || 0, 2)
|
|
4444
4670
|
};
|
|
4445
4671
|
});
|
|
4446
4672
|
return {
|
|
4447
4673
|
employeeCount: overallStats.employeeCount,
|
|
4448
|
-
totalBase:
|
|
4449
|
-
totalGross:
|
|
4450
|
-
totalNet:
|
|
4451
|
-
averageBase:
|
|
4452
|
-
averageGross:
|
|
4453
|
-
averageNet:
|
|
4674
|
+
totalBase: roundMoney(overallStats.totalBase || 0, 2),
|
|
4675
|
+
totalGross: roundMoney(overallStats.totalGross || 0, 2),
|
|
4676
|
+
totalNet: roundMoney(overallStats.totalNet || 0, 2),
|
|
4677
|
+
averageBase: roundMoney(overallStats.avgBase || 0, 2),
|
|
4678
|
+
averageGross: roundMoney(overallStats.avgGross || 0, 2),
|
|
4679
|
+
averageNet: roundMoney(overallStats.avgNet || 0, 2),
|
|
4454
4680
|
byDepartment
|
|
4455
4681
|
};
|
|
4456
4682
|
}
|
|
@@ -4485,13 +4711,14 @@ init_date();
|
|
|
4485
4711
|
init_prorating_calculator();
|
|
4486
4712
|
|
|
4487
4713
|
// src/calculators/attendance.calculator.ts
|
|
4714
|
+
init_money();
|
|
4488
4715
|
function calculateAttendanceDeduction(input) {
|
|
4489
4716
|
const { expectedWorkingDays, actualWorkingDays, dailyRate } = input;
|
|
4490
4717
|
const expected = Math.max(0, expectedWorkingDays);
|
|
4491
4718
|
const actual = Math.max(0, actualWorkingDays);
|
|
4492
4719
|
const rate = Math.max(0, dailyRate);
|
|
4493
4720
|
const absentDays = Math.max(0, expected - actual);
|
|
4494
|
-
const deductionAmount =
|
|
4721
|
+
const deductionAmount = roundMoney(absentDays * rate, 2);
|
|
4495
4722
|
return {
|
|
4496
4723
|
absentDays,
|
|
4497
4724
|
deductionAmount,
|
|
@@ -4501,20 +4728,20 @@ function calculateAttendanceDeduction(input) {
|
|
|
4501
4728
|
}
|
|
4502
4729
|
function calculateDailyRate(monthlySalary, workingDays) {
|
|
4503
4730
|
if (workingDays <= 0) return 0;
|
|
4504
|
-
return
|
|
4731
|
+
return roundMoney(monthlySalary / workingDays, 2);
|
|
4505
4732
|
}
|
|
4506
4733
|
function calculateHourlyRate(monthlySalary, workingDays, hoursPerDay = 8) {
|
|
4507
4734
|
const dailyRate = calculateDailyRate(monthlySalary, workingDays);
|
|
4508
4735
|
if (hoursPerDay <= 0) return 0;
|
|
4509
|
-
return
|
|
4736
|
+
return roundMoney(dailyRate / hoursPerDay, 2);
|
|
4510
4737
|
}
|
|
4511
4738
|
function calculatePartialDayDeduction(dailyRate, fractionAbsent) {
|
|
4512
4739
|
const fraction = Math.min(1, Math.max(0, fractionAbsent));
|
|
4513
|
-
return
|
|
4740
|
+
return roundMoney(dailyRate * fraction, 2);
|
|
4514
4741
|
}
|
|
4515
4742
|
function calculateTotalAttendanceDeduction(input) {
|
|
4516
4743
|
const { dailyRate, fullDayAbsences = 0, partialDayAbsences = [] } = input;
|
|
4517
|
-
const fullDayDeduction =
|
|
4744
|
+
const fullDayDeduction = roundMoney(dailyRate * Math.max(0, fullDayAbsences), 2);
|
|
4518
4745
|
const partialDayDeduction = partialDayAbsences.reduce(
|
|
4519
4746
|
(sum, fraction) => sum + calculatePartialDayDeduction(dailyRate, fraction),
|
|
4520
4747
|
0
|
|
@@ -4528,7 +4755,7 @@ function calculateTotalAttendanceDeduction(input) {
|
|
|
4528
4755
|
|
|
4529
4756
|
// src/calculators/salary.calculator.ts
|
|
4530
4757
|
function calculateSalaryBreakdown(input) {
|
|
4531
|
-
const { employee: employee2, period, attendance, options = {}, config, taxBrackets } = input;
|
|
4758
|
+
const { employee: employee2, period, attendance, options = {}, config, taxBrackets, taxOptions, jurisdictionTaxConfig } = input;
|
|
4532
4759
|
const comp = employee2.compensation;
|
|
4533
4760
|
const originalBaseAmount = comp.baseAmount;
|
|
4534
4761
|
const proRating = calculateProRatingForSalary(
|
|
@@ -4545,8 +4772,8 @@ function calculateSalaryBreakdown(input) {
|
|
|
4545
4772
|
}
|
|
4546
4773
|
const effectiveAllowances = (comp.allowances || []).filter((a) => isEffectiveForPeriod(a, period.startDate, period.endDate));
|
|
4547
4774
|
const effectiveDeductions = (comp.deductions || []).filter((d) => isEffectiveForPeriod(d, period.startDate, period.endDate)).filter((d) => d.auto || d.recurring);
|
|
4548
|
-
const allowances = processAllowances(effectiveAllowances, originalBaseAmount, proRating, config);
|
|
4549
|
-
const deductions = processDeductions(effectiveDeductions, originalBaseAmount, proRating, config);
|
|
4775
|
+
const allowances = processAllowances(effectiveAllowances, originalBaseAmount, proRating, config, options.skipProration);
|
|
4776
|
+
const deductions = processDeductions(effectiveDeductions, originalBaseAmount, proRating, config, options.skipProration);
|
|
4550
4777
|
if (!options.skipAttendance && config.attendanceIntegration && attendance) {
|
|
4551
4778
|
const attendanceDeductionResult = calculateAttendanceDeductionFromData(
|
|
4552
4779
|
attendance,
|
|
@@ -4563,12 +4790,24 @@ function calculateSalaryBreakdown(input) {
|
|
|
4563
4790
|
}
|
|
4564
4791
|
const grossSalary = calculateGross(baseAmount, allowances);
|
|
4565
4792
|
const taxableAllowances = allowances.filter((a) => a.taxable);
|
|
4566
|
-
|
|
4793
|
+
let taxableAmount = baseAmount + sumAllowances(taxableAllowances);
|
|
4794
|
+
const preTaxDeductionAmount = calculatePreTaxDeductions(
|
|
4795
|
+
effectiveDeductions,
|
|
4796
|
+
deductions,
|
|
4797
|
+
taxOptions,
|
|
4798
|
+
jurisdictionTaxConfig
|
|
4799
|
+
);
|
|
4800
|
+
taxableAmount = Math.max(0, taxableAmount - preTaxDeductionAmount);
|
|
4801
|
+
const frequency = employee2.compensation?.frequency || "monthly";
|
|
4567
4802
|
let taxAmount = 0;
|
|
4568
4803
|
if (!options.skipTax && taxBrackets.length > 0 && config.autoDeductions) {
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4804
|
+
taxAmount = calculateEnhancedTax(
|
|
4805
|
+
taxableAmount,
|
|
4806
|
+
taxBrackets,
|
|
4807
|
+
taxOptions,
|
|
4808
|
+
jurisdictionTaxConfig,
|
|
4809
|
+
frequency
|
|
4810
|
+
);
|
|
4572
4811
|
}
|
|
4573
4812
|
if (taxAmount > 0) {
|
|
4574
4813
|
deductions.push({
|
|
@@ -4604,11 +4843,11 @@ function calculateProRatingForSalary(hireDate, terminationDate, periodStart, per
|
|
|
4604
4843
|
holidays
|
|
4605
4844
|
});
|
|
4606
4845
|
}
|
|
4607
|
-
function processAllowances(allowances, originalBaseAmount, proRating, config) {
|
|
4846
|
+
function processAllowances(allowances, originalBaseAmount, proRating, config, skipProration) {
|
|
4608
4847
|
return allowances.map((a) => {
|
|
4609
4848
|
let amount = a.isPercentage && a.value !== void 0 ? percentageOf(originalBaseAmount, a.value) : a.amount;
|
|
4610
4849
|
const originalAmount = amount;
|
|
4611
|
-
if (proRating.isProRated && config.allowProRating) {
|
|
4850
|
+
if (proRating.isProRated && config.allowProRating && !skipProration) {
|
|
4612
4851
|
amount = prorateAmount(amount, proRating.ratio);
|
|
4613
4852
|
}
|
|
4614
4853
|
return {
|
|
@@ -4621,11 +4860,11 @@ function processAllowances(allowances, originalBaseAmount, proRating, config) {
|
|
|
4621
4860
|
};
|
|
4622
4861
|
});
|
|
4623
4862
|
}
|
|
4624
|
-
function processDeductions(deductions, originalBaseAmount, proRating, config) {
|
|
4863
|
+
function processDeductions(deductions, originalBaseAmount, proRating, config, skipProration) {
|
|
4625
4864
|
return deductions.map((d) => {
|
|
4626
4865
|
let amount = d.isPercentage && d.value !== void 0 ? percentageOf(originalBaseAmount, d.value) : d.amount;
|
|
4627
4866
|
const originalAmount = amount;
|
|
4628
|
-
if (proRating.isProRated && config.allowProRating) {
|
|
4867
|
+
if (proRating.isProRated && config.allowProRating && !skipProration) {
|
|
4629
4868
|
amount = prorateAmount(amount, proRating.ratio);
|
|
4630
4869
|
}
|
|
4631
4870
|
return {
|
|
@@ -4656,6 +4895,78 @@ function calculateAttendanceDeductionFromData(attendance, baseAmount, effectiveW
|
|
|
4656
4895
|
absentDays: result.absentDays
|
|
4657
4896
|
};
|
|
4658
4897
|
}
|
|
4898
|
+
function calculatePreTaxDeductions(effectiveDeductions, processedDeductions, taxOptions, jurisdictionTaxConfig) {
|
|
4899
|
+
let totalPreTax = 0;
|
|
4900
|
+
for (let i = 0; i < effectiveDeductions.length; i++) {
|
|
4901
|
+
const original = effectiveDeductions[i];
|
|
4902
|
+
const processed = processedDeductions[i];
|
|
4903
|
+
if (original.reducesTaxableIncome) {
|
|
4904
|
+
totalPreTax += processed?.amount || 0;
|
|
4905
|
+
}
|
|
4906
|
+
}
|
|
4907
|
+
if (jurisdictionTaxConfig?.preTaxDeductionTypes?.length) {
|
|
4908
|
+
const preTaxTypes = new Set(jurisdictionTaxConfig.preTaxDeductionTypes);
|
|
4909
|
+
for (let i = 0; i < effectiveDeductions.length; i++) {
|
|
4910
|
+
const original = effectiveDeductions[i];
|
|
4911
|
+
const processed = processedDeductions[i];
|
|
4912
|
+
if (original.reducesTaxableIncome) continue;
|
|
4913
|
+
if (preTaxTypes.has(original.type)) {
|
|
4914
|
+
totalPreTax += processed?.amount || 0;
|
|
4915
|
+
}
|
|
4916
|
+
}
|
|
4917
|
+
}
|
|
4918
|
+
if (taxOptions?.preTaxDeductions?.length) {
|
|
4919
|
+
for (const deduction of taxOptions.preTaxDeductions) {
|
|
4920
|
+
totalPreTax += deduction.amount;
|
|
4921
|
+
}
|
|
4922
|
+
}
|
|
4923
|
+
return roundMoney(totalPreTax);
|
|
4924
|
+
}
|
|
4925
|
+
function calculateEnhancedTax(periodTaxable, taxBrackets, taxOptions, jurisdictionTaxConfig, frequency = "monthly") {
|
|
4926
|
+
const periodsPerYear = getPayPeriodsPerYear(frequency);
|
|
4927
|
+
let annualTaxable = periodTaxable * periodsPerYear;
|
|
4928
|
+
const threshold = getApplicableThreshold(taxOptions, jurisdictionTaxConfig);
|
|
4929
|
+
if (threshold > 0) {
|
|
4930
|
+
annualTaxable = Math.max(0, annualTaxable - threshold);
|
|
4931
|
+
}
|
|
4932
|
+
let annualTax = applyTaxBrackets(annualTaxable, taxBrackets);
|
|
4933
|
+
if (taxOptions?.taxCredits?.length && annualTax > 0) {
|
|
4934
|
+
annualTax = applyTaxCredits(annualTax, taxOptions.taxCredits);
|
|
4935
|
+
}
|
|
4936
|
+
return roundMoney(annualTax / periodsPerYear);
|
|
4937
|
+
}
|
|
4938
|
+
function getApplicableThreshold(taxOptions, jurisdictionTaxConfig) {
|
|
4939
|
+
if (taxOptions?.standardDeductionOverride !== void 0) {
|
|
4940
|
+
return taxOptions.standardDeductionOverride;
|
|
4941
|
+
}
|
|
4942
|
+
if (taxOptions?.taxpayerCategory) {
|
|
4943
|
+
const category = taxOptions.taxpayerCategory;
|
|
4944
|
+
if (taxOptions.thresholdOverrides?.[category] !== void 0) {
|
|
4945
|
+
return taxOptions.thresholdOverrides[category];
|
|
4946
|
+
}
|
|
4947
|
+
if (jurisdictionTaxConfig?.thresholdsByCategory?.[category] !== void 0) {
|
|
4948
|
+
return jurisdictionTaxConfig.thresholdsByCategory[category];
|
|
4949
|
+
}
|
|
4950
|
+
}
|
|
4951
|
+
if (taxOptions?.applyStandardDeduction && jurisdictionTaxConfig?.standardDeduction) {
|
|
4952
|
+
return jurisdictionTaxConfig.standardDeduction;
|
|
4953
|
+
}
|
|
4954
|
+
return 0;
|
|
4955
|
+
}
|
|
4956
|
+
function applyTaxCredits(annualTax, taxCredits) {
|
|
4957
|
+
let remainingTax = annualTax;
|
|
4958
|
+
for (const credit of taxCredits) {
|
|
4959
|
+
if (remainingTax <= 0) break;
|
|
4960
|
+
let creditAmount = credit.amount;
|
|
4961
|
+
if (credit.maxPercent !== void 0 && credit.maxPercent > 0) {
|
|
4962
|
+
const maxCredit = annualTax * credit.maxPercent;
|
|
4963
|
+
creditAmount = Math.min(creditAmount, maxCredit);
|
|
4964
|
+
}
|
|
4965
|
+
creditAmount = Math.min(creditAmount, remainingTax);
|
|
4966
|
+
remainingTax -= creditAmount;
|
|
4967
|
+
}
|
|
4968
|
+
return Math.max(0, remainingTax);
|
|
4969
|
+
}
|
|
4659
4970
|
|
|
4660
4971
|
// src/core/repository-plugins.ts
|
|
4661
4972
|
init_query_builders();
|
|
@@ -4800,26 +5111,9 @@ function getEmployeeName(employee2) {
|
|
|
4800
5111
|
}
|
|
4801
5112
|
return employee2.employeeId;
|
|
4802
5113
|
}
|
|
4803
|
-
function isMongoError(error) {
|
|
4804
|
-
return error instanceof MongoServerError;
|
|
4805
|
-
}
|
|
4806
|
-
function isDuplicateKeyError(error) {
|
|
4807
|
-
return isMongoError(error) && error.code === 11e3;
|
|
4808
|
-
}
|
|
4809
|
-
function parseDuplicateKeyError(error) {
|
|
4810
|
-
const message = error.message || "";
|
|
4811
|
-
const match = message.match(/dup key: \{ ([^:]+):/);
|
|
4812
|
-
if (match && match[1]) {
|
|
4813
|
-
return match[1].trim();
|
|
4814
|
-
}
|
|
4815
|
-
const errmsgMatch = message.match(/index: ([^_]+)_/);
|
|
4816
|
-
if (errmsgMatch && errmsgMatch[1]) {
|
|
4817
|
-
return errmsgMatch[1].trim();
|
|
4818
|
-
}
|
|
4819
|
-
return "unknown";
|
|
4820
|
-
}
|
|
4821
5114
|
|
|
4822
5115
|
// src/managers/salary-processing.manager.ts
|
|
5116
|
+
init_type_guards();
|
|
4823
5117
|
var SalaryProcessingManager = class {
|
|
4824
5118
|
constructor(models, _container, events, idempotency, repositoryManager, calculateSalaryBreakdownFn, resolveOrganizationIdFn, resolveEmployeeIdFn, findEmployeeFn, updatePayrollStatsFn, config) {
|
|
4825
5119
|
this.models = models;
|
|
@@ -4862,32 +5156,65 @@ var SalaryProcessingManager = class {
|
|
|
4862
5156
|
* Process salary for a single employee
|
|
4863
5157
|
*/
|
|
4864
5158
|
async processSalary(params) {
|
|
4865
|
-
const {
|
|
5159
|
+
const {
|
|
5160
|
+
employeeId,
|
|
5161
|
+
employeeIdMode,
|
|
5162
|
+
organizationId: explicitOrgId,
|
|
5163
|
+
month,
|
|
5164
|
+
year,
|
|
5165
|
+
paymentDate = /* @__PURE__ */ new Date(),
|
|
5166
|
+
paymentMethod = "bank",
|
|
5167
|
+
attendance,
|
|
5168
|
+
options,
|
|
5169
|
+
context,
|
|
5170
|
+
idempotencyKey,
|
|
5171
|
+
payrollRunType = "regular",
|
|
5172
|
+
retroactiveAdjustment,
|
|
5173
|
+
employerContributions
|
|
5174
|
+
} = params;
|
|
4866
5175
|
const orgId = this.resolveOrganizationIdFn(explicitOrgId || context?.organizationId);
|
|
5176
|
+
const repos = this.repositoryManager.getReposForRequest(orgId);
|
|
4867
5177
|
const resolvedEmployeeId = await this.resolveEmployeeIdFn(employeeId, employeeIdMode, orgId, context?.session);
|
|
4868
|
-
const
|
|
5178
|
+
const employee2 = await this.findEmployeeFn({
|
|
5179
|
+
employeeId,
|
|
5180
|
+
employeeIdMode,
|
|
5181
|
+
organizationId: orgId,
|
|
5182
|
+
session: context?.session ?? void 0,
|
|
5183
|
+
populate: "userId"
|
|
5184
|
+
});
|
|
5185
|
+
const employeeFrequency = employee2.compensation?.frequency || "monthly";
|
|
5186
|
+
const frequencyPeriod = getPayPeriodForFrequency(employeeFrequency, paymentDate, month, year);
|
|
5187
|
+
const period = { ...frequencyPeriod, payDate: paymentDate };
|
|
5188
|
+
const idempotentKey = idempotencyKey || generatePayrollIdempotencyKey(
|
|
5189
|
+
orgId,
|
|
5190
|
+
resolvedEmployeeId,
|
|
5191
|
+
month,
|
|
5192
|
+
year,
|
|
5193
|
+
payrollRunType,
|
|
5194
|
+
employeeFrequency !== "monthly" ? period.startDate : void 0
|
|
5195
|
+
);
|
|
4869
5196
|
const cached = this.idempotency.get(idempotentKey);
|
|
4870
5197
|
if (cached) {
|
|
4871
5198
|
getLogger().info("Returning cached payroll result (idempotent)", {
|
|
4872
5199
|
idempotencyKey: idempotentKey,
|
|
4873
|
-
cachedAt: cached.createdAt
|
|
5200
|
+
cachedAt: cached.createdAt,
|
|
5201
|
+
frequency: employeeFrequency
|
|
4874
5202
|
});
|
|
4875
5203
|
return cached.value;
|
|
4876
5204
|
}
|
|
4877
5205
|
const providedSession = context?.session;
|
|
4878
|
-
const repos = this.repositoryManager.getReposForRequest(orgId);
|
|
4879
5206
|
if (providedSession) {
|
|
4880
|
-
return this._processSalaryWithSession(providedSession, repos, params, resolvedEmployeeId, orgId, idempotentKey);
|
|
5207
|
+
return this._processSalaryWithSession(providedSession, repos, params, resolvedEmployeeId, orgId, idempotentKey, employee2, period);
|
|
4881
5208
|
}
|
|
4882
5209
|
try {
|
|
4883
5210
|
return await repos.employee.withTransaction(
|
|
4884
|
-
async (session) => this._processSalaryWithSession(session, repos, params, resolvedEmployeeId, orgId, idempotentKey),
|
|
5211
|
+
async (session) => this._processSalaryWithSession(session, repos, params, resolvedEmployeeId, orgId, idempotentKey, employee2, period),
|
|
4885
5212
|
{ allowFallback: true }
|
|
4886
5213
|
// Fallback to non-transactional for standalone MongoDB
|
|
4887
5214
|
);
|
|
4888
5215
|
} catch (error) {
|
|
4889
5216
|
if (isDuplicateKeyError(error)) {
|
|
4890
|
-
return this._handleDuplicateKeyError(error, resolvedEmployeeId, orgId, params.month, params.year, idempotentKey);
|
|
5217
|
+
return this._handleDuplicateKeyError(error, resolvedEmployeeId, orgId, params.month, params.year, payrollRunType, idempotentKey, period.startDate, employeeFrequency);
|
|
4891
5218
|
}
|
|
4892
5219
|
throw error;
|
|
4893
5220
|
}
|
|
@@ -4896,21 +5223,29 @@ var SalaryProcessingManager = class {
|
|
|
4896
5223
|
* Handle duplicate key error by fetching existing payroll record
|
|
4897
5224
|
* MUST be called OUTSIDE transaction to avoid session state corruption
|
|
4898
5225
|
*/
|
|
4899
|
-
async _handleDuplicateKeyError(error, resolvedEmployeeId, orgId, month, year, idempotentKey) {
|
|
5226
|
+
async _handleDuplicateKeyError(error, resolvedEmployeeId, orgId, month, year, payrollRunType, idempotentKey, periodStartDate, employeeFrequency) {
|
|
4900
5227
|
const duplicateField = parseDuplicateKeyError(error);
|
|
4901
|
-
getLogger().warn("Duplicate payroll record detected, fetching existing", {
|
|
5228
|
+
getLogger().warn("Duplicate payroll record detected (E11000), fetching existing", {
|
|
4902
5229
|
employeeId: resolvedEmployeeId.toString(),
|
|
4903
5230
|
month,
|
|
4904
5231
|
year,
|
|
5232
|
+
payrollRunType,
|
|
4905
5233
|
idempotencyKey: idempotentKey,
|
|
4906
|
-
duplicateField
|
|
5234
|
+
duplicateField,
|
|
5235
|
+
periodStartDate: periodStartDate.toISOString(),
|
|
5236
|
+
frequency: employeeFrequency
|
|
4907
5237
|
});
|
|
4908
|
-
const
|
|
5238
|
+
const query = {
|
|
4909
5239
|
organizationId: orgId,
|
|
4910
5240
|
employeeId: resolvedEmployeeId,
|
|
4911
5241
|
"period.month": month,
|
|
4912
|
-
"period.year": year
|
|
4913
|
-
|
|
5242
|
+
"period.year": year,
|
|
5243
|
+
payrollRunType
|
|
5244
|
+
};
|
|
5245
|
+
if (employeeFrequency !== "monthly") {
|
|
5246
|
+
query["period.startDate"] = periodStartDate;
|
|
5247
|
+
}
|
|
5248
|
+
const existingPayroll = await this.models.PayrollRecordModel.findOne(query).populate("transactionId");
|
|
4914
5249
|
if (existingPayroll && existingPayroll.transactionId) {
|
|
4915
5250
|
const existingEmployee = await this.models.EmployeeModel.findOne({
|
|
4916
5251
|
_id: resolvedEmployeeId,
|
|
@@ -4939,43 +5274,111 @@ var SalaryProcessingManager = class {
|
|
|
4939
5274
|
this.idempotency.set(idempotentKey, result);
|
|
4940
5275
|
return result;
|
|
4941
5276
|
}
|
|
5277
|
+
if (existingPayroll && !existingPayroll.transactionId) {
|
|
5278
|
+
const status = existingPayroll.status;
|
|
5279
|
+
if (status === "failed") {
|
|
5280
|
+
throw new PayrollError(
|
|
5281
|
+
`Previous payroll attempt failed for employee in ${month}/${year}. Retry to automatically clean up and reprocess.`,
|
|
5282
|
+
"DUPLICATE_PAYROLL",
|
|
5283
|
+
409,
|
|
5284
|
+
{
|
|
5285
|
+
existingRecordId: existingPayroll._id.toString(),
|
|
5286
|
+
status,
|
|
5287
|
+
reason: "orphaned_failed_record",
|
|
5288
|
+
retryable: true
|
|
5289
|
+
}
|
|
5290
|
+
);
|
|
5291
|
+
}
|
|
5292
|
+
if (status === "processing" || status === "pending") {
|
|
5293
|
+
try {
|
|
5294
|
+
await this.models.PayrollRecordModel.updateOne(
|
|
5295
|
+
{ _id: existingPayroll._id },
|
|
5296
|
+
{ $set: { status: "failed" } }
|
|
5297
|
+
);
|
|
5298
|
+
getLogger().warn("Marked orphaned payroll record as failed", {
|
|
5299
|
+
payrollRecordId: existingPayroll._id.toString(),
|
|
5300
|
+
previousStatus: status
|
|
5301
|
+
});
|
|
5302
|
+
} catch {
|
|
5303
|
+
}
|
|
5304
|
+
throw new PayrollError(
|
|
5305
|
+
`Previous payroll attempt was incomplete for employee in ${month}/${year}. The record has been marked as failed - retry to reprocess.`,
|
|
5306
|
+
"DUPLICATE_PAYROLL",
|
|
5307
|
+
409,
|
|
5308
|
+
{
|
|
5309
|
+
existingRecordId: existingPayroll._id.toString(),
|
|
5310
|
+
status: "failed",
|
|
5311
|
+
reason: "orphaned_record_cleaned",
|
|
5312
|
+
retryable: true
|
|
5313
|
+
}
|
|
5314
|
+
);
|
|
5315
|
+
}
|
|
5316
|
+
}
|
|
4942
5317
|
throw error;
|
|
4943
5318
|
}
|
|
4944
5319
|
/**
|
|
4945
5320
|
* Internal: Process salary with a specific session
|
|
4946
5321
|
* Extracted to support both external sessions and withTransaction() pattern
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
5322
|
+
*
|
|
5323
|
+
* @param session - Database session (for transactions)
|
|
5324
|
+
* @param repos - Request-scoped repositories
|
|
5325
|
+
* @param params - Salary processing parameters
|
|
5326
|
+
* @param resolvedEmployeeId - Pre-resolved employee ObjectId
|
|
5327
|
+
* @param orgId - Organization ID
|
|
5328
|
+
* @param idempotentKey - Pre-calculated idempotency key
|
|
5329
|
+
* @param prefetchedEmployee - Pre-fetched employee (to avoid duplicate DB call)
|
|
5330
|
+
* @param prefetchedPeriod - Pre-calculated period (to avoid duplicate calculation)
|
|
5331
|
+
*/
|
|
5332
|
+
async _processSalaryWithSession(session, repos, params, resolvedEmployeeId, orgId, idempotentKey, prefetchedEmployee, prefetchedPeriod) {
|
|
5333
|
+
const {
|
|
5334
|
+
month,
|
|
5335
|
+
year,
|
|
5336
|
+
paymentDate = /* @__PURE__ */ new Date(),
|
|
5337
|
+
paymentMethod = "bank",
|
|
5338
|
+
attendance,
|
|
5339
|
+
options,
|
|
5340
|
+
context,
|
|
5341
|
+
payrollRunType = "regular",
|
|
5342
|
+
retroactiveAdjustment,
|
|
5343
|
+
employerContributions
|
|
5344
|
+
} = params;
|
|
4950
5345
|
const mongooseSession = session ?? void 0;
|
|
5346
|
+
let createdPayrollRecordId = null;
|
|
5347
|
+
let createdTransactionId = null;
|
|
4951
5348
|
try {
|
|
4952
|
-
const employee2 =
|
|
4953
|
-
employeeId,
|
|
4954
|
-
// Supports both ObjectId and string
|
|
4955
|
-
employeeIdMode,
|
|
4956
|
-
// Explicit disambiguation if needed
|
|
4957
|
-
organizationId: orgId,
|
|
4958
|
-
session: mongooseSession,
|
|
4959
|
-
populate: "userId"
|
|
4960
|
-
});
|
|
5349
|
+
const employee2 = prefetchedEmployee;
|
|
4961
5350
|
const canReceive = hasPluginMethod(employee2, "canReceiveSalary") ? employee2.canReceiveSalary() : (employee2.status === "active" || employee2.status === "on_leave") && (employee2.compensation?.baseAmount || 0) > 0;
|
|
4962
5351
|
if (!canReceive) {
|
|
4963
5352
|
throw new NotEligibleError("Employee is not eligible to receive salary");
|
|
4964
5353
|
}
|
|
4965
|
-
const
|
|
5354
|
+
const period = prefetchedPeriod;
|
|
5355
|
+
const employeeFrequency = employee2.compensation?.frequency || "monthly";
|
|
5356
|
+
const existingQuery = {
|
|
5357
|
+
...payroll().forOrganization(orgId).forEmployee(employee2._id).forPeriod(month, year).build(),
|
|
5358
|
+
payrollRunType
|
|
5359
|
+
// Only check for same run type
|
|
5360
|
+
};
|
|
5361
|
+
if (employeeFrequency !== "monthly") {
|
|
5362
|
+
existingQuery["period.startDate"] = period.startDate;
|
|
5363
|
+
}
|
|
4966
5364
|
let existingRecordQuery = this.models.PayrollRecordModel.findOne(existingQuery);
|
|
4967
5365
|
if (mongooseSession) existingRecordQuery = existingRecordQuery.session(mongooseSession);
|
|
4968
5366
|
const existingRecord = await existingRecordQuery;
|
|
4969
5367
|
if (existingRecord) {
|
|
4970
5368
|
if (existingRecord.status === "paid" || existingRecord.status === "processing") {
|
|
4971
|
-
throw new DuplicatePayrollError(employee2.employeeId, month, year);
|
|
5369
|
+
throw new DuplicatePayrollError(employee2.employeeId, month, year, payrollRunType);
|
|
4972
5370
|
}
|
|
4973
5371
|
if (existingRecord.status === "voided") {
|
|
4974
5372
|
throw new PayrollError(
|
|
4975
|
-
`Cannot
|
|
4976
|
-
"
|
|
5373
|
+
`Cannot re-process voided payroll for employee ${employee2.employeeId} in ${month}/${year}. To re-process, first call restorePayroll() to restore the voided record to 'pending' status.`,
|
|
5374
|
+
"VOIDED_PAYROLL_REPROCESS",
|
|
4977
5375
|
409,
|
|
4978
|
-
{
|
|
5376
|
+
{
|
|
5377
|
+
status: existingRecord.status,
|
|
5378
|
+
reason: "voided_requires_restore",
|
|
5379
|
+
suggestedAction: "Call restorePayroll({ payrollRecordId }) first",
|
|
5380
|
+
existingRecordId: existingRecord._id.toString()
|
|
5381
|
+
}
|
|
4979
5382
|
);
|
|
4980
5383
|
}
|
|
4981
5384
|
if (existingRecord.status === "reversed") {
|
|
@@ -4998,29 +5401,29 @@ var SalaryProcessingManager = class {
|
|
|
4998
5401
|
}
|
|
4999
5402
|
);
|
|
5000
5403
|
}
|
|
5001
|
-
if (existingRecord.status === "failed") {
|
|
5002
|
-
getLogger().info("Removing
|
|
5404
|
+
if (existingRecord.status === "failed" || existingRecord.status === "pending") {
|
|
5405
|
+
getLogger().info("Removing incomplete record without transaction for retry", {
|
|
5003
5406
|
recordId: existingRecord._id.toString(),
|
|
5004
5407
|
status: existingRecord.status,
|
|
5005
5408
|
employeeId: employee2.employeeId,
|
|
5006
5409
|
month,
|
|
5007
|
-
year
|
|
5410
|
+
year,
|
|
5411
|
+
payrollRunType
|
|
5008
5412
|
});
|
|
5009
5413
|
await this.cascadeDeletePayrollRecord(existingRecord._id, mongooseSession);
|
|
5010
5414
|
} else {
|
|
5011
5415
|
throw new PayrollError(
|
|
5012
|
-
`Cannot retry ${existingRecord.status} payroll for employee ${employee2.employeeId} in ${month}/${year} -
|
|
5416
|
+
`Cannot retry ${existingRecord.status} payroll for employee ${employee2.employeeId} in ${month}/${year} - unexpected status`,
|
|
5013
5417
|
"DUPLICATE_PAYROLL",
|
|
5014
5418
|
409,
|
|
5015
|
-
{ status: existingRecord.status, reason: "
|
|
5419
|
+
{ status: existingRecord.status, reason: "unexpected_status" }
|
|
5016
5420
|
);
|
|
5017
5421
|
}
|
|
5018
5422
|
}
|
|
5019
5423
|
}
|
|
5020
|
-
const period = { ...getPayPeriod(month, year), payDate: paymentDate };
|
|
5021
5424
|
const breakdown = await this.calculateSalaryBreakdownFn(employee2, period, { attendance, options }, mongooseSession);
|
|
5022
5425
|
const userIdValue = employee2.userId ? typeof employee2.userId === "object" && "_id" in employee2.userId ? employee2.userId._id : employee2.userId : void 0;
|
|
5023
|
-
const
|
|
5426
|
+
const payrollRecord = await repos.payrollRecord.create({
|
|
5024
5427
|
organizationId: orgId,
|
|
5025
5428
|
employeeId: employee2._id,
|
|
5026
5429
|
userId: userIdValue,
|
|
@@ -5029,10 +5432,18 @@ var SalaryProcessingManager = class {
|
|
|
5029
5432
|
status: "processing",
|
|
5030
5433
|
paymentMethod,
|
|
5031
5434
|
processedAt: /* @__PURE__ */ new Date(),
|
|
5032
|
-
processedBy: context?.userId ? toObjectId(context.userId) : void 0
|
|
5033
|
-
|
|
5435
|
+
processedBy: context?.userId ? toObjectId(context.userId) : void 0,
|
|
5436
|
+
// Payroll run type and related fields (v2.8.0+)
|
|
5437
|
+
payrollRunType,
|
|
5438
|
+
// Payment frequency at time of processing (v2.9.0+)
|
|
5439
|
+
// Stored for proper idempotency key reconstruction in void/reverse
|
|
5440
|
+
paymentFrequency: employeeFrequency,
|
|
5441
|
+
retroactiveAdjustment,
|
|
5442
|
+
employerContributions
|
|
5443
|
+
}, mongooseSession ? { session: mongooseSession } : {});
|
|
5444
|
+
createdPayrollRecordId = payrollRecord._id;
|
|
5034
5445
|
const frequency = employee2.compensation.frequency || "monthly";
|
|
5035
|
-
const
|
|
5446
|
+
const transaction = await repos.transaction.create({
|
|
5036
5447
|
organizationId: orgId,
|
|
5037
5448
|
// Classification (shared-types)
|
|
5038
5449
|
type: "salary",
|
|
@@ -5063,7 +5474,7 @@ var SalaryProcessingManager = class {
|
|
|
5063
5474
|
customerId: userIdValue,
|
|
5064
5475
|
// Use normalized value (handles populated docs)
|
|
5065
5476
|
processedBy: context?.userId ? toObjectId(context.userId) : void 0,
|
|
5066
|
-
//
|
|
5477
|
+
// Breakdown structure
|
|
5067
5478
|
breakdown: {
|
|
5068
5479
|
base: breakdown.baseAmount,
|
|
5069
5480
|
additions: breakdown.allowances.map((a) => ({
|
|
@@ -5105,7 +5516,8 @@ var SalaryProcessingManager = class {
|
|
|
5105
5516
|
email: getEmployeeEmail(employee2),
|
|
5106
5517
|
payrollRecordId: payrollRecord._id.toString()
|
|
5107
5518
|
}
|
|
5108
|
-
}
|
|
5519
|
+
}, mongooseSession ? { session: mongooseSession } : {});
|
|
5520
|
+
createdTransactionId = transaction._id;
|
|
5109
5521
|
payrollRecord.transactionId = transaction._id;
|
|
5110
5522
|
payrollRecord.status = "paid";
|
|
5111
5523
|
payrollRecord.paidAt = paymentDate;
|
|
@@ -5120,6 +5532,7 @@ var SalaryProcessingManager = class {
|
|
|
5120
5532
|
await taxService.createFromBreakdown({
|
|
5121
5533
|
organizationId: orgId,
|
|
5122
5534
|
employeeId: employee2._id,
|
|
5535
|
+
employeeBusinessId: employee2.employeeId,
|
|
5123
5536
|
userId: userIdValue,
|
|
5124
5537
|
payrollRecordId: payrollRecord._id,
|
|
5125
5538
|
transactionId: transaction._id,
|
|
@@ -5168,6 +5581,41 @@ var SalaryProcessingManager = class {
|
|
|
5168
5581
|
this.idempotency.set(idempotentKey, result);
|
|
5169
5582
|
return result;
|
|
5170
5583
|
} catch (error) {
|
|
5584
|
+
if (!session && createdPayrollRecordId) {
|
|
5585
|
+
try {
|
|
5586
|
+
const existingRecord = await this.models.PayrollRecordModel.findById(createdPayrollRecordId);
|
|
5587
|
+
if (existingRecord?.status === "paid" && existingRecord?.transactionId) {
|
|
5588
|
+
getLogger().warn("Payroll record already paid with transaction - not marking as failed", {
|
|
5589
|
+
payrollRecordId: createdPayrollRecordId.toString(),
|
|
5590
|
+
transactionId: existingRecord.transactionId.toString(),
|
|
5591
|
+
status: existingRecord.status,
|
|
5592
|
+
error: error.message
|
|
5593
|
+
});
|
|
5594
|
+
} else {
|
|
5595
|
+
const updateFields = { status: "failed" };
|
|
5596
|
+
if (createdTransactionId) {
|
|
5597
|
+
updateFields.transactionId = createdTransactionId;
|
|
5598
|
+
}
|
|
5599
|
+
await repos.payrollRecord.update(
|
|
5600
|
+
createdPayrollRecordId,
|
|
5601
|
+
updateFields,
|
|
5602
|
+
{ throwOnNotFound: false }
|
|
5603
|
+
);
|
|
5604
|
+
getLogger().warn("Marked payroll record as failed after non-transactional error", {
|
|
5605
|
+
payrollRecordId: createdPayrollRecordId.toString(),
|
|
5606
|
+
transactionId: createdTransactionId?.toString(),
|
|
5607
|
+
error: error.message
|
|
5608
|
+
});
|
|
5609
|
+
}
|
|
5610
|
+
} catch (cleanupError) {
|
|
5611
|
+
getLogger().error("Failed to mark payroll record as failed during cleanup", {
|
|
5612
|
+
payrollRecordId: createdPayrollRecordId.toString(),
|
|
5613
|
+
transactionId: createdTransactionId?.toString(),
|
|
5614
|
+
cleanupError: cleanupError.message,
|
|
5615
|
+
originalError: error.message
|
|
5616
|
+
});
|
|
5617
|
+
}
|
|
5618
|
+
}
|
|
5171
5619
|
throw error;
|
|
5172
5620
|
}
|
|
5173
5621
|
}
|
|
@@ -5192,10 +5640,11 @@ function createSalaryProcessingManager(models, container, events, idempotency, r
|
|
|
5192
5640
|
init_logger();
|
|
5193
5641
|
init_query_builders();
|
|
5194
5642
|
var BulkOperationsManager = class {
|
|
5195
|
-
constructor(models, events, processSalaryFn) {
|
|
5643
|
+
constructor(models, events, processSalaryFn, resolveOrganizationIdFn) {
|
|
5196
5644
|
this.models = models;
|
|
5197
5645
|
this.events = events;
|
|
5198
5646
|
this.processSalaryFn = processSalaryFn;
|
|
5647
|
+
this.resolveOrganizationIdFn = resolveOrganizationIdFn;
|
|
5199
5648
|
}
|
|
5200
5649
|
/**
|
|
5201
5650
|
* Process bulk payroll for multiple employees
|
|
@@ -5209,7 +5658,7 @@ var BulkOperationsManager = class {
|
|
|
5209
5658
|
*/
|
|
5210
5659
|
async processBulkPayroll(params) {
|
|
5211
5660
|
const {
|
|
5212
|
-
organizationId,
|
|
5661
|
+
organizationId: explicitOrgId,
|
|
5213
5662
|
month,
|
|
5214
5663
|
year,
|
|
5215
5664
|
employeeIds = [],
|
|
@@ -5223,9 +5672,11 @@ var BulkOperationsManager = class {
|
|
|
5223
5672
|
batchSize = 10,
|
|
5224
5673
|
batchDelay = 0,
|
|
5225
5674
|
concurrency = 1,
|
|
5226
|
-
useStreaming
|
|
5675
|
+
useStreaming,
|
|
5676
|
+
maxResultDetails = Infinity
|
|
5227
5677
|
} = params;
|
|
5228
|
-
const
|
|
5678
|
+
const organizationId = this.resolveOrganizationIdFn(explicitOrgId || context?.organizationId);
|
|
5679
|
+
const query = { organizationId, status: { $in: ["active", "on_leave"] } };
|
|
5229
5680
|
if (employeeIds.length > 0) {
|
|
5230
5681
|
query._id = { $in: employeeIds.map(toObjectId) };
|
|
5231
5682
|
}
|
|
@@ -5246,7 +5697,8 @@ var BulkOperationsManager = class {
|
|
|
5246
5697
|
batchDelay,
|
|
5247
5698
|
concurrency,
|
|
5248
5699
|
onProgress,
|
|
5249
|
-
total: employeeCount
|
|
5700
|
+
total: employeeCount,
|
|
5701
|
+
maxResultDetails
|
|
5250
5702
|
});
|
|
5251
5703
|
}
|
|
5252
5704
|
const employees = await this.models.EmployeeModel.find(query);
|
|
@@ -5254,16 +5706,19 @@ var BulkOperationsManager = class {
|
|
|
5254
5706
|
const results = {
|
|
5255
5707
|
successful: [],
|
|
5256
5708
|
failed: [],
|
|
5257
|
-
total
|
|
5709
|
+
total,
|
|
5710
|
+
successCount: 0,
|
|
5711
|
+
failCount: 0,
|
|
5712
|
+
totalAmount: 0
|
|
5258
5713
|
};
|
|
5259
5714
|
const reportProgress = async (currentEmployee) => {
|
|
5260
5715
|
if (onProgress) {
|
|
5261
|
-
const processed = results.
|
|
5716
|
+
const processed = results.successCount + results.failCount;
|
|
5262
5717
|
await onProgress({
|
|
5263
5718
|
processed,
|
|
5264
5719
|
total,
|
|
5265
|
-
successful: results.
|
|
5266
|
-
failed: results.
|
|
5720
|
+
successful: results.successCount,
|
|
5721
|
+
failed: results.failCount,
|
|
5267
5722
|
currentEmployee,
|
|
5268
5723
|
percentage: total > 0 ? Math.round(processed / total * 100) : 0
|
|
5269
5724
|
});
|
|
@@ -5273,7 +5728,7 @@ var BulkOperationsManager = class {
|
|
|
5273
5728
|
if (signal?.aborted) {
|
|
5274
5729
|
getLogger().warn("Bulk payroll cancelled", {
|
|
5275
5730
|
organizationId: organizationId.toString(),
|
|
5276
|
-
processed: results.
|
|
5731
|
+
processed: results.successCount + results.failCount,
|
|
5277
5732
|
total
|
|
5278
5733
|
});
|
|
5279
5734
|
throw new Error("Payroll processing cancelled by user");
|
|
@@ -5293,16 +5748,24 @@ var BulkOperationsManager = class {
|
|
|
5293
5748
|
options,
|
|
5294
5749
|
context: { ...context, session: void 0 }
|
|
5295
5750
|
});
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5751
|
+
const amount = result.payrollRecord.breakdown.netSalary;
|
|
5752
|
+
results.successCount++;
|
|
5753
|
+
results.totalAmount += amount;
|
|
5754
|
+
if (results.successful.length < maxResultDetails) {
|
|
5755
|
+
results.successful.push({
|
|
5756
|
+
employeeId: employee2.employeeId,
|
|
5757
|
+
amount,
|
|
5758
|
+
transactionId: result.transaction._id
|
|
5759
|
+
});
|
|
5760
|
+
}
|
|
5301
5761
|
} catch (error) {
|
|
5302
|
-
results.
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5762
|
+
results.failCount++;
|
|
5763
|
+
if (results.failed.length < maxResultDetails) {
|
|
5764
|
+
results.failed.push({
|
|
5765
|
+
employeeId: employee2.employeeId,
|
|
5766
|
+
error: error.message
|
|
5767
|
+
});
|
|
5768
|
+
}
|
|
5306
5769
|
getLogger().error("Failed to process salary", {
|
|
5307
5770
|
employeeId: employee2.employeeId,
|
|
5308
5771
|
error: error.message
|
|
@@ -5332,16 +5795,24 @@ var BulkOperationsManager = class {
|
|
|
5332
5795
|
const batchResult = batchResults[j];
|
|
5333
5796
|
const employee2 = batch[j];
|
|
5334
5797
|
if (batchResult.status === "fulfilled") {
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5798
|
+
const amount = batchResult.value.result.payrollRecord.breakdown.netSalary;
|
|
5799
|
+
results.successCount++;
|
|
5800
|
+
results.totalAmount += amount;
|
|
5801
|
+
if (results.successful.length < maxResultDetails) {
|
|
5802
|
+
results.successful.push({
|
|
5803
|
+
employeeId: batchResult.value.employee.employeeId,
|
|
5804
|
+
amount,
|
|
5805
|
+
transactionId: batchResult.value.result.transaction._id
|
|
5806
|
+
});
|
|
5807
|
+
}
|
|
5340
5808
|
} else {
|
|
5341
|
-
results.
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5809
|
+
results.failCount++;
|
|
5810
|
+
if (results.failed.length < maxResultDetails) {
|
|
5811
|
+
results.failed.push({
|
|
5812
|
+
employeeId: employee2.employeeId,
|
|
5813
|
+
error: batchResult.reason.message || "Unknown error"
|
|
5814
|
+
});
|
|
5815
|
+
}
|
|
5345
5816
|
getLogger().error("Failed to process salary (concurrent)", {
|
|
5346
5817
|
employeeId: employee2.employeeId,
|
|
5347
5818
|
error: batchResult.reason.message
|
|
@@ -5359,9 +5830,9 @@ var BulkOperationsManager = class {
|
|
|
5359
5830
|
period: { month, year },
|
|
5360
5831
|
summary: {
|
|
5361
5832
|
total: results.total,
|
|
5362
|
-
successful: results.
|
|
5363
|
-
failed: results.
|
|
5364
|
-
totalAmount: results.
|
|
5833
|
+
successful: results.successCount,
|
|
5834
|
+
failed: results.failCount,
|
|
5835
|
+
totalAmount: results.totalAmount
|
|
5365
5836
|
},
|
|
5366
5837
|
context
|
|
5367
5838
|
});
|
|
@@ -5370,8 +5841,8 @@ var BulkOperationsManager = class {
|
|
|
5370
5841
|
month,
|
|
5371
5842
|
year,
|
|
5372
5843
|
total: results.total,
|
|
5373
|
-
successful: results.
|
|
5374
|
-
failed: results.
|
|
5844
|
+
successful: results.successCount,
|
|
5845
|
+
failed: results.failCount,
|
|
5375
5846
|
concurrency,
|
|
5376
5847
|
batchSize
|
|
5377
5848
|
});
|
|
@@ -5401,13 +5872,17 @@ var BulkOperationsManager = class {
|
|
|
5401
5872
|
batchDelay,
|
|
5402
5873
|
concurrency,
|
|
5403
5874
|
onProgress,
|
|
5404
|
-
total
|
|
5875
|
+
total,
|
|
5876
|
+
maxResultDetails
|
|
5405
5877
|
} = params;
|
|
5406
5878
|
const startTime = Date.now();
|
|
5407
5879
|
const results = {
|
|
5408
5880
|
successful: [],
|
|
5409
5881
|
failed: [],
|
|
5410
|
-
total
|
|
5882
|
+
total,
|
|
5883
|
+
successCount: 0,
|
|
5884
|
+
failCount: 0,
|
|
5885
|
+
totalAmount: 0
|
|
5411
5886
|
};
|
|
5412
5887
|
const cursor = this.models.EmployeeModel.find(query).cursor();
|
|
5413
5888
|
let processed = 0;
|
|
@@ -5418,8 +5893,8 @@ var BulkOperationsManager = class {
|
|
|
5418
5893
|
await onProgress({
|
|
5419
5894
|
processed,
|
|
5420
5895
|
total,
|
|
5421
|
-
successful: results.
|
|
5422
|
-
failed: results.
|
|
5896
|
+
successful: results.successCount,
|
|
5897
|
+
failed: results.failCount,
|
|
5423
5898
|
currentEmployee,
|
|
5424
5899
|
percentage: total > 0 ? Math.round(processed / total * 100) : 0
|
|
5425
5900
|
});
|
|
@@ -5446,16 +5921,24 @@ var BulkOperationsManager = class {
|
|
|
5446
5921
|
options,
|
|
5447
5922
|
context: { ...context, session: void 0 }
|
|
5448
5923
|
});
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5924
|
+
const amount = result.payrollRecord.breakdown.netSalary;
|
|
5925
|
+
results.successCount++;
|
|
5926
|
+
results.totalAmount += amount;
|
|
5927
|
+
if (results.successful.length < maxResultDetails) {
|
|
5928
|
+
results.successful.push({
|
|
5929
|
+
employeeId: employee2.employeeId,
|
|
5930
|
+
amount,
|
|
5931
|
+
transactionId: result.transaction._id
|
|
5932
|
+
});
|
|
5933
|
+
}
|
|
5454
5934
|
} catch (error) {
|
|
5455
|
-
results.
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5935
|
+
results.failCount++;
|
|
5936
|
+
if (results.failed.length < maxResultDetails) {
|
|
5937
|
+
results.failed.push({
|
|
5938
|
+
employeeId: employee2.employeeId,
|
|
5939
|
+
error: error.message
|
|
5940
|
+
});
|
|
5941
|
+
}
|
|
5459
5942
|
getLogger().error("Failed to process salary (streaming)", {
|
|
5460
5943
|
employeeId: employee2.employeeId,
|
|
5461
5944
|
error: error.message
|
|
@@ -5482,17 +5965,17 @@ var BulkOperationsManager = class {
|
|
|
5482
5965
|
period: { month, year },
|
|
5483
5966
|
summary: {
|
|
5484
5967
|
total: results.total,
|
|
5485
|
-
successful: results.
|
|
5486
|
-
failed: results.
|
|
5487
|
-
totalAmount: results.
|
|
5968
|
+
successful: results.successCount,
|
|
5969
|
+
failed: results.failCount,
|
|
5970
|
+
totalAmount: results.totalAmount
|
|
5488
5971
|
},
|
|
5489
5972
|
context
|
|
5490
5973
|
});
|
|
5491
5974
|
const duration = Date.now() - startTime;
|
|
5492
5975
|
getLogger().info("Streaming bulk payroll completed", {
|
|
5493
5976
|
total: results.total,
|
|
5494
|
-
successful: results.
|
|
5495
|
-
failed: results.
|
|
5977
|
+
successful: results.successCount,
|
|
5978
|
+
failed: results.failCount,
|
|
5496
5979
|
duration,
|
|
5497
5980
|
concurrency,
|
|
5498
5981
|
batchSize
|
|
@@ -5500,8 +5983,8 @@ var BulkOperationsManager = class {
|
|
|
5500
5983
|
return results;
|
|
5501
5984
|
}
|
|
5502
5985
|
};
|
|
5503
|
-
function createBulkOperationsManager(models, events, processSalaryFn) {
|
|
5504
|
-
return new BulkOperationsManager(models, events, processSalaryFn);
|
|
5986
|
+
function createBulkOperationsManager(models, events, processSalaryFn, resolveOrganizationIdFn) {
|
|
5987
|
+
return new BulkOperationsManager(models, events, processSalaryFn, resolveOrganizationIdFn);
|
|
5505
5988
|
}
|
|
5506
5989
|
|
|
5507
5990
|
// src/managers/employee-operations.manager.ts
|
|
@@ -5978,9 +6461,6 @@ function createCompensationManager(events, resolveOrganizationIdFn, resolveEmplo
|
|
|
5978
6461
|
getServicesForRequestFn
|
|
5979
6462
|
);
|
|
5980
6463
|
}
|
|
5981
|
-
|
|
5982
|
-
// src/managers/payroll-history.manager.ts
|
|
5983
|
-
init_logger();
|
|
5984
6464
|
init_query_builders();
|
|
5985
6465
|
var PayrollHistoryManager = class {
|
|
5986
6466
|
constructor(models, events, resolveOrganizationIdFn, findEmployeeFn) {
|
|
@@ -6047,8 +6527,9 @@ var PayrollHistoryManager = class {
|
|
|
6047
6527
|
* @returns Aggregated summary statistics
|
|
6048
6528
|
*/
|
|
6049
6529
|
async payrollSummary(params) {
|
|
6050
|
-
const { organizationId, month, year } = params;
|
|
6051
|
-
const
|
|
6530
|
+
const { organizationId: explicitOrgId, month, year } = params;
|
|
6531
|
+
const orgId = this.resolveOrganizationIdFn(explicitOrgId);
|
|
6532
|
+
const query = { organizationId: orgId };
|
|
6052
6533
|
if (month) query["period.month"] = month;
|
|
6053
6534
|
if (year) query["period.year"] = year;
|
|
6054
6535
|
const [summary] = await this.models.PayrollRecordModel.aggregate([
|
|
@@ -6076,43 +6557,6 @@ var PayrollHistoryManager = class {
|
|
|
6076
6557
|
pendingCount: 0
|
|
6077
6558
|
};
|
|
6078
6559
|
}
|
|
6079
|
-
/**
|
|
6080
|
-
* Export payroll data for a date range
|
|
6081
|
-
*
|
|
6082
|
-
* Features:
|
|
6083
|
-
* - Retrieves all payroll records in date range
|
|
6084
|
-
* - Populates employee and transaction details
|
|
6085
|
-
* - Marks records as exported with timestamp
|
|
6086
|
-
* - Emits audit event for compliance
|
|
6087
|
-
*
|
|
6088
|
-
* IMPORTANT: Marks records as exported to prevent duplicate exports
|
|
6089
|
-
*
|
|
6090
|
-
* @param params - Export parameters (organization, date range)
|
|
6091
|
-
* @returns Payroll records with full details
|
|
6092
|
-
*/
|
|
6093
|
-
async exportPayroll(params) {
|
|
6094
|
-
const { organizationId, startDate, endDate } = params;
|
|
6095
|
-
const query = {
|
|
6096
|
-
organizationId: toObjectId(organizationId),
|
|
6097
|
-
"period.payDate": { $gte: startDate, $lte: endDate }
|
|
6098
|
-
};
|
|
6099
|
-
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 });
|
|
6100
|
-
await this.models.PayrollRecordModel.updateMany(query, {
|
|
6101
|
-
exported: true,
|
|
6102
|
-
exportedAt: /* @__PURE__ */ new Date()
|
|
6103
|
-
});
|
|
6104
|
-
this.events.emitSync("payroll:exported", {
|
|
6105
|
-
organizationId: toObjectId(organizationId),
|
|
6106
|
-
dateRange: { start: startDate, end: endDate },
|
|
6107
|
-
recordCount: records.length,
|
|
6108
|
-
format: "json"
|
|
6109
|
-
});
|
|
6110
|
-
getLogger().info("Payroll data exported", {
|
|
6111
|
-
organizationId: organizationId.toString(),
|
|
6112
|
-
count: records.length
|
|
6113
|
-
});
|
|
6114
|
-
return records;
|
|
6115
|
-
}
|
|
6116
6560
|
};
|
|
6117
6561
|
function createPayrollHistoryManager(models, events, resolveOrganizationIdFn, findEmployeeFn) {
|
|
6118
6562
|
return new PayrollHistoryManager(models, events, resolveOrganizationIdFn, findEmployeeFn);
|
|
@@ -6442,6 +6886,20 @@ var PayrollStateManager = class {
|
|
|
6442
6886
|
"Cannot restore a payroll with reversal transaction. The transaction would become orphaned."
|
|
6443
6887
|
);
|
|
6444
6888
|
}
|
|
6889
|
+
const existingActive = await this.models.PayrollRecordModel.findOne({
|
|
6890
|
+
organizationId: toObjectId(organizationId),
|
|
6891
|
+
employeeId: payrollRecord.employeeId,
|
|
6892
|
+
"period.month": payrollRecord.period.month,
|
|
6893
|
+
"period.year": payrollRecord.period.year,
|
|
6894
|
+
payrollRunType: payrollRecord.payrollRunType,
|
|
6895
|
+
isVoided: { $ne: true },
|
|
6896
|
+
_id: { $ne: payrollRecord._id }
|
|
6897
|
+
}).session(session || null);
|
|
6898
|
+
if (existingActive) {
|
|
6899
|
+
throw new ValidationError(
|
|
6900
|
+
`Cannot restore: An active payroll record already exists for employee ${payrollRecord.employeeId} in ${payrollRecord.period.month}/${payrollRecord.period.year} with run type '${payrollRecord.payrollRunType}'. Void or reverse the existing record first.`
|
|
6901
|
+
);
|
|
6902
|
+
}
|
|
6445
6903
|
payrollRecord.status = PAYROLL_STATUS.PENDING;
|
|
6446
6904
|
payrollRecord.isVoided = false;
|
|
6447
6905
|
payrollRecord.notes = `${payrollRecord.notes || ""}
|
|
@@ -6534,6 +6992,7 @@ var Payroll = class _Payroll {
|
|
|
6534
6992
|
singleTenant: singleTenant ?? null,
|
|
6535
6993
|
logger: customLogger
|
|
6536
6994
|
});
|
|
6995
|
+
const containerBase = this._container;
|
|
6537
6996
|
this.repositoryManager = createRepositoryManager(
|
|
6538
6997
|
{
|
|
6539
6998
|
EmployeeModel,
|
|
@@ -6542,7 +7001,7 @@ var Payroll = class _Payroll {
|
|
|
6542
7001
|
LeaveRequestModel: config.LeaveRequestModel ?? null,
|
|
6543
7002
|
TaxWithholdingModel: config.TaxWithholdingModel ?? null
|
|
6544
7003
|
},
|
|
6545
|
-
|
|
7004
|
+
containerBase
|
|
6546
7005
|
);
|
|
6547
7006
|
this.salaryProcessingManager = createSalaryProcessingManager(
|
|
6548
7007
|
{
|
|
@@ -6551,7 +7010,6 @@ var Payroll = class _Payroll {
|
|
|
6551
7010
|
TransactionModel,
|
|
6552
7011
|
AttendanceModel: AttendanceModel ?? null,
|
|
6553
7012
|
LeaveRequestModel: config.LeaveRequestModel ?? null,
|
|
6554
|
-
// Type assertion: The model has custom statics defined in schema but not reflected in config type
|
|
6555
7013
|
TaxWithholdingModel: config.TaxWithholdingModel ?? null
|
|
6556
7014
|
},
|
|
6557
7015
|
this._container,
|
|
@@ -6573,7 +7031,8 @@ var Payroll = class _Payroll {
|
|
|
6573
7031
|
AttendanceModel: AttendanceModel ?? null
|
|
6574
7032
|
},
|
|
6575
7033
|
this._events,
|
|
6576
|
-
this.processSalary.bind(this)
|
|
7034
|
+
this.processSalary.bind(this),
|
|
7035
|
+
this.resolveOrganizationId.bind(this)
|
|
6577
7036
|
);
|
|
6578
7037
|
this.employeeOperationsManager = createEmployeeOperationsManager(
|
|
6579
7038
|
this._events,
|
|
@@ -6582,7 +7041,6 @@ var Payroll = class _Payroll {
|
|
|
6582
7041
|
this.findEmployee.bind(this),
|
|
6583
7042
|
(orgId) => this.repositoryManager.getReposForRequest(orgId),
|
|
6584
7043
|
(repos) => {
|
|
6585
|
-
repos.employee ? repos.employee._organizationId : void 0;
|
|
6586
7044
|
return this.getServicesForRequest(repos);
|
|
6587
7045
|
}
|
|
6588
7046
|
);
|
|
@@ -6593,7 +7051,6 @@ var Payroll = class _Payroll {
|
|
|
6593
7051
|
this.findEmployee.bind(this),
|
|
6594
7052
|
(orgId) => this.repositoryManager.getReposForRequest(orgId),
|
|
6595
7053
|
(repos) => {
|
|
6596
|
-
repos.employee ? repos.employee._organizationId : void 0;
|
|
6597
7054
|
return this.getServicesForRequest(repos);
|
|
6598
7055
|
}
|
|
6599
7056
|
);
|
|
@@ -7032,7 +7489,11 @@ var Payroll = class _Payroll {
|
|
|
7032
7489
|
populateUser,
|
|
7033
7490
|
session
|
|
7034
7491
|
});
|
|
7035
|
-
} catch {
|
|
7492
|
+
} catch (modeError) {
|
|
7493
|
+
if (modeError instanceof EmployeeNotFoundError) {
|
|
7494
|
+
continue;
|
|
7495
|
+
}
|
|
7496
|
+
throw modeError;
|
|
7036
7497
|
}
|
|
7037
7498
|
}
|
|
7038
7499
|
break;
|
|
@@ -7205,13 +7666,6 @@ var Payroll = class _Payroll {
|
|
|
7205
7666
|
this.ensureInitialized();
|
|
7206
7667
|
return this.payrollHistoryManager.payrollSummary(params);
|
|
7207
7668
|
}
|
|
7208
|
-
/**
|
|
7209
|
-
* Export payroll data
|
|
7210
|
-
*/
|
|
7211
|
-
async exportPayroll(params) {
|
|
7212
|
-
this.ensureInitialized();
|
|
7213
|
-
return this.payrollHistoryManager.exportPayroll(params);
|
|
7214
|
-
}
|
|
7215
7669
|
// ========================================
|
|
7216
7670
|
// Void / Reversal Methods (v2.4.0+)
|
|
7217
7671
|
// ========================================
|
|
@@ -7234,12 +7688,15 @@ var Payroll = class _Payroll {
|
|
|
7234
7688
|
async voidPayroll(params) {
|
|
7235
7689
|
this.ensureInitialized();
|
|
7236
7690
|
const result = await this.payrollStateManager.voidPayroll(params);
|
|
7237
|
-
const { employeeId, period, organizationId } = result.payrollRecord;
|
|
7691
|
+
const { employeeId, period, organizationId, payrollRunType = "regular", paymentFrequency = "monthly" } = result.payrollRecord;
|
|
7692
|
+
const isNonMonthly = paymentFrequency !== "monthly";
|
|
7238
7693
|
const idempotencyKey = generatePayrollIdempotencyKey(
|
|
7239
7694
|
organizationId,
|
|
7240
7695
|
employeeId,
|
|
7241
7696
|
period.month,
|
|
7242
|
-
period.year
|
|
7697
|
+
period.year,
|
|
7698
|
+
payrollRunType,
|
|
7699
|
+
isNonMonthly ? period.startDate : void 0
|
|
7243
7700
|
);
|
|
7244
7701
|
this._idempotency.delete(idempotencyKey);
|
|
7245
7702
|
return result;
|
|
@@ -7263,12 +7720,15 @@ var Payroll = class _Payroll {
|
|
|
7263
7720
|
async reversePayroll(params) {
|
|
7264
7721
|
this.ensureInitialized();
|
|
7265
7722
|
const result = await this.payrollStateManager.reversePayroll(params);
|
|
7266
|
-
const { employeeId, period, organizationId } = result.payrollRecord;
|
|
7723
|
+
const { employeeId, period, organizationId, payrollRunType = "regular", paymentFrequency = "monthly" } = result.payrollRecord;
|
|
7724
|
+
const isNonMonthly = paymentFrequency !== "monthly";
|
|
7267
7725
|
const idempotencyKey = generatePayrollIdempotencyKey(
|
|
7268
7726
|
organizationId,
|
|
7269
7727
|
employeeId,
|
|
7270
7728
|
period.month,
|
|
7271
|
-
period.year
|
|
7729
|
+
period.year,
|
|
7730
|
+
payrollRunType,
|
|
7731
|
+
isNonMonthly ? period.startDate : void 0
|
|
7272
7732
|
);
|
|
7273
7733
|
this._idempotency.delete(idempotencyKey);
|
|
7274
7734
|
return result;
|
|
@@ -7400,33 +7860,6 @@ var Payroll = class _Payroll {
|
|
|
7400
7860
|
taxBrackets
|
|
7401
7861
|
});
|
|
7402
7862
|
}
|
|
7403
|
-
/**
|
|
7404
|
-
* Calculate attendance deduction using working days (not calendar days)
|
|
7405
|
-
*/
|
|
7406
|
-
async calculateAttendanceDeduction(employeeId, organizationId, period, dailyRate, expectedWorkingDays, session) {
|
|
7407
|
-
try {
|
|
7408
|
-
if (!this.models.AttendanceModel) return 0;
|
|
7409
|
-
let query = this.models.AttendanceModel.findOne({
|
|
7410
|
-
organizationId,
|
|
7411
|
-
targetId: employeeId,
|
|
7412
|
-
targetModel: "Employee",
|
|
7413
|
-
year: period.year,
|
|
7414
|
-
month: period.month
|
|
7415
|
-
});
|
|
7416
|
-
if (session) query = query.session(session);
|
|
7417
|
-
const attendance = await query;
|
|
7418
|
-
if (!attendance) return 0;
|
|
7419
|
-
const workedDays = attendance.totalWorkDays || 0;
|
|
7420
|
-
const absentDays = Math.max(0, expectedWorkingDays - workedDays);
|
|
7421
|
-
return Math.round(absentDays * dailyRate);
|
|
7422
|
-
} catch (error) {
|
|
7423
|
-
getLogger().warn("Failed to calculate attendance deduction", {
|
|
7424
|
-
employeeId: employeeId.toString(),
|
|
7425
|
-
error: error.message
|
|
7426
|
-
});
|
|
7427
|
-
return 0;
|
|
7428
|
-
}
|
|
7429
|
-
}
|
|
7430
7863
|
async updatePayrollStats(employee2, amount, paymentDate, repos, session) {
|
|
7431
7864
|
if (!employee2.payrollStats) {
|
|
7432
7865
|
employee2.payrollStats = {
|
|
@@ -7438,10 +7871,27 @@ var Payroll = class _Payroll {
|
|
|
7438
7871
|
employee2.payrollStats.totalPaid = (employee2.payrollStats.totalPaid || 0) + amount;
|
|
7439
7872
|
employee2.payrollStats.lastPaymentDate = paymentDate;
|
|
7440
7873
|
employee2.payrollStats.paymentsThisYear = (employee2.payrollStats.paymentsThisYear || 0) + 1;
|
|
7441
|
-
employee2.payrollStats.averageMonthly =
|
|
7442
|
-
employee2.payrollStats.totalPaid / employee2.payrollStats.paymentsThisYear
|
|
7874
|
+
employee2.payrollStats.averageMonthly = roundMoney(
|
|
7875
|
+
employee2.payrollStats.totalPaid / employee2.payrollStats.paymentsThisYear,
|
|
7876
|
+
2
|
|
7443
7877
|
);
|
|
7444
|
-
employee2.
|
|
7878
|
+
const frequency = employee2.compensation?.frequency || "monthly";
|
|
7879
|
+
switch (frequency) {
|
|
7880
|
+
case "hourly":
|
|
7881
|
+
case "daily":
|
|
7882
|
+
employee2.payrollStats.nextPaymentDate = addDays(paymentDate, 1);
|
|
7883
|
+
break;
|
|
7884
|
+
case "weekly":
|
|
7885
|
+
employee2.payrollStats.nextPaymentDate = addDays(paymentDate, 7);
|
|
7886
|
+
break;
|
|
7887
|
+
case "bi_weekly":
|
|
7888
|
+
employee2.payrollStats.nextPaymentDate = addDays(paymentDate, 14);
|
|
7889
|
+
break;
|
|
7890
|
+
case "monthly":
|
|
7891
|
+
default:
|
|
7892
|
+
employee2.payrollStats.nextPaymentDate = addMonths(paymentDate, 1);
|
|
7893
|
+
break;
|
|
7894
|
+
}
|
|
7445
7895
|
await repos.employee.update(
|
|
7446
7896
|
employee2._id,
|
|
7447
7897
|
{ payrollStats: employee2.payrollStats },
|
|
@@ -7449,6 +7899,206 @@ var Payroll = class _Payroll {
|
|
|
7449
7899
|
);
|
|
7450
7900
|
}
|
|
7451
7901
|
// ========================================
|
|
7902
|
+
// Crash Recovery Methods (v2.8.0+)
|
|
7903
|
+
// ========================================
|
|
7904
|
+
/**
|
|
7905
|
+
* Recover stuck payroll records
|
|
7906
|
+
*
|
|
7907
|
+
* Finds payroll records stuck in 'processing' or 'pending' status for longer
|
|
7908
|
+
* than the threshold and handles them appropriately:
|
|
7909
|
+
* - Records WITHOUT transactionId: Marked as 'failed' (safe to retry)
|
|
7910
|
+
* - Records WITH transactionId: Flagged for manual review (orphaned transaction)
|
|
7911
|
+
*
|
|
7912
|
+
* This helps recover from server crashes or partial failures.
|
|
7913
|
+
*
|
|
7914
|
+
* @example
|
|
7915
|
+
* ```typescript
|
|
7916
|
+
* const recovered = await payroll.recoverStuckPayrolls({
|
|
7917
|
+
* organizationId: org._id,
|
|
7918
|
+
* staleThresholdMinutes: 30,
|
|
7919
|
+
* });
|
|
7920
|
+
* console.log(`Recovered ${recovered.markedFailed} records`);
|
|
7921
|
+
* if (recovered.requiresManualReview.length > 0) {
|
|
7922
|
+
* console.warn('Manual review needed:', recovered.requiresManualReview);
|
|
7923
|
+
* }
|
|
7924
|
+
* ```
|
|
7925
|
+
*/
|
|
7926
|
+
async recoverStuckPayrolls(params) {
|
|
7927
|
+
this.ensureInitialized();
|
|
7928
|
+
const orgId = this.resolveOrganizationId(params.organizationId);
|
|
7929
|
+
const threshold = params.staleThresholdMinutes ?? 30;
|
|
7930
|
+
const cutoffTime = new Date(Date.now() - threshold * 60 * 1e3);
|
|
7931
|
+
const dryRun = params.dryRun ?? false;
|
|
7932
|
+
const stuckRecords = await this.models.PayrollRecordModel.find({
|
|
7933
|
+
organizationId: orgId,
|
|
7934
|
+
status: { $in: ["processing", "pending"] },
|
|
7935
|
+
processedAt: { $lt: cutoffTime }
|
|
7936
|
+
});
|
|
7937
|
+
const result = {
|
|
7938
|
+
markedFailed: 0,
|
|
7939
|
+
requiresManualReview: [],
|
|
7940
|
+
scanned: stuckRecords.length
|
|
7941
|
+
};
|
|
7942
|
+
for (const record of stuckRecords) {
|
|
7943
|
+
if (record.transactionId) {
|
|
7944
|
+
result.requiresManualReview.push({
|
|
7945
|
+
_id: record._id,
|
|
7946
|
+
status: record.status,
|
|
7947
|
+
transactionId: record.transactionId
|
|
7948
|
+
});
|
|
7949
|
+
getLogger().warn("Stuck payroll record with transaction needs manual review", {
|
|
7950
|
+
payrollRecordId: record._id.toString(),
|
|
7951
|
+
status: record.status,
|
|
7952
|
+
transactionId: record.transactionId.toString(),
|
|
7953
|
+
processedAt: record.processedAt
|
|
7954
|
+
});
|
|
7955
|
+
} else {
|
|
7956
|
+
if (!dryRun) {
|
|
7957
|
+
await this.models.PayrollRecordModel.updateOne(
|
|
7958
|
+
{ _id: record._id },
|
|
7959
|
+
{ $set: { status: "failed" } }
|
|
7960
|
+
);
|
|
7961
|
+
}
|
|
7962
|
+
result.markedFailed++;
|
|
7963
|
+
getLogger().info("Marked stuck payroll record as failed", {
|
|
7964
|
+
payrollRecordId: record._id.toString(),
|
|
7965
|
+
status: record.status,
|
|
7966
|
+
processedAt: record.processedAt,
|
|
7967
|
+
dryRun
|
|
7968
|
+
});
|
|
7969
|
+
}
|
|
7970
|
+
}
|
|
7971
|
+
getLogger().info("Crash recovery completed", {
|
|
7972
|
+
organizationId: orgId.toString(),
|
|
7973
|
+
scanned: result.scanned,
|
|
7974
|
+
markedFailed: result.markedFailed,
|
|
7975
|
+
requiresManualReview: result.requiresManualReview.length,
|
|
7976
|
+
dryRun
|
|
7977
|
+
});
|
|
7978
|
+
return result;
|
|
7979
|
+
}
|
|
7980
|
+
// ========================================
|
|
7981
|
+
// Two-Phase Export Methods (v2.8.0+)
|
|
7982
|
+
// ========================================
|
|
7983
|
+
/**
|
|
7984
|
+
* Prepare payroll data for export (Phase 1)
|
|
7985
|
+
*
|
|
7986
|
+
* Retrieves records but does NOT mark them as exported yet.
|
|
7987
|
+
* Returns an exportId that must be used to confirm or cancel the export.
|
|
7988
|
+
*
|
|
7989
|
+
* @example
|
|
7990
|
+
* ```typescript
|
|
7991
|
+
* // Phase 1: Prepare
|
|
7992
|
+
* const { records, exportId } = await payroll.prepareExport({
|
|
7993
|
+
* organizationId: org._id,
|
|
7994
|
+
* startDate: new Date('2024-01-01'),
|
|
7995
|
+
* endDate: new Date('2024-01-31'),
|
|
7996
|
+
* });
|
|
7997
|
+
*
|
|
7998
|
+
* // Send records to external system...
|
|
7999
|
+
*
|
|
8000
|
+
* // Phase 2: Confirm (if successful) or Cancel (if failed)
|
|
8001
|
+
* await payroll.confirmExport({ organizationId: org._id, exportId });
|
|
8002
|
+
* ```
|
|
8003
|
+
*/
|
|
8004
|
+
async prepareExport(params) {
|
|
8005
|
+
this.ensureInitialized();
|
|
8006
|
+
const orgId = this.resolveOrganizationId(params.organizationId);
|
|
8007
|
+
const { startDate, endDate } = params;
|
|
8008
|
+
const query = {
|
|
8009
|
+
organizationId: toObjectId(orgId),
|
|
8010
|
+
"period.payDate": { $gte: startDate, $lte: endDate }
|
|
8011
|
+
};
|
|
8012
|
+
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 });
|
|
8013
|
+
const exportId = `export-${orgId}-${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
|
8014
|
+
this._idempotency.set(`pending-export:${exportId}`, {
|
|
8015
|
+
organizationId: orgId.toString(),
|
|
8016
|
+
recordIds: records.map((r) => r._id.toString()),
|
|
8017
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
8018
|
+
startDate,
|
|
8019
|
+
endDate
|
|
8020
|
+
});
|
|
8021
|
+
getLogger().info("Prepared payroll export", {
|
|
8022
|
+
organizationId: orgId.toString(),
|
|
8023
|
+
exportId,
|
|
8024
|
+
recordCount: records.length,
|
|
8025
|
+
dateRange: { start: startDate, end: endDate }
|
|
8026
|
+
});
|
|
8027
|
+
return {
|
|
8028
|
+
records,
|
|
8029
|
+
exportId,
|
|
8030
|
+
total: records.length
|
|
8031
|
+
};
|
|
8032
|
+
}
|
|
8033
|
+
/**
|
|
8034
|
+
* Confirm export success (Phase 2a)
|
|
8035
|
+
*
|
|
8036
|
+
* Marks records as exported after downstream system confirms receipt.
|
|
8037
|
+
*/
|
|
8038
|
+
async confirmExport(params) {
|
|
8039
|
+
this.ensureInitialized();
|
|
8040
|
+
const orgId = this.resolveOrganizationId(params.organizationId);
|
|
8041
|
+
const { exportId } = params;
|
|
8042
|
+
const pendingExport = this._idempotency.get(`pending-export:${exportId}`);
|
|
8043
|
+
if (!pendingExport) {
|
|
8044
|
+
throw new PayrollError(
|
|
8045
|
+
`Export ${exportId} not found or already processed`,
|
|
8046
|
+
"EXPORT_NOT_FOUND",
|
|
8047
|
+
404,
|
|
8048
|
+
{ exportId }
|
|
8049
|
+
);
|
|
8050
|
+
}
|
|
8051
|
+
if (pendingExport.value.organizationId !== orgId.toString()) {
|
|
8052
|
+
throw new PayrollError(
|
|
8053
|
+
"Organization mismatch for export confirmation",
|
|
8054
|
+
"EXPORT_ORG_MISMATCH",
|
|
8055
|
+
403,
|
|
8056
|
+
{ exportId, expectedOrg: pendingExport.value.organizationId }
|
|
8057
|
+
);
|
|
8058
|
+
}
|
|
8059
|
+
const recordIds = pendingExport.value.recordIds.map((id) => toObjectId(id));
|
|
8060
|
+
const result = await this.models.PayrollRecordModel.updateMany(
|
|
8061
|
+
{ _id: { $in: recordIds }, organizationId: orgId },
|
|
8062
|
+
{ $set: { exported: true, exportedAt: /* @__PURE__ */ new Date() } }
|
|
8063
|
+
);
|
|
8064
|
+
this._idempotency.delete(`pending-export:${exportId}`);
|
|
8065
|
+
this._events.emitSync("payroll:exported", {
|
|
8066
|
+
organizationId: orgId,
|
|
8067
|
+
exportId,
|
|
8068
|
+
recordCount: result.modifiedCount,
|
|
8069
|
+
format: "json"
|
|
8070
|
+
});
|
|
8071
|
+
getLogger().info("Confirmed payroll export", {
|
|
8072
|
+
organizationId: orgId.toString(),
|
|
8073
|
+
exportId,
|
|
8074
|
+
confirmed: result.modifiedCount
|
|
8075
|
+
});
|
|
8076
|
+
return { confirmed: result.modifiedCount ?? 0 };
|
|
8077
|
+
}
|
|
8078
|
+
/**
|
|
8079
|
+
* Cancel export (Phase 2b)
|
|
8080
|
+
*
|
|
8081
|
+
* Called when downstream system fails to process the export.
|
|
8082
|
+
* Records remain unmarked and can be exported again.
|
|
8083
|
+
*/
|
|
8084
|
+
async cancelExport(params) {
|
|
8085
|
+
this.ensureInitialized();
|
|
8086
|
+
const orgId = this.resolveOrganizationId(params.organizationId);
|
|
8087
|
+
const { exportId, reason } = params;
|
|
8088
|
+
const pendingExport = this._idempotency.get(`pending-export:${exportId}`);
|
|
8089
|
+
if (!pendingExport) {
|
|
8090
|
+
getLogger().warn("Export cancellation for unknown export", { exportId });
|
|
8091
|
+
return { cancelled: false };
|
|
8092
|
+
}
|
|
8093
|
+
this._idempotency.delete(`pending-export:${exportId}`);
|
|
8094
|
+
getLogger().info("Cancelled payroll export", {
|
|
8095
|
+
organizationId: orgId.toString(),
|
|
8096
|
+
exportId,
|
|
8097
|
+
reason
|
|
8098
|
+
});
|
|
8099
|
+
return { cancelled: true };
|
|
8100
|
+
}
|
|
8101
|
+
// ========================================
|
|
7452
8102
|
// Static Factory
|
|
7453
8103
|
// ========================================
|
|
7454
8104
|
/**
|
|
@@ -7986,7 +8636,7 @@ leaveRequestSchema.statics.hasOverlap = async function(employeeId, startDate, en
|
|
|
7986
8636
|
const count = await this.countDocuments(query);
|
|
7987
8637
|
return count > 0;
|
|
7988
8638
|
};
|
|
7989
|
-
function getLeaveRequestModel(connection =
|
|
8639
|
+
function getLeaveRequestModel(connection = mongoose5.connection) {
|
|
7990
8640
|
const modelName = "LeaveRequest";
|
|
7991
8641
|
if (connection.models[modelName]) {
|
|
7992
8642
|
return connection.models[modelName];
|
|
@@ -8384,7 +9034,7 @@ taxWithholdingSchema.statics.removeTTLIndex = async function(fieldName) {
|
|
|
8384
9034
|
throw error;
|
|
8385
9035
|
}
|
|
8386
9036
|
};
|
|
8387
|
-
function getTaxWithholdingModel(connection =
|
|
9037
|
+
function getTaxWithholdingModel(connection = mongoose5.connection) {
|
|
8388
9038
|
const modelName = "TaxWithholding";
|
|
8389
9039
|
if (connection.models[modelName]) {
|
|
8390
9040
|
return connection.models[modelName];
|
|
@@ -8643,7 +9293,63 @@ function createPayrollRecordFields(options = {}) {
|
|
|
8643
9293
|
reversalTransactionId: { type: Schema.Types.ObjectId },
|
|
8644
9294
|
originalPayrollId: { type: Schema.Types.ObjectId },
|
|
8645
9295
|
// TTL expiration (per-document)
|
|
8646
|
-
expireAt: { type: Date }
|
|
9296
|
+
expireAt: { type: Date },
|
|
9297
|
+
// Payroll run type (v2.8.0+)
|
|
9298
|
+
payrollRunType: {
|
|
9299
|
+
type: String,
|
|
9300
|
+
enum: ["regular", "off-cycle", "supplemental", "retroactive"],
|
|
9301
|
+
default: "regular"
|
|
9302
|
+
},
|
|
9303
|
+
// Payment frequency at time of processing (v2.9.0+)
|
|
9304
|
+
// Stored for proper idempotency key reconstruction in void/reverse operations
|
|
9305
|
+
paymentFrequency: {
|
|
9306
|
+
type: String,
|
|
9307
|
+
enum: PAYMENT_FREQUENCY_VALUES,
|
|
9308
|
+
default: "monthly"
|
|
9309
|
+
},
|
|
9310
|
+
// Retroactive adjustment details (v2.8.0+)
|
|
9311
|
+
retroactiveAdjustment: {
|
|
9312
|
+
type: new Schema(
|
|
9313
|
+
{
|
|
9314
|
+
originalPeriod: {
|
|
9315
|
+
month: { type: Number, required: true, min: 1, max: 12 },
|
|
9316
|
+
year: { type: Number, required: true }
|
|
9317
|
+
},
|
|
9318
|
+
originalPayrollId: { type: Schema.Types.ObjectId },
|
|
9319
|
+
reason: { type: String, required: true },
|
|
9320
|
+
adjustmentAmount: { type: Number, required: true },
|
|
9321
|
+
approved: { type: Boolean },
|
|
9322
|
+
approvedBy: { type: Schema.Types.ObjectId },
|
|
9323
|
+
approvedAt: { type: Date }
|
|
9324
|
+
},
|
|
9325
|
+
{ _id: false }
|
|
9326
|
+
),
|
|
9327
|
+
required: false
|
|
9328
|
+
},
|
|
9329
|
+
// Employer contributions (v2.8.0+)
|
|
9330
|
+
employerContributions: [
|
|
9331
|
+
{
|
|
9332
|
+
type: {
|
|
9333
|
+
type: String,
|
|
9334
|
+
enum: ["social_security", "pension", "unemployment", "health_insurance", "other"],
|
|
9335
|
+
required: true
|
|
9336
|
+
},
|
|
9337
|
+
amount: { type: Number, required: true },
|
|
9338
|
+
description: { type: String },
|
|
9339
|
+
mandatory: { type: Boolean },
|
|
9340
|
+
referenceNumber: { type: String }
|
|
9341
|
+
}
|
|
9342
|
+
],
|
|
9343
|
+
// Corrections history (v2.8.0+)
|
|
9344
|
+
corrections: [
|
|
9345
|
+
{
|
|
9346
|
+
previousAmount: { type: Number },
|
|
9347
|
+
newAmount: { type: Number },
|
|
9348
|
+
reason: { type: String },
|
|
9349
|
+
correctedBy: { type: Schema.Types.ObjectId, ref: userRef },
|
|
9350
|
+
correctedAt: { type: Date, default: Date.now }
|
|
9351
|
+
}
|
|
9352
|
+
]
|
|
8647
9353
|
};
|
|
8648
9354
|
}
|
|
8649
9355
|
var employeeIndexes = [
|
|
@@ -8674,17 +9380,51 @@ var employeeIndexes = [
|
|
|
8674
9380
|
{ fields: { organizationId: 1, "compensation.netSalary": -1 } }
|
|
8675
9381
|
];
|
|
8676
9382
|
var payrollRecordIndexes = [
|
|
8677
|
-
|
|
9383
|
+
/**
|
|
9384
|
+
* UNIQUE Compound Index (v2.9.0+) - PRIMARY duplicate protection
|
|
9385
|
+
*
|
|
9386
|
+
* Prevents duplicate payrolls at the database level for the same:
|
|
9387
|
+
* - Organization, Employee, Period (month + year + startDate), Payroll run type
|
|
9388
|
+
*
|
|
9389
|
+
* The period.startDate is critical for non-monthly frequencies (weekly, bi_weekly,
|
|
9390
|
+
* daily, hourly) where multiple payroll runs can occur within the same calendar month.
|
|
9391
|
+
*
|
|
9392
|
+
* Partial filter excludes voided records to allow re-processing.
|
|
9393
|
+
* Duplicate inserts fail with E11000 → converted to DuplicatePayrollError.
|
|
9394
|
+
*/
|
|
9395
|
+
{
|
|
9396
|
+
fields: {
|
|
9397
|
+
organizationId: 1,
|
|
9398
|
+
employeeId: 1,
|
|
9399
|
+
"period.month": 1,
|
|
9400
|
+
"period.year": 1,
|
|
9401
|
+
"period.startDate": 1,
|
|
9402
|
+
payrollRunType: 1
|
|
9403
|
+
},
|
|
9404
|
+
options: {
|
|
9405
|
+
unique: true,
|
|
9406
|
+
name: "unique_payroll_per_period_startdate_runtype",
|
|
9407
|
+
// Only enforce for non-voided records (uses $eq which is supported in partial indexes)
|
|
9408
|
+
// When a record is voided, isVoided is set to true, excluding it from unique constraint
|
|
9409
|
+
partialFilterExpression: {
|
|
9410
|
+
isVoided: { $eq: false }
|
|
9411
|
+
}
|
|
9412
|
+
}
|
|
9413
|
+
},
|
|
9414
|
+
// Composite index for common queries
|
|
8678
9415
|
{ fields: { organizationId: 1, employeeId: 1, "period.month": 1, "period.year": 1 } },
|
|
8679
9416
|
{ fields: { organizationId: 1, "period.year": 1, "period.month": 1 } },
|
|
8680
9417
|
{ fields: { employeeId: 1, "period.year": -1, "period.month": -1 } },
|
|
8681
9418
|
{ fields: { status: 1, createdAt: -1 } },
|
|
8682
9419
|
{ fields: { organizationId: 1, status: 1, "period.payDate": 1 } },
|
|
9420
|
+
// Index for payroll run type queries (supplemental, retroactive, etc.)
|
|
9421
|
+
{ fields: { organizationId: 1, payrollRunType: 1, "period.year": 1, "period.month": 1 } },
|
|
9422
|
+
// TTL index using expireAt field for per-document retention (jurisdiction-specific)
|
|
8683
9423
|
{
|
|
8684
|
-
fields: {
|
|
9424
|
+
fields: { expireAt: 1 },
|
|
8685
9425
|
options: {
|
|
8686
|
-
expireAfterSeconds:
|
|
8687
|
-
//
|
|
9426
|
+
expireAfterSeconds: 0
|
|
9427
|
+
// Delete immediately when expireAt is reached
|
|
8688
9428
|
}
|
|
8689
9429
|
}
|
|
8690
9430
|
];
|
|
@@ -9165,6 +9905,7 @@ init_utils();
|
|
|
9165
9905
|
init_prorating_calculator();
|
|
9166
9906
|
|
|
9167
9907
|
// src/shift-compliance/calculators/late-penalty.ts
|
|
9908
|
+
init_money();
|
|
9168
9909
|
function calculateFlatPenalty(occurrenceCount, flatAmount) {
|
|
9169
9910
|
return {
|
|
9170
9911
|
amount: occurrenceCount * flatAmount,
|
|
@@ -9173,12 +9914,12 @@ function calculateFlatPenalty(occurrenceCount, flatAmount) {
|
|
|
9173
9914
|
}
|
|
9174
9915
|
function calculatePerMinutePenalty(totalMinutesLate, perMinuteRate) {
|
|
9175
9916
|
return {
|
|
9176
|
-
amount:
|
|
9917
|
+
amount: roundMoney(totalMinutesLate * perMinuteRate, 2),
|
|
9177
9918
|
minutes: totalMinutesLate
|
|
9178
9919
|
};
|
|
9179
9920
|
}
|
|
9180
9921
|
function calculatePercentagePenalty(occurrenceCount, percentageRate, dailyWage) {
|
|
9181
|
-
const penaltyPerOccurrence =
|
|
9922
|
+
const penaltyPerOccurrence = roundMoney(dailyWage * percentageRate / 100, 2);
|
|
9182
9923
|
return {
|
|
9183
9924
|
amount: occurrenceCount * penaltyPerOccurrence,
|
|
9184
9925
|
percentage: percentageRate
|
|
@@ -9293,26 +10034,27 @@ function calculateLatePenalty(input) {
|
|
|
9293
10034
|
return {
|
|
9294
10035
|
date: occ.date,
|
|
9295
10036
|
minutesLate: occ.minutesLate,
|
|
9296
|
-
penaltyAmount:
|
|
10037
|
+
penaltyAmount: roundMoney(penaltyPerOccurrence, 2),
|
|
9297
10038
|
tier: tierInfo?.tier,
|
|
9298
10039
|
waived: false
|
|
9299
10040
|
};
|
|
9300
10041
|
});
|
|
9301
10042
|
return {
|
|
9302
|
-
amount:
|
|
10043
|
+
amount: roundMoney(totalPenalty, 2),
|
|
9303
10044
|
occurrences: penalizableCount,
|
|
9304
10045
|
breakdown
|
|
9305
10046
|
};
|
|
9306
10047
|
}
|
|
9307
10048
|
|
|
9308
10049
|
// src/shift-compliance/calculators/overtime.ts
|
|
10050
|
+
init_money();
|
|
9309
10051
|
function calculateDailyOvertime(hoursWorked, threshold, multiplier, hourlyRate) {
|
|
9310
10052
|
const overtimeHours = Math.max(0, hoursWorked - threshold);
|
|
9311
10053
|
if (overtimeHours === 0) {
|
|
9312
10054
|
return { amount: 0, overtimeHours: 0 };
|
|
9313
10055
|
}
|
|
9314
10056
|
const extraMultiplier = multiplier - 1;
|
|
9315
|
-
const bonus =
|
|
10057
|
+
const bonus = roundMoney(overtimeHours * hourlyRate * extraMultiplier, 2);
|
|
9316
10058
|
return { amount: bonus, overtimeHours };
|
|
9317
10059
|
}
|
|
9318
10060
|
function calculateWeeklyOvertime(hoursWorked, threshold, multiplier, hourlyRate) {
|
|
@@ -9323,12 +10065,12 @@ function calculateMonthlyOvertime(hoursWorked, threshold, multiplier, hourlyRate
|
|
|
9323
10065
|
}
|
|
9324
10066
|
function calculateWeekendPremium(hours, multiplier, hourlyRate, day) {
|
|
9325
10067
|
const extraMultiplier = multiplier - 1;
|
|
9326
|
-
const bonus =
|
|
10068
|
+
const bonus = roundMoney(hours * hourlyRate * extraMultiplier, 2);
|
|
9327
10069
|
return { amount: bonus, hours, day };
|
|
9328
10070
|
}
|
|
9329
10071
|
function calculateNightShiftDifferential(hours, multiplier, hourlyRate) {
|
|
9330
10072
|
const extraMultiplier = multiplier - 1;
|
|
9331
|
-
const bonus =
|
|
10073
|
+
const bonus = roundMoney(hours * hourlyRate * extraMultiplier, 2);
|
|
9332
10074
|
return { amount: bonus, hours };
|
|
9333
10075
|
}
|
|
9334
10076
|
function calculateOvertimeBonus(input) {
|
|
@@ -9357,14 +10099,14 @@ function calculateOvertimeBonus(input) {
|
|
|
9357
10099
|
case "daily":
|
|
9358
10100
|
case "weekly":
|
|
9359
10101
|
case "monthly":
|
|
9360
|
-
bonus =
|
|
10102
|
+
bonus = roundMoney(occ.hours * hourlyRate * extraMultiplier, 2);
|
|
9361
10103
|
break;
|
|
9362
10104
|
case "weekend-saturday":
|
|
9363
10105
|
case "weekend-sunday":
|
|
9364
|
-
bonus =
|
|
10106
|
+
bonus = roundMoney(occ.hours * hourlyRate * extraMultiplier, 2);
|
|
9365
10107
|
break;
|
|
9366
10108
|
case "night-shift":
|
|
9367
|
-
bonus =
|
|
10109
|
+
bonus = roundMoney(occ.hours * hourlyRate * extraMultiplier, 2);
|
|
9368
10110
|
break;
|
|
9369
10111
|
}
|
|
9370
10112
|
totalBonus += bonus;
|
|
@@ -9422,7 +10164,7 @@ function calculateOvertimeBonus(input) {
|
|
|
9422
10164
|
}
|
|
9423
10165
|
}
|
|
9424
10166
|
return {
|
|
9425
|
-
amount:
|
|
10167
|
+
amount: roundMoney(totalBonus, 2),
|
|
9426
10168
|
hours: totalHours,
|
|
9427
10169
|
breakdown
|
|
9428
10170
|
};
|
|
@@ -9997,7 +10739,7 @@ var TieredPenaltyBuilder = class {
|
|
|
9997
10739
|
this.saveTier();
|
|
9998
10740
|
}
|
|
9999
10741
|
if (this.parent instanceof LatePolicyBuilder) {
|
|
10000
|
-
this.parent.
|
|
10742
|
+
this.parent._getPolicy().tiers = this.tiers;
|
|
10001
10743
|
}
|
|
10002
10744
|
return this.parent;
|
|
10003
10745
|
}
|
|
@@ -10642,7 +11384,71 @@ init_enums();
|
|
|
10642
11384
|
init_utils();
|
|
10643
11385
|
init_utils();
|
|
10644
11386
|
init_utils();
|
|
11387
|
+
init_type_guards();
|
|
11388
|
+
init_error_helpers();
|
|
11389
|
+
|
|
11390
|
+
// src/core/mongokit-plugins/payroll-audit.plugin.ts
|
|
11391
|
+
function payrollAuditPlugin(context) {
|
|
11392
|
+
return (repo) => {
|
|
11393
|
+
repo.on("before:create", async (ctx) => {
|
|
11394
|
+
if (context.userId) {
|
|
11395
|
+
ctx.data.createdBy = context.userId;
|
|
11396
|
+
}
|
|
11397
|
+
ctx.data.createdAt = /* @__PURE__ */ new Date();
|
|
11398
|
+
if (context.organizationId && !ctx.data.organizationId) {
|
|
11399
|
+
ctx.data.organizationId = context.organizationId;
|
|
11400
|
+
}
|
|
11401
|
+
});
|
|
11402
|
+
repo.on("before:update", async (ctx) => {
|
|
11403
|
+
if (!ctx.data.$set) {
|
|
11404
|
+
ctx.data.$set = {};
|
|
11405
|
+
}
|
|
11406
|
+
if (context.userId) {
|
|
11407
|
+
ctx.data.$set.updatedBy = context.userId;
|
|
11408
|
+
}
|
|
11409
|
+
ctx.data.$set.updatedAt = /* @__PURE__ */ new Date();
|
|
11410
|
+
});
|
|
11411
|
+
};
|
|
11412
|
+
}
|
|
11413
|
+
function readAuditPlugin(context, logger2) {
|
|
11414
|
+
return (repo) => {
|
|
11415
|
+
repo.on("after:read", async (ctx) => {
|
|
11416
|
+
const event = {
|
|
11417
|
+
operation: "read",
|
|
11418
|
+
model: repo.model,
|
|
11419
|
+
userId: context.userId,
|
|
11420
|
+
organizationId: context.organizationId,
|
|
11421
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
11422
|
+
query: ctx.query
|
|
11423
|
+
};
|
|
11424
|
+
if (logger2) {
|
|
11425
|
+
logger2(event);
|
|
11426
|
+
}
|
|
11427
|
+
});
|
|
11428
|
+
};
|
|
11429
|
+
}
|
|
11430
|
+
function fullAuditPlugin(context, onEvent) {
|
|
11431
|
+
return (repo) => {
|
|
11432
|
+
payrollAuditPlugin(context)(repo);
|
|
11433
|
+
["after:read", "after:create", "after:update", "after:delete"].forEach(
|
|
11434
|
+
(hookName) => {
|
|
11435
|
+
repo.on(hookName, async (ctx) => {
|
|
11436
|
+
const operation = hookName.split(":")[1];
|
|
11437
|
+
const event = {
|
|
11438
|
+
operation,
|
|
11439
|
+
model: repo.model,
|
|
11440
|
+
userId: context.userId,
|
|
11441
|
+
organizationId: context.organizationId,
|
|
11442
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
11443
|
+
query: ctx.query || ctx.data
|
|
11444
|
+
};
|
|
11445
|
+
await onEvent(event);
|
|
11446
|
+
});
|
|
11447
|
+
}
|
|
11448
|
+
);
|
|
11449
|
+
};
|
|
11450
|
+
}
|
|
10645
11451
|
|
|
10646
|
-
export { ALLOWANCE_TYPE, AlreadyProcessedError, AttendancePolicyBuilder, AttendancePolicySchema, AttendancePolicySchemaDefinition, ClockRoundingPolicyBuilder, ClockRoundingPolicySchema, ClockRoundingPolicySchemaDefinition, DEDUCTION_TYPE, DEFAULT_ATTENDANCE_POLICY, DEFAULT_CARRY_OVER, DEFAULT_LEAVE_ALLOCATIONS, DEPARTMENT, DuplicatePayrollError, EMPLOYEE_STATUS, EMPLOYEE_TIMELINE_CONFIG, EMPLOYMENT_TYPE, EarlyDeparturePolicySchema, EarlyDeparturePolicySchemaDefinition, EmployeeBuilder, EmployeeFactory, EmployeeNotFoundError, EmployeeStatusMachine, EmployeeTerminatedError, HEALTHCARE_POLICY, HOSPITALITY_POLICY, HRM_CONFIG, IdempotencyManager, InvalidEmployeeError, LEAVE_REQUEST_STATUS, LEAVE_REQUEST_TIMELINE_CONFIG, LEAVE_TYPE, LateArrivalPolicySchema, LateArrivalPolicySchemaDefinition, LatePolicyBuilder, LeaveRequestStatusMachine, MANUFACTURING_POLICY, MaxPenaltiesSchema, MaxPenaltiesSchemaDefinition, NightShiftDifferentialSchema, NightShiftDifferentialSchemaDefinition, NotEligibleError, NotInitializedError, OFFICE_POLICY, OvertimePolicyBuilder, OvertimePolicySchema, OvertimePolicySchemaDefinition, PAYMENT_FREQUENCY, PAYROLL_EVENTS, PAYROLL_RECORD_TIMELINE_CONFIG, PAYROLL_STATUS, Payroll, PayrollBuilder, PayrollError, PayrollStatusMachine, PenaltyTierSchema, PenaltyTierSchemaDefinition, RETAIL_POLICY, SecurityError, StateMachine, TAX_STATUS, TAX_STATUS_VALUES, TAX_TYPE, TAX_TYPE_VALUES, TERMINATION_REASON, TaxStatusMachine, TieredPenaltyBuilder, TransactionFactory, ValidationError, WebhookManager, WeekendPremiumSchema, WeekendPremiumSchemaDefinition, accrueLeaveToBalance, allowanceSchema, applyEmployeeIndexes, applyLeaveRequestIndexes, applyPayrollRecordIndexes, applyProRating, applyTaxWithholdingIndexes, bankDetailsSchema, batchGetAttendance, buildRequestContext, buildTimelineMetadata, calculateAttendanceDeduction, calculateCarryOver, calculateDailyOvertime, calculateDailyRate, calculateFlatPenalty, calculateHourlyRate, calculateLatePenalty, calculateLeaveDays, calculateMonthlyOvertime, calculateNightShiftDifferential, calculateOvertimeBonus, calculatePartialDayDeduction, calculatePerMinutePenalty, calculatePercentagePenalty, calculateProRating, calculateSalaryBreakdown, calculateShiftCompliance, calculateTieredPenalty, calculateTotalAttendanceDeduction, calculateUnpaidLeaveDeduction, calculateWeekendPremium, calculateWeeklyOvertime, compensationSchema, createClockRoundingPolicyBuilder, createEmployee, createEmployeeSchema, createEmploymentFields, createError, createHolidaySchema, createLatePolicyBuilder, createOvertimePolicyBuilder, createPayrollInstance, createPayrollRecordFields, createPayrollRecordSchema, createPayrollTransaction, createPolicyFromPreset, createStateMachine, createTaxPaymentTransaction, deductionSchema, detectEmployeeIdType, determineOrgRole, employeeExistsSecure, employeePlugin, employmentHistorySchema, extractErrorInfo, findEmployeeSecure, findEmployeesSecure, formatEmployeeId, generatePayrollIdempotencyKey, getAttendance, getAvailableDays, getHolidays, getLeaveBalance, getLeaveBalances, getLeaveRequestFields, getLeaveRequestModel, getLeaveSummary, getTaxWithholdingFields, getTaxWithholdingModel, getUnpaidLeaveDays, hasLeaveBalance, initializeLeaveBalances, isApprovedLeaveStatus, isCancelledTaxStatus, isObjectIdEmployeeId, isPaidLeaveType, isPaidTaxStatus, isPayrollError, isPayrollTransaction, isPendingLeaveStatus, isPendingTaxStatus, isStringEmployeeId, isValidLeaveRequestStatus, isValidLeaveType, isValidTaxStatus, isValidTaxType, isVoidablePayrollStatus, isVoidedOrReversedStatus, leaveBalanceFields, leaveBalanceSchema, leaveRequestIndexes, leaveRequestSchema, mergeConfig, multiTenantPlugin, normalizeEmployeeId, payrollStatsSchema, proRateAllocation, requireOrganizationId, requiresReversalPayrollStatus, resolveOrganizationId, shouldProRate, taxWithholdingIndexes, taxWithholdingSchema, toPayrollError, tryResolveOrganizationId, validateOrganizationId, workScheduleSchema };
|
|
11452
|
+
export { ALLOWANCE_TYPE, AlreadyProcessedError, AttendancePolicyBuilder, AttendancePolicySchema, AttendancePolicySchemaDefinition, ClockRoundingPolicyBuilder, ClockRoundingPolicySchema, ClockRoundingPolicySchemaDefinition, DEDUCTION_TYPE, DEFAULT_ATTENDANCE_POLICY, DEFAULT_CARRY_OVER, DEFAULT_LEAVE_ALLOCATIONS, DEPARTMENT, DuplicatePayrollError, EMPLOYEE_STATUS, EMPLOYEE_TIMELINE_CONFIG, EMPLOYMENT_TYPE, EarlyDeparturePolicySchema, EarlyDeparturePolicySchemaDefinition, EmployeeBuilder, EmployeeFactory, EmployeeNotFoundError, EmployeeStatusMachine, EmployeeTerminatedError, HEALTHCARE_POLICY, HOSPITALITY_POLICY, HRM_CONFIG, IdempotencyManager, InvalidEmployeeError, LEAVE_REQUEST_STATUS, LEAVE_REQUEST_TIMELINE_CONFIG, LEAVE_TYPE, LateArrivalPolicySchema, LateArrivalPolicySchemaDefinition, LatePolicyBuilder, LeaveRequestStatusMachine, MANUFACTURING_POLICY, MaxPenaltiesSchema, MaxPenaltiesSchemaDefinition, NightShiftDifferentialSchema, NightShiftDifferentialSchemaDefinition, NotEligibleError, NotInitializedError, OFFICE_POLICY, OvertimePolicyBuilder, OvertimePolicySchema, OvertimePolicySchemaDefinition, PAYMENT_FREQUENCY, PAYROLL_EVENTS, PAYROLL_RECORD_TIMELINE_CONFIG, PAYROLL_STATUS, Payroll, PayrollBuilder, PayrollError, PayrollStatusMachine, PenaltyTierSchema, PenaltyTierSchemaDefinition, RETAIL_POLICY, SecurityError, StateMachine, TAX_STATUS, TAX_STATUS_VALUES, TAX_TYPE, TAX_TYPE_VALUES, TERMINATION_REASON, TaxStatusMachine, TieredPenaltyBuilder, TransactionFactory, ValidationError, WebhookManager, WeekendPremiumSchema, WeekendPremiumSchemaDefinition, accrueLeaveToBalance, allowanceSchema, applyEmployeeIndexes, applyLeaveRequestIndexes, applyPayrollRecordIndexes, applyProRating, applyTaxWithholdingIndexes, bankDetailsSchema, batchGetAttendance, buildRequestContext, buildTimelineMetadata, calculateAttendanceDeduction, calculateCarryOver, calculateDailyOvertime, calculateDailyRate, calculateFlatPenalty, calculateHourlyRate, calculateLatePenalty, calculateLeaveDays, calculateMonthlyOvertime, calculateNightShiftDifferential, calculateOvertimeBonus, calculatePartialDayDeduction, calculatePerMinutePenalty, calculatePercentagePenalty, calculateProRating, calculateSalaryBreakdown, calculateShiftCompliance, calculateTieredPenalty, calculateTotalAttendanceDeduction, calculateUnpaidLeaveDeduction, calculateWeekendPremium, calculateWeeklyOvertime, compensationSchema, createClockRoundingPolicyBuilder, createEmployee, createEmployeeSchema, createEmploymentFields, createError, createHolidaySchema, createLatePolicyBuilder, createOvertimePolicyBuilder, createPayrollInstance, createPayrollRecordFields, createPayrollRecordSchema, createPayrollTransaction, createPolicyFromPreset, createStateMachine, createTaxPaymentTransaction, deductionSchema, definePlugin, detectEmployeeIdType, determineOrgRole, employeeExistsSecure, employeeIndexes, employeePlugin, employmentHistorySchema, extractErrorInfo, findEmployeeSecure, findEmployeesSecure, formatEmployeeId, formatUserError, fullAuditPlugin, generatePayrollIdempotencyKey, getAttendance, getAvailableDays, getEmployeeEmail, getEmployeeName, getErrorMessage, getHolidays, getLeaveBalance, getLeaveBalances, getLeaveRequestFields, getLeaveRequestModel, getLeaveSummary, getTaxWithholdingFields, getTaxWithholdingModel, getUnpaidLeaveDays, handleDuplicateKeyError, handlePayrollError, handleTransactionError, hasLeaveBalance, hasUserId, initializeLeaveBalances, isApprovedLeaveStatus, isCancelledTaxStatus, isConnectionError, isDuplicateKeyError, isError, isGuestEmployee, isMongoError, isObjectIdEmployeeId, isPaidLeaveType, isPaidTaxStatus, isPayrollError, isPayrollTransaction, isPendingLeaveStatus, isPendingTaxStatus, isStringEmployeeId, isTransactionError, isTransactionUnsupportedError, isValidLeaveRequestStatus, isValidLeaveType, isValidTaxStatus, isValidTaxType, isValidationError, isVoidablePayrollStatus, isVoidedOrReversedStatus, leaveBalanceFields, leaveBalanceSchema, leaveRequestIndexes, leaveRequestSchema, mergeConfig, multiTenantPlugin, normalizeEmployeeId, parseDuplicateKeyError, payrollAuditPlugin, payrollRecordIndexes, payrollStatsSchema, proRateAllocation, readAuditPlugin, requireOrganizationId, requiresReversalPayrollStatus, resolveOrganizationId, shouldProRate, taxWithholdingIndexes, taxWithholdingSchema, toPayrollError, tryResolveOrganizationId, validateOrganizationId, workScheduleSchema };
|
|
10647
11453
|
//# sourceMappingURL=index.js.map
|
|
10648
11454
|
//# sourceMappingURL=index.js.map
|