@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/dist/index.js CHANGED
@@ -1,15 +1,13 @@
1
- import mongoose6, { Schema, Types, isValidObjectId as isValidObjectId$1 } from 'mongoose';
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
- function resetLogger() {
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
- return Math.ceil(
574
- (new Date(end).getTime() - new Date(start).getTime()) / (1e3 * 60 * 60 * 24)
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) => new Date(d).toDateString())
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.toDateString());
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 Math.round(baseAmount * ratio);
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) => new Date(d).toDateString()));
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.toDateString());
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 = diffInDays2(fiscalYearStart, fiscalYearEnd);
1198
- const remainingDays = diffInDays2(hireDate, fiscalYearEnd);
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 = diffInDays2(fiscalYearStart, fiscalYearEnd);
1223
- const remainingDays = Math.max(0, diffInDays2(hireDate, fiscalYearEnd));
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 Math.round(dailyRate * unpaidDays);
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 separate
1566
- * TaxWithholding records for each tax type
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, optionally creates government payment transaction,
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.markAsPaid(
1749
- governmentTransaction?._id,
1750
- referenceNumber,
1751
- paidAt
1752
- );
1753
- await withholding.save({ session });
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: 3e5, rate: 0 },
2009
- { min: 3e5, max: 4e5, rate: 0.05 },
2010
- { min: 4e5, max: 5e5, rate: 0.1 },
2011
- { min: 5e5, max: 6e5, rate: 0.15 },
2012
- { min: 6e5, max: 3e6, rate: 0.2 },
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
- console.error(`Event handler error for ${event}:`, error);
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
- const { getLogger: getLogger2 } = (init_logger(), __toCommonJS(logger_exports));
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
- return `payroll:${organizationId}:${employeeId}:${year}-${month}`;
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 ? Math.round(baseAmount * allowance.value / 100) : allowance.amount;
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 ? Math.round(baseAmount * deduction.value / 100) : deduction.amount;
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
- Math.round(newBaseAmount),
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: Math.round(stats.totalBase || 0),
4374
- totalGross: Math.round(stats.totalGross || 0),
4375
- totalNet: Math.round(stats.totalNet || 0),
4376
- averageBase: Math.round(stats.avgBase || 0),
4377
- averageGross: Math.round(stats.avgGross || 0),
4378
- averageNet: Math.round(stats.avgNet || 0)
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: Math.round(dept.totalNet || 0)
4669
+ totalNet: roundMoney(dept.totalNet || 0, 2)
4444
4670
  };
4445
4671
  });
4446
4672
  return {
4447
4673
  employeeCount: overallStats.employeeCount,
4448
- totalBase: Math.round(overallStats.totalBase || 0),
4449
- totalGross: Math.round(overallStats.totalGross || 0),
4450
- totalNet: Math.round(overallStats.totalNet || 0),
4451
- averageBase: Math.round(overallStats.avgBase || 0),
4452
- averageGross: Math.round(overallStats.avgGross || 0),
4453
- averageNet: Math.round(overallStats.avgNet || 0),
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 = Math.round(absentDays * rate);
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 Math.round(monthlySalary / workingDays);
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 Math.round(dailyRate / hoursPerDay);
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 Math.round(dailyRate * fraction);
4740
+ return roundMoney(dailyRate * fraction, 2);
4514
4741
  }
4515
4742
  function calculateTotalAttendanceDeduction(input) {
4516
4743
  const { dailyRate, fullDayAbsences = 0, partialDayAbsences = [] } = input;
4517
- const fullDayDeduction = Math.round(dailyRate * Math.max(0, fullDayAbsences));
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
- const taxableAmount = baseAmount + sumAllowances(taxableAllowances);
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
- const annualTaxable = taxableAmount * 12;
4570
- const annualTax = applyTaxBrackets(annualTaxable, taxBrackets);
4571
- taxAmount = roundMoney(annualTax / 12);
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 { employeeId, employeeIdMode, organizationId: explicitOrgId, month, year, paymentDate = /* @__PURE__ */ new Date(), paymentMethod = "bank", attendance, options, context, idempotencyKey } = params;
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 idempotentKey = idempotencyKey || generatePayrollIdempotencyKey(orgId, resolvedEmployeeId, month, year);
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 existingPayroll = await this.models.PayrollRecordModel.findOne({
5238
+ const query = {
4909
5239
  organizationId: orgId,
4910
5240
  employeeId: resolvedEmployeeId,
4911
5241
  "period.month": month,
4912
- "period.year": year
4913
- }).populate("transactionId");
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
- async _processSalaryWithSession(session, repos, params, resolvedEmployeeId, orgId, idempotentKey) {
4949
- const { employeeId, employeeIdMode, month, year, paymentDate = /* @__PURE__ */ new Date(), paymentMethod = "bank", attendance, options, context } = params;
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 = await this.findEmployeeFn({
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 existingQuery = payroll().forOrganization(orgId).forEmployee(employee2._id).forPeriod(month, year).build();
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 retry voided payroll for employee ${employee2.employeeId} in ${month}/${year} - intentionally cancelled`,
4976
- "DUPLICATE_PAYROLL",
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
- { status: existingRecord.status, reason: "intentionally_cancelled" }
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 failed record without transaction for retry", {
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} - preserve data integrity`,
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: "data_integrity_preservation" }
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 [payrollRecord] = await this.models.PayrollRecordModel.create([{
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
- }], mongooseSession ? { session: mongooseSession } : {});
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 [transaction] = await this.models.TransactionModel.create([{
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
- // ✅ UNIFIED: Breakdown structure
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
- }], mongooseSession ? { session: mongooseSession } : {});
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 query = { organizationId: toObjectId(organizationId), status: "active" };
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.successful.length + results.failed.length;
5716
+ const processed = results.successCount + results.failCount;
5262
5717
  await onProgress({
5263
5718
  processed,
5264
5719
  total,
5265
- successful: results.successful.length,
5266
- failed: results.failed.length,
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.successful.length + results.failed.length,
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
- results.successful.push({
5297
- employeeId: employee2.employeeId,
5298
- amount: result.payrollRecord.breakdown.netSalary,
5299
- transactionId: result.transaction._id
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.failed.push({
5303
- employeeId: employee2.employeeId,
5304
- error: error.message
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
- results.successful.push({
5336
- employeeId: batchResult.value.employee.employeeId,
5337
- amount: batchResult.value.result.payrollRecord.breakdown.netSalary,
5338
- transactionId: batchResult.value.result.transaction._id
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.failed.push({
5342
- employeeId: employee2.employeeId,
5343
- error: batchResult.reason.message || "Unknown error"
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.successful.length,
5363
- failed: results.failed.length,
5364
- totalAmount: results.successful.reduce((sum, r) => sum + r.amount, 0)
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.successful.length,
5374
- failed: results.failed.length,
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.successful.length,
5422
- failed: results.failed.length,
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
- results.successful.push({
5450
- employeeId: employee2.employeeId,
5451
- amount: result.payrollRecord.breakdown.netSalary,
5452
- transactionId: result.transaction._id
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.failed.push({
5456
- employeeId: employee2.employeeId,
5457
- error: error.message
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.successful.length,
5486
- failed: results.failed.length,
5487
- totalAmount: results.successful.reduce((sum, r) => sum + r.amount, 0)
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.successful.length,
5495
- failed: results.failed.length,
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 query = { organizationId: toObjectId(organizationId) };
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
- this._container
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 = Math.round(
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.payrollStats.nextPaymentDate = addMonths(paymentDate, 1);
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 = mongoose6.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 = mongoose6.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
- // Composite index for common queries (not unique - app handles duplicates)
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: { createdAt: 1 },
9424
+ fields: { expireAt: 1 },
8685
9425
  options: {
8686
- expireAfterSeconds: HRM_CONFIG.dataRetention.payrollRecordsTTL
8687
- // TTL applies to ALL records (user handles backups/exports at app level)
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: Math.round(totalMinutesLate * perMinuteRate),
9917
+ amount: roundMoney(totalMinutesLate * perMinuteRate, 2),
9177
9918
  minutes: totalMinutesLate
9178
9919
  };
9179
9920
  }
9180
9921
  function calculatePercentagePenalty(occurrenceCount, percentageRate, dailyWage) {
9181
- const penaltyPerOccurrence = Math.round(dailyWage * percentageRate / 100);
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: Math.round(penaltyPerOccurrence),
10037
+ penaltyAmount: roundMoney(penaltyPerOccurrence, 2),
9297
10038
  tier: tierInfo?.tier,
9298
10039
  waived: false
9299
10040
  };
9300
10041
  });
9301
10042
  return {
9302
- amount: Math.round(totalPenalty),
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 = Math.round(overtimeHours * hourlyRate * extraMultiplier);
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 = Math.round(hours * hourlyRate * extraMultiplier);
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 = Math.round(hours * hourlyRate * extraMultiplier);
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 = Math.round(occ.hours * hourlyRate * extraMultiplier);
10102
+ bonus = roundMoney(occ.hours * hourlyRate * extraMultiplier, 2);
9361
10103
  break;
9362
10104
  case "weekend-saturday":
9363
10105
  case "weekend-sunday":
9364
- bonus = Math.round(occ.hours * hourlyRate * extraMultiplier);
10106
+ bonus = roundMoney(occ.hours * hourlyRate * extraMultiplier, 2);
9365
10107
  break;
9366
10108
  case "night-shift":
9367
- bonus = Math.round(occ.hours * hourlyRate * extraMultiplier);
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: Math.round(totalBonus),
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.policy.tiers = this.tiers;
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