@classytic/payroll 1.0.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @classytic/payroll might be problematic. Click here for more details.

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