@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.
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
@@ -0,0 +1,2103 @@
1
+ import mongoose2, { 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
+ function getLogger() {
40
+ return currentLogger;
41
+ }
42
+ function setLogger(logger) {
43
+ currentLogger = logger;
44
+ }
45
+
46
+ // src/config.ts
47
+ var HRM_CONFIG = {
48
+ dataRetention: {
49
+ payrollRecordsTTL: 63072e3,
50
+ // 2 years in seconds
51
+ exportWarningDays: 30,
52
+ archiveBeforeDeletion: true
53
+ },
54
+ payroll: {
55
+ defaultCurrency: "BDT",
56
+ allowProRating: true,
57
+ attendanceIntegration: true,
58
+ autoDeductions: true,
59
+ overtimeEnabled: false,
60
+ overtimeMultiplier: 1.5
61
+ },
62
+ salary: {
63
+ minimumWage: 0,
64
+ maximumAllowances: 10,
65
+ maximumDeductions: 10,
66
+ defaultFrequency: "monthly"
67
+ },
68
+ employment: {
69
+ defaultProbationMonths: 3,
70
+ maxProbationMonths: 6,
71
+ allowReHiring: true,
72
+ trackEmploymentHistory: true
73
+ },
74
+ validation: {
75
+ requireBankDetails: false,
76
+ requireEmployeeId: true,
77
+ uniqueEmployeeIdPerOrg: true,
78
+ allowMultiTenantEmployees: true
79
+ }
80
+ };
81
+ var TAX_BRACKETS = {
82
+ BDT: [
83
+ { min: 0, max: 3e5, rate: 0 },
84
+ { min: 3e5, max: 4e5, rate: 0.05 },
85
+ { min: 4e5, max: 5e5, rate: 0.1 },
86
+ { min: 5e5, max: 6e5, rate: 0.15 },
87
+ { min: 6e5, max: 3e6, rate: 0.2 },
88
+ { min: 3e6, max: Infinity, rate: 0.25 }
89
+ ],
90
+ USD: [
91
+ { min: 0, max: 1e4, rate: 0.1 },
92
+ { min: 1e4, max: 4e4, rate: 0.12 },
93
+ { min: 4e4, max: 85e3, rate: 0.22 },
94
+ { min: 85e3, max: 165e3, rate: 0.24 },
95
+ { min: 165e3, max: 215e3, rate: 0.32 },
96
+ { min: 215e3, max: 54e4, rate: 0.35 },
97
+ { min: 54e4, max: Infinity, rate: 0.37 }
98
+ ]
99
+ };
100
+ var ORG_ROLES = {
101
+ OWNER: {
102
+ key: "owner",
103
+ label: "Owner",
104
+ description: "Full organization access (set by Organization model)"
105
+ },
106
+ MANAGER: {
107
+ key: "manager",
108
+ label: "Manager",
109
+ description: "Management and administrative features"
110
+ },
111
+ TRAINER: {
112
+ key: "trainer",
113
+ label: "Trainer",
114
+ description: "Training and coaching features"
115
+ },
116
+ STAFF: {
117
+ key: "staff",
118
+ label: "Staff",
119
+ description: "General staff access to basic features"
120
+ },
121
+ INTERN: {
122
+ key: "intern",
123
+ label: "Intern",
124
+ description: "Limited access for interns"
125
+ },
126
+ CONSULTANT: {
127
+ key: "consultant",
128
+ label: "Consultant",
129
+ description: "Project-based consultant access"
130
+ }
131
+ };
132
+ Object.values(ORG_ROLES).map((role) => role.key);
133
+ function mergeConfig(customConfig) {
134
+ if (!customConfig) return HRM_CONFIG;
135
+ return {
136
+ dataRetention: { ...HRM_CONFIG.dataRetention, ...customConfig.dataRetention },
137
+ payroll: { ...HRM_CONFIG.payroll, ...customConfig.payroll },
138
+ salary: { ...HRM_CONFIG.salary, ...customConfig.salary },
139
+ employment: { ...HRM_CONFIG.employment, ...customConfig.employment },
140
+ validation: { ...HRM_CONFIG.validation, ...customConfig.validation }
141
+ };
142
+ }
143
+
144
+ // src/core/container.ts
145
+ var Container = class _Container {
146
+ static instance = null;
147
+ _models = null;
148
+ _config = HRM_CONFIG;
149
+ _singleTenant = null;
150
+ _logger;
151
+ _initialized = false;
152
+ constructor() {
153
+ this._logger = getLogger();
154
+ }
155
+ /**
156
+ * Get singleton instance
157
+ */
158
+ static getInstance() {
159
+ if (!_Container.instance) {
160
+ _Container.instance = new _Container();
161
+ }
162
+ return _Container.instance;
163
+ }
164
+ /**
165
+ * Reset instance (for testing)
166
+ */
167
+ static resetInstance() {
168
+ _Container.instance = null;
169
+ }
170
+ /**
171
+ * Initialize container with configuration
172
+ */
173
+ initialize(config) {
174
+ if (this._initialized) {
175
+ this._logger.warn("Container already initialized, re-initializing");
176
+ }
177
+ this._models = config.models;
178
+ this._config = mergeConfig(config.config);
179
+ this._singleTenant = config.singleTenant ?? null;
180
+ if (config.logger) {
181
+ this._logger = config.logger;
182
+ }
183
+ this._initialized = true;
184
+ this._logger.info("Container initialized", {
185
+ hasEmployeeModel: !!this._models.EmployeeModel,
186
+ hasPayrollRecordModel: !!this._models.PayrollRecordModel,
187
+ hasTransactionModel: !!this._models.TransactionModel,
188
+ hasAttendanceModel: !!this._models.AttendanceModel,
189
+ isSingleTenant: !!this._singleTenant
190
+ });
191
+ }
192
+ /**
193
+ * Check if container is initialized
194
+ */
195
+ isInitialized() {
196
+ return this._initialized;
197
+ }
198
+ /**
199
+ * Ensure container is initialized
200
+ */
201
+ ensureInitialized() {
202
+ if (!this._initialized || !this._models) {
203
+ throw new Error(
204
+ "Payroll not initialized. Call Payroll.initialize() first."
205
+ );
206
+ }
207
+ }
208
+ /**
209
+ * Get models container
210
+ */
211
+ getModels() {
212
+ this.ensureInitialized();
213
+ return this._models;
214
+ }
215
+ /**
216
+ * Get Employee model
217
+ */
218
+ getEmployeeModel() {
219
+ this.ensureInitialized();
220
+ return this._models.EmployeeModel;
221
+ }
222
+ /**
223
+ * Get PayrollRecord model
224
+ */
225
+ getPayrollRecordModel() {
226
+ this.ensureInitialized();
227
+ return this._models.PayrollRecordModel;
228
+ }
229
+ /**
230
+ * Get Transaction model
231
+ */
232
+ getTransactionModel() {
233
+ this.ensureInitialized();
234
+ return this._models.TransactionModel;
235
+ }
236
+ /**
237
+ * Get Attendance model (optional)
238
+ */
239
+ getAttendanceModel() {
240
+ this.ensureInitialized();
241
+ return this._models.AttendanceModel ?? null;
242
+ }
243
+ /**
244
+ * Get configuration
245
+ */
246
+ getConfig() {
247
+ return this._config;
248
+ }
249
+ /**
250
+ * Get specific config section
251
+ */
252
+ getConfigSection(section) {
253
+ return this._config[section];
254
+ }
255
+ /**
256
+ * Check if single-tenant mode
257
+ */
258
+ isSingleTenant() {
259
+ return !!this._singleTenant;
260
+ }
261
+ /**
262
+ * Get single-tenant config
263
+ */
264
+ getSingleTenantConfig() {
265
+ return this._singleTenant;
266
+ }
267
+ /**
268
+ * Get organization ID (for single-tenant mode)
269
+ */
270
+ getOrganizationId() {
271
+ if (!this._singleTenant || !this._singleTenant.organizationId) return null;
272
+ return typeof this._singleTenant.organizationId === "string" ? this._singleTenant.organizationId : this._singleTenant.organizationId.toString();
273
+ }
274
+ /**
275
+ * Get logger
276
+ */
277
+ getLogger() {
278
+ return this._logger;
279
+ }
280
+ /**
281
+ * Set logger
282
+ */
283
+ setLogger(logger) {
284
+ this._logger = logger;
285
+ }
286
+ /**
287
+ * Has attendance integration
288
+ */
289
+ hasAttendanceIntegration() {
290
+ return !!this._models?.AttendanceModel && this._config.payroll.attendanceIntegration;
291
+ }
292
+ /**
293
+ * Create operation context with defaults
294
+ */
295
+ createOperationContext(overrides) {
296
+ const context = {};
297
+ if (this._singleTenant?.autoInject && !overrides?.organizationId) {
298
+ context.organizationId = this.getOrganizationId();
299
+ }
300
+ return { ...context, ...overrides };
301
+ }
302
+ };
303
+ function initializeContainer(config) {
304
+ Container.getInstance().initialize(config);
305
+ }
306
+
307
+ // src/core/events.ts
308
+ var EventBus = class {
309
+ handlers = /* @__PURE__ */ new Map();
310
+ /**
311
+ * Register an event handler
312
+ */
313
+ on(event, handler) {
314
+ if (!this.handlers.has(event)) {
315
+ this.handlers.set(event, /* @__PURE__ */ new Set());
316
+ }
317
+ this.handlers.get(event).add(handler);
318
+ return () => this.off(event, handler);
319
+ }
320
+ /**
321
+ * Register a one-time event handler
322
+ */
323
+ once(event, handler) {
324
+ const wrappedHandler = async (payload) => {
325
+ this.off(event, wrappedHandler);
326
+ await handler(payload);
327
+ };
328
+ return this.on(event, wrappedHandler);
329
+ }
330
+ /**
331
+ * Remove an event handler
332
+ */
333
+ off(event, handler) {
334
+ const eventHandlers = this.handlers.get(event);
335
+ if (eventHandlers) {
336
+ eventHandlers.delete(handler);
337
+ }
338
+ }
339
+ /**
340
+ * Emit an event
341
+ */
342
+ async emit(event, payload) {
343
+ const eventHandlers = this.handlers.get(event);
344
+ if (!eventHandlers || eventHandlers.size === 0) {
345
+ return;
346
+ }
347
+ const handlers = Array.from(eventHandlers);
348
+ await Promise.all(
349
+ handlers.map(async (handler) => {
350
+ try {
351
+ await handler(payload);
352
+ } catch (error) {
353
+ console.error(`Event handler error for ${event}:`, error);
354
+ }
355
+ })
356
+ );
357
+ }
358
+ /**
359
+ * Emit event synchronously (fire-and-forget)
360
+ */
361
+ emitSync(event, payload) {
362
+ void this.emit(event, payload);
363
+ }
364
+ /**
365
+ * Remove all handlers for an event
366
+ */
367
+ removeAllListeners(event) {
368
+ if (event) {
369
+ this.handlers.delete(event);
370
+ } else {
371
+ this.handlers.clear();
372
+ }
373
+ }
374
+ /**
375
+ * Get listener count for an event
376
+ */
377
+ listenerCount(event) {
378
+ return this.handlers.get(event)?.size ?? 0;
379
+ }
380
+ /**
381
+ * Get all registered events
382
+ */
383
+ eventNames() {
384
+ return Array.from(this.handlers.keys());
385
+ }
386
+ };
387
+ function createEventBus() {
388
+ return new EventBus();
389
+ }
390
+
391
+ // src/core/plugin.ts
392
+ var PluginManager = class {
393
+ constructor(context) {
394
+ this.context = context;
395
+ }
396
+ plugins = /* @__PURE__ */ new Map();
397
+ hooks = /* @__PURE__ */ new Map();
398
+ /**
399
+ * Register a plugin
400
+ */
401
+ async register(plugin) {
402
+ if (this.plugins.has(plugin.name)) {
403
+ throw new Error(`Plugin "${plugin.name}" is already registered`);
404
+ }
405
+ if (plugin.hooks) {
406
+ for (const [hookName, handler] of Object.entries(plugin.hooks)) {
407
+ if (handler) {
408
+ this.addHook(hookName, handler);
409
+ }
410
+ }
411
+ }
412
+ if (plugin.init) {
413
+ await plugin.init(this.context);
414
+ }
415
+ this.plugins.set(plugin.name, plugin);
416
+ this.context.logger.debug(`Plugin "${plugin.name}" registered`);
417
+ }
418
+ /**
419
+ * Unregister a plugin
420
+ */
421
+ async unregister(name) {
422
+ const plugin = this.plugins.get(name);
423
+ if (!plugin) {
424
+ return;
425
+ }
426
+ if (plugin.destroy) {
427
+ await plugin.destroy();
428
+ }
429
+ this.plugins.delete(name);
430
+ this.context.logger.debug(`Plugin "${name}" unregistered`);
431
+ }
432
+ /**
433
+ * Add a hook handler
434
+ */
435
+ addHook(hookName, handler) {
436
+ if (!this.hooks.has(hookName)) {
437
+ this.hooks.set(hookName, []);
438
+ }
439
+ this.hooks.get(hookName).push(handler);
440
+ }
441
+ /**
442
+ * Execute hooks for a given event
443
+ */
444
+ async executeHooks(hookName, ...args) {
445
+ const handlers = this.hooks.get(hookName);
446
+ if (!handlers || handlers.length === 0) {
447
+ return;
448
+ }
449
+ for (const handler of handlers) {
450
+ try {
451
+ await handler(...args);
452
+ } catch (error) {
453
+ this.context.logger.error(`Hook "${hookName}" error:`, { error });
454
+ const errorHandlers = this.hooks.get("onError");
455
+ if (errorHandlers) {
456
+ for (const errorHandler of errorHandlers) {
457
+ try {
458
+ await errorHandler(error, hookName);
459
+ } catch {
460
+ }
461
+ }
462
+ }
463
+ }
464
+ }
465
+ }
466
+ /**
467
+ * Get registered plugin names
468
+ */
469
+ getPluginNames() {
470
+ return Array.from(this.plugins.keys());
471
+ }
472
+ /**
473
+ * Check if plugin is registered
474
+ */
475
+ hasPlugin(name) {
476
+ return this.plugins.has(name);
477
+ }
478
+ };
479
+
480
+ // src/utils/date.ts
481
+ function addMonths(date, months) {
482
+ const result = new Date(date);
483
+ result.setMonth(result.getMonth() + months);
484
+ return result;
485
+ }
486
+ function startOfMonth(date) {
487
+ const result = new Date(date);
488
+ result.setDate(1);
489
+ result.setHours(0, 0, 0, 0);
490
+ return result;
491
+ }
492
+ function endOfMonth(date) {
493
+ const result = new Date(date);
494
+ result.setMonth(result.getMonth() + 1, 0);
495
+ result.setHours(23, 59, 59, 999);
496
+ return result;
497
+ }
498
+ function diffInDays(start, end) {
499
+ return Math.ceil(
500
+ (new Date(end).getTime() - new Date(start).getTime()) / (1e3 * 60 * 60 * 24)
501
+ );
502
+ }
503
+ function isWeekday(date) {
504
+ const day = new Date(date).getDay();
505
+ return day >= 1 && day <= 5;
506
+ }
507
+ function getPayPeriod(month, year) {
508
+ const startDate = new Date(year, month - 1, 1);
509
+ return {
510
+ month,
511
+ year,
512
+ startDate: startOfMonth(startDate),
513
+ endDate: endOfMonth(startDate)
514
+ };
515
+ }
516
+ function getWorkingDaysInMonth(year, month) {
517
+ const start = new Date(year, month - 1, 1);
518
+ const end = endOfMonth(start);
519
+ let count = 0;
520
+ const current = new Date(start);
521
+ while (current <= end) {
522
+ if (isWeekday(current)) {
523
+ count++;
524
+ }
525
+ current.setDate(current.getDate() + 1);
526
+ }
527
+ return count;
528
+ }
529
+ function calculateProbationEnd(hireDate, probationMonths) {
530
+ if (!probationMonths || probationMonths <= 0) return null;
531
+ return addMonths(hireDate, probationMonths);
532
+ }
533
+
534
+ // src/factories/employee.factory.ts
535
+ var EmployeeFactory = class {
536
+ /**
537
+ * Create employee data object
538
+ */
539
+ static create(params) {
540
+ const { userId, organizationId, employment, compensation, bankDetails } = params;
541
+ const hireDate = employment.hireDate || /* @__PURE__ */ new Date();
542
+ return {
543
+ userId,
544
+ organizationId,
545
+ employeeId: employment.employeeId || `EMP-${Date.now()}-${Math.random().toString(36).substr(2, 6).toUpperCase()}`,
546
+ employmentType: employment.type || "full_time",
547
+ status: "active",
548
+ department: employment.department,
549
+ position: employment.position,
550
+ hireDate,
551
+ probationEndDate: calculateProbationEnd(
552
+ hireDate,
553
+ employment.probationMonths ?? HRM_CONFIG.employment.defaultProbationMonths
554
+ ),
555
+ compensation: this.createCompensation(compensation),
556
+ workSchedule: employment.workSchedule || this.defaultWorkSchedule(),
557
+ bankDetails: bankDetails || {},
558
+ payrollStats: {
559
+ totalPaid: 0,
560
+ paymentsThisYear: 0,
561
+ averageMonthly: 0
562
+ }
563
+ };
564
+ }
565
+ /**
566
+ * Create compensation object
567
+ */
568
+ static createCompensation(params) {
569
+ return {
570
+ baseAmount: params.baseAmount,
571
+ frequency: params.frequency || "monthly",
572
+ currency: params.currency || HRM_CONFIG.payroll.defaultCurrency,
573
+ allowances: (params.allowances || []).map((a) => ({
574
+ type: a.type || "other",
575
+ name: a.name || a.type || "other",
576
+ amount: a.amount || 0,
577
+ taxable: a.taxable,
578
+ recurring: a.recurring,
579
+ effectiveFrom: a.effectiveFrom,
580
+ effectiveTo: a.effectiveTo
581
+ })),
582
+ deductions: (params.deductions || []).map((d) => ({
583
+ type: d.type || "other",
584
+ name: d.name || d.type || "other",
585
+ amount: d.amount || 0,
586
+ auto: d.auto,
587
+ recurring: d.recurring,
588
+ description: d.description,
589
+ effectiveFrom: d.effectiveFrom,
590
+ effectiveTo: d.effectiveTo
591
+ })),
592
+ grossSalary: 0,
593
+ netSalary: 0,
594
+ effectiveFrom: /* @__PURE__ */ new Date(),
595
+ lastModified: /* @__PURE__ */ new Date()
596
+ };
597
+ }
598
+ /**
599
+ * Create allowance object
600
+ */
601
+ static createAllowance(params) {
602
+ return {
603
+ type: params.type,
604
+ name: params.name || params.type,
605
+ amount: params.amount,
606
+ isPercentage: params.isPercentage ?? false,
607
+ taxable: params.taxable ?? true,
608
+ recurring: params.recurring ?? true,
609
+ effectiveFrom: /* @__PURE__ */ new Date()
610
+ };
611
+ }
612
+ /**
613
+ * Create deduction object
614
+ */
615
+ static createDeduction(params) {
616
+ return {
617
+ type: params.type,
618
+ name: params.name || params.type,
619
+ amount: params.amount,
620
+ isPercentage: params.isPercentage ?? false,
621
+ auto: params.auto ?? false,
622
+ recurring: params.recurring ?? true,
623
+ description: params.description,
624
+ effectiveFrom: /* @__PURE__ */ new Date()
625
+ };
626
+ }
627
+ /**
628
+ * Default work schedule
629
+ */
630
+ static defaultWorkSchedule() {
631
+ return {
632
+ hoursPerWeek: 40,
633
+ hoursPerDay: 8,
634
+ workingDays: [1, 2, 3, 4, 5],
635
+ // Mon-Fri
636
+ shiftStart: "09:00",
637
+ shiftEnd: "17:00"
638
+ };
639
+ }
640
+ /**
641
+ * Create termination data
642
+ */
643
+ static createTermination(params) {
644
+ return {
645
+ terminatedAt: params.date || /* @__PURE__ */ new Date(),
646
+ terminationReason: params.reason,
647
+ terminationNotes: params.notes,
648
+ terminatedBy: {
649
+ userId: params.context?.userId,
650
+ name: params.context?.userName,
651
+ role: params.context?.userRole
652
+ }
653
+ };
654
+ }
655
+ };
656
+ var HRM_TRANSACTION_CATEGORIES = {
657
+ SALARY: "salary",
658
+ BONUS: "bonus",
659
+ COMMISSION: "commission",
660
+ OVERTIME: "overtime",
661
+ SEVERANCE: "severance"
662
+ };
663
+ function toObjectId(id) {
664
+ if (id instanceof Types.ObjectId) return id;
665
+ return new Types.ObjectId(id);
666
+ }
667
+ var QueryBuilder = class {
668
+ query;
669
+ constructor(initialQuery = {}) {
670
+ this.query = { ...initialQuery };
671
+ }
672
+ /**
673
+ * Add where condition
674
+ */
675
+ where(field, value) {
676
+ this.query[field] = value;
677
+ return this;
678
+ }
679
+ /**
680
+ * Add $in condition
681
+ */
682
+ whereIn(field, values) {
683
+ this.query[field] = { $in: values };
684
+ return this;
685
+ }
686
+ /**
687
+ * Add $nin condition
688
+ */
689
+ whereNotIn(field, values) {
690
+ this.query[field] = { $nin: values };
691
+ return this;
692
+ }
693
+ /**
694
+ * Add $gte condition
695
+ */
696
+ whereGte(field, value) {
697
+ const existing = this.query[field] || {};
698
+ this.query[field] = { ...existing, $gte: value };
699
+ return this;
700
+ }
701
+ /**
702
+ * Add $lte condition
703
+ */
704
+ whereLte(field, value) {
705
+ const existing = this.query[field] || {};
706
+ this.query[field] = { ...existing, $lte: value };
707
+ return this;
708
+ }
709
+ /**
710
+ * Add $gt condition
711
+ */
712
+ whereGt(field, value) {
713
+ const existing = this.query[field] || {};
714
+ this.query[field] = { ...existing, $gt: value };
715
+ return this;
716
+ }
717
+ /**
718
+ * Add $lt condition
719
+ */
720
+ whereLt(field, value) {
721
+ const existing = this.query[field] || {};
722
+ this.query[field] = { ...existing, $lt: value };
723
+ return this;
724
+ }
725
+ /**
726
+ * Add between condition
727
+ */
728
+ whereBetween(field, start, end) {
729
+ this.query[field] = { $gte: start, $lte: end };
730
+ return this;
731
+ }
732
+ /**
733
+ * Add $exists condition
734
+ */
735
+ whereExists(field) {
736
+ this.query[field] = { $exists: true };
737
+ return this;
738
+ }
739
+ /**
740
+ * Add $exists: false condition
741
+ */
742
+ whereNotExists(field) {
743
+ this.query[field] = { $exists: false };
744
+ return this;
745
+ }
746
+ /**
747
+ * Add $ne condition
748
+ */
749
+ whereNot(field, value) {
750
+ this.query[field] = { $ne: value };
751
+ return this;
752
+ }
753
+ /**
754
+ * Add regex condition
755
+ */
756
+ whereRegex(field, pattern, flags = "i") {
757
+ this.query[field] = { $regex: pattern, $options: flags };
758
+ return this;
759
+ }
760
+ /**
761
+ * Merge another query
762
+ */
763
+ merge(otherQuery) {
764
+ this.query = { ...this.query, ...otherQuery };
765
+ return this;
766
+ }
767
+ /**
768
+ * Build and return the query
769
+ */
770
+ build() {
771
+ return { ...this.query };
772
+ }
773
+ };
774
+ var EmployeeQueryBuilder = class extends QueryBuilder {
775
+ /**
776
+ * Filter by organization
777
+ */
778
+ forOrganization(organizationId) {
779
+ return this.where("organizationId", toObjectId(organizationId));
780
+ }
781
+ /**
782
+ * Filter by user
783
+ */
784
+ forUser(userId) {
785
+ return this.where("userId", toObjectId(userId));
786
+ }
787
+ /**
788
+ * Filter by status(es)
789
+ */
790
+ withStatus(...statuses) {
791
+ if (statuses.length === 1) {
792
+ return this.where("status", statuses[0]);
793
+ }
794
+ return this.whereIn("status", statuses);
795
+ }
796
+ /**
797
+ * Filter active employees
798
+ */
799
+ active() {
800
+ return this.withStatus("active");
801
+ }
802
+ /**
803
+ * Filter employed employees (not terminated)
804
+ */
805
+ employed() {
806
+ return this.whereIn("status", ["active", "on_leave", "suspended"]);
807
+ }
808
+ /**
809
+ * Filter terminated employees
810
+ */
811
+ terminated() {
812
+ return this.withStatus("terminated");
813
+ }
814
+ /**
815
+ * Filter by department
816
+ */
817
+ inDepartment(department) {
818
+ return this.where("department", department);
819
+ }
820
+ /**
821
+ * Filter by position
822
+ */
823
+ inPosition(position) {
824
+ return this.where("position", position);
825
+ }
826
+ /**
827
+ * Filter by employment type
828
+ */
829
+ withEmploymentType(type) {
830
+ return this.where("employmentType", type);
831
+ }
832
+ /**
833
+ * Filter by hire date (after)
834
+ */
835
+ hiredAfter(date) {
836
+ return this.whereGte("hireDate", date);
837
+ }
838
+ /**
839
+ * Filter by hire date (before)
840
+ */
841
+ hiredBefore(date) {
842
+ return this.whereLte("hireDate", date);
843
+ }
844
+ /**
845
+ * Filter by minimum salary
846
+ */
847
+ withMinSalary(amount) {
848
+ return this.whereGte("compensation.netSalary", amount);
849
+ }
850
+ /**
851
+ * Filter by maximum salary
852
+ */
853
+ withMaxSalary(amount) {
854
+ return this.whereLte("compensation.netSalary", amount);
855
+ }
856
+ /**
857
+ * Filter by salary range
858
+ */
859
+ withSalaryRange(min, max) {
860
+ return this.whereBetween("compensation.netSalary", min, max);
861
+ }
862
+ };
863
+ var PayrollQueryBuilder = class extends QueryBuilder {
864
+ /**
865
+ * Filter by organization
866
+ */
867
+ forOrganization(organizationId) {
868
+ return this.where("organizationId", toObjectId(organizationId));
869
+ }
870
+ /**
871
+ * Filter by employee
872
+ */
873
+ forEmployee(employeeId) {
874
+ return this.where("employeeId", toObjectId(employeeId));
875
+ }
876
+ /**
877
+ * Filter by period
878
+ */
879
+ forPeriod(month, year) {
880
+ if (month !== void 0) {
881
+ this.where("period.month", month);
882
+ }
883
+ if (year !== void 0) {
884
+ this.where("period.year", year);
885
+ }
886
+ return this;
887
+ }
888
+ /**
889
+ * Filter by status(es)
890
+ */
891
+ withStatus(...statuses) {
892
+ if (statuses.length === 1) {
893
+ return this.where("status", statuses[0]);
894
+ }
895
+ return this.whereIn("status", statuses);
896
+ }
897
+ /**
898
+ * Filter paid records
899
+ */
900
+ paid() {
901
+ return this.withStatus("paid");
902
+ }
903
+ /**
904
+ * Filter pending records
905
+ */
906
+ pending() {
907
+ return this.whereIn("status", ["pending", "processing"]);
908
+ }
909
+ /**
910
+ * Filter by date range
911
+ */
912
+ inDateRange(start, end) {
913
+ return this.whereBetween("period.payDate", start, end);
914
+ }
915
+ /**
916
+ * Filter exported records
917
+ */
918
+ exported() {
919
+ return this.where("exported", true);
920
+ }
921
+ /**
922
+ * Filter not exported records
923
+ */
924
+ notExported() {
925
+ return this.where("exported", false);
926
+ }
927
+ };
928
+ function employee() {
929
+ return new EmployeeQueryBuilder();
930
+ }
931
+ function payroll() {
932
+ return new PayrollQueryBuilder();
933
+ }
934
+
935
+ // src/utils/calculation.ts
936
+ function sumBy(items, getter) {
937
+ return items.reduce((total, item) => total + getter(item), 0);
938
+ }
939
+ function sumAllowances(allowances) {
940
+ return sumBy(allowances, (a) => a.amount);
941
+ }
942
+ function sumDeductions(deductions) {
943
+ return sumBy(deductions, (d) => d.amount);
944
+ }
945
+ function calculateGross(baseAmount, allowances) {
946
+ return baseAmount + sumAllowances(allowances);
947
+ }
948
+ function calculateNet(gross, deductions) {
949
+ return Math.max(0, gross - sumDeductions(deductions));
950
+ }
951
+ function applyTaxBrackets(amount, brackets) {
952
+ let tax = 0;
953
+ for (const bracket of brackets) {
954
+ if (amount > bracket.min) {
955
+ const taxableAmount = Math.min(amount, bracket.max) - bracket.min;
956
+ tax += taxableAmount * bracket.rate;
957
+ }
958
+ }
959
+ return Math.round(tax);
960
+ }
961
+
962
+ // src/errors/index.ts
963
+ var PayrollError = class _PayrollError extends Error {
964
+ code;
965
+ status;
966
+ context;
967
+ timestamp;
968
+ constructor(message, code = "PAYROLL_ERROR", status = 500, context) {
969
+ super(message);
970
+ this.name = "PayrollError";
971
+ this.code = code;
972
+ this.status = status;
973
+ this.context = context;
974
+ this.timestamp = /* @__PURE__ */ new Date();
975
+ if (Error.captureStackTrace) {
976
+ Error.captureStackTrace(this, _PayrollError);
977
+ }
978
+ }
979
+ toJSON() {
980
+ return {
981
+ name: this.name,
982
+ code: this.code,
983
+ message: this.message,
984
+ status: this.status,
985
+ context: this.context,
986
+ timestamp: this.timestamp.toISOString()
987
+ };
988
+ }
989
+ };
990
+ var NotInitializedError = class extends PayrollError {
991
+ constructor(message = "Payroll not initialized. Call Payroll.initialize() first.") {
992
+ super(message, "NOT_INITIALIZED", 500);
993
+ this.name = "NotInitializedError";
994
+ }
995
+ };
996
+ var EmployeeNotFoundError = class extends PayrollError {
997
+ constructor(employeeId, context) {
998
+ super(
999
+ employeeId ? `Employee not found: ${employeeId}` : "Employee not found",
1000
+ "EMPLOYEE_NOT_FOUND",
1001
+ 404,
1002
+ context
1003
+ );
1004
+ this.name = "EmployeeNotFoundError";
1005
+ }
1006
+ };
1007
+ var DuplicatePayrollError = class extends PayrollError {
1008
+ constructor(employeeId, month, year, context) {
1009
+ super(
1010
+ `Payroll already processed for employee ${employeeId} in ${month}/${year}`,
1011
+ "DUPLICATE_PAYROLL",
1012
+ 409,
1013
+ { employeeId, month, year, ...context }
1014
+ );
1015
+ this.name = "DuplicatePayrollError";
1016
+ }
1017
+ };
1018
+ var ValidationError = class extends PayrollError {
1019
+ errors;
1020
+ constructor(errors, context) {
1021
+ const errorArray = Array.isArray(errors) ? errors : [errors];
1022
+ super(errorArray.join(", "), "VALIDATION_ERROR", 400, context);
1023
+ this.name = "ValidationError";
1024
+ this.errors = errorArray;
1025
+ }
1026
+ };
1027
+ var EmployeeTerminatedError = class extends PayrollError {
1028
+ constructor(employeeId, context) {
1029
+ super(
1030
+ employeeId ? `Cannot perform operation on terminated employee: ${employeeId}` : "Cannot perform operation on terminated employee",
1031
+ "EMPLOYEE_TERMINATED",
1032
+ 400,
1033
+ context
1034
+ );
1035
+ this.name = "EmployeeTerminatedError";
1036
+ }
1037
+ };
1038
+ var NotEligibleError = class extends PayrollError {
1039
+ constructor(message, context) {
1040
+ super(message, "NOT_ELIGIBLE", 400, context);
1041
+ this.name = "NotEligibleError";
1042
+ }
1043
+ };
1044
+
1045
+ // src/payroll.ts
1046
+ function hasPluginMethod(obj, method) {
1047
+ return typeof obj === "object" && obj !== null && typeof obj[method] === "function";
1048
+ }
1049
+ function assertPluginMethod(obj, method, context) {
1050
+ if (!hasPluginMethod(obj, method)) {
1051
+ throw new Error(
1052
+ `Method '${method}' not found on employee. Did you forget to apply employeePlugin to your Employee schema? Context: ${context}`
1053
+ );
1054
+ }
1055
+ }
1056
+ function isEffectiveForPeriod(item, periodStart, periodEnd) {
1057
+ const effectiveFrom = item.effectiveFrom ? new Date(item.effectiveFrom) : /* @__PURE__ */ new Date(0);
1058
+ const effectiveTo = item.effectiveTo ? new Date(item.effectiveTo) : /* @__PURE__ */ new Date("2099-12-31");
1059
+ return effectiveFrom <= periodEnd && effectiveTo >= periodStart;
1060
+ }
1061
+ var Payroll = class _Payroll {
1062
+ _container;
1063
+ _events;
1064
+ _plugins = null;
1065
+ _initialized = false;
1066
+ constructor() {
1067
+ this._container = Container.getInstance();
1068
+ this._events = createEventBus();
1069
+ }
1070
+ // ========================================
1071
+ // Initialization
1072
+ // ========================================
1073
+ /**
1074
+ * Initialize Payroll with models and configuration
1075
+ */
1076
+ initialize(config) {
1077
+ const { EmployeeModel, PayrollRecordModel, TransactionModel, AttendanceModel, singleTenant, logger: customLogger, config: customConfig } = config;
1078
+ if (!EmployeeModel || !PayrollRecordModel || !TransactionModel) {
1079
+ throw new Error("EmployeeModel, PayrollRecordModel, and TransactionModel are required");
1080
+ }
1081
+ if (customLogger) {
1082
+ setLogger(customLogger);
1083
+ }
1084
+ initializeContainer({
1085
+ models: {
1086
+ EmployeeModel,
1087
+ PayrollRecordModel,
1088
+ TransactionModel,
1089
+ AttendanceModel: AttendanceModel ?? null
1090
+ },
1091
+ config: customConfig,
1092
+ singleTenant: singleTenant ?? null,
1093
+ logger: customLogger
1094
+ });
1095
+ const pluginContext = {
1096
+ payroll: this,
1097
+ events: this._events,
1098
+ logger: getLogger(),
1099
+ getConfig: (key) => {
1100
+ const config2 = this._container.getConfig();
1101
+ return config2[key];
1102
+ },
1103
+ addHook: (event, handler) => this._events.on(event, handler)
1104
+ };
1105
+ this._plugins = new PluginManager(pluginContext);
1106
+ this._initialized = true;
1107
+ getLogger().info("Payroll initialized", {
1108
+ hasAttendanceIntegration: !!AttendanceModel,
1109
+ isSingleTenant: !!singleTenant
1110
+ });
1111
+ return this;
1112
+ }
1113
+ /**
1114
+ * Check if initialized
1115
+ */
1116
+ isInitialized() {
1117
+ return this._initialized;
1118
+ }
1119
+ /**
1120
+ * Ensure initialized
1121
+ */
1122
+ ensureInitialized() {
1123
+ if (!this._initialized) {
1124
+ throw new NotInitializedError();
1125
+ }
1126
+ }
1127
+ /**
1128
+ * Get models
1129
+ */
1130
+ get models() {
1131
+ this.ensureInitialized();
1132
+ return this._container.getModels();
1133
+ }
1134
+ /**
1135
+ * Get config
1136
+ */
1137
+ get config() {
1138
+ return this._container.getConfig();
1139
+ }
1140
+ // ========================================
1141
+ // Plugin System
1142
+ // ========================================
1143
+ /**
1144
+ * Register a plugin
1145
+ */
1146
+ async use(plugin) {
1147
+ this.ensureInitialized();
1148
+ await this._plugins.register(plugin);
1149
+ return this;
1150
+ }
1151
+ /**
1152
+ * Subscribe to events
1153
+ */
1154
+ on(event, handler) {
1155
+ return this._events.on(event, handler);
1156
+ }
1157
+ // ========================================
1158
+ // Employment Lifecycle
1159
+ // ========================================
1160
+ /**
1161
+ * Hire a new employee
1162
+ */
1163
+ async hire(params) {
1164
+ this.ensureInitialized();
1165
+ const { userId, employment, compensation, bankDetails, context } = params;
1166
+ const session = context?.session;
1167
+ const organizationId = params.organizationId ?? this._container.getOrganizationId();
1168
+ if (!organizationId) {
1169
+ throw new Error("organizationId is required (or configure single-tenant mode)");
1170
+ }
1171
+ const existingQuery = employee().forUser(userId).forOrganization(organizationId).employed().build();
1172
+ let existing = this.models.EmployeeModel.findOne(existingQuery);
1173
+ if (session) existing = existing.session(session);
1174
+ if (await existing) {
1175
+ throw new Error("User is already an active employee in this organization");
1176
+ }
1177
+ const employeeData = EmployeeFactory.create({
1178
+ userId,
1179
+ organizationId,
1180
+ employment,
1181
+ compensation: {
1182
+ ...compensation,
1183
+ currency: compensation.currency || this.config.payroll.defaultCurrency
1184
+ },
1185
+ bankDetails
1186
+ });
1187
+ const [employee2] = await this.models.EmployeeModel.create([employeeData], { session });
1188
+ this._events.emitSync("employee:hired", {
1189
+ employee: {
1190
+ id: employee2._id,
1191
+ employeeId: employee2.employeeId,
1192
+ position: employee2.position,
1193
+ department: employee2.department
1194
+ },
1195
+ organizationId: employee2.organizationId,
1196
+ context
1197
+ });
1198
+ getLogger().info("Employee hired", {
1199
+ employeeId: employee2.employeeId,
1200
+ organizationId: organizationId.toString(),
1201
+ position: employment.position
1202
+ });
1203
+ return employee2;
1204
+ }
1205
+ /**
1206
+ * Update employment details
1207
+ * NOTE: Status changes to 'terminated' must use terminate() method
1208
+ */
1209
+ async updateEmployment(params) {
1210
+ this.ensureInitialized();
1211
+ const { employeeId, updates, context } = params;
1212
+ const session = context?.session;
1213
+ let query = this.models.EmployeeModel.findById(toObjectId(employeeId));
1214
+ if (session) query = query.session(session);
1215
+ const employee2 = await query;
1216
+ if (!employee2) {
1217
+ throw new EmployeeNotFoundError(employeeId.toString());
1218
+ }
1219
+ if (employee2.status === "terminated") {
1220
+ throw new EmployeeTerminatedError(employee2.employeeId);
1221
+ }
1222
+ if (updates.status === "terminated") {
1223
+ throw new ValidationError(
1224
+ "Cannot set status to terminated directly. Use the terminate() method instead to ensure proper history tracking.",
1225
+ { field: "status" }
1226
+ );
1227
+ }
1228
+ const allowedUpdates = ["department", "position", "employmentType", "status", "workSchedule"];
1229
+ for (const [key, value] of Object.entries(updates)) {
1230
+ if (allowedUpdates.includes(key)) {
1231
+ employee2[key] = value;
1232
+ }
1233
+ }
1234
+ await employee2.save({ session });
1235
+ getLogger().info("Employee updated", {
1236
+ employeeId: employee2.employeeId,
1237
+ updates: Object.keys(updates)
1238
+ });
1239
+ return employee2;
1240
+ }
1241
+ /**
1242
+ * Terminate employee
1243
+ */
1244
+ async terminate(params) {
1245
+ this.ensureInitialized();
1246
+ const { employeeId, terminationDate = /* @__PURE__ */ new Date(), reason = "resignation", notes, context } = params;
1247
+ const session = context?.session;
1248
+ let query = this.models.EmployeeModel.findById(toObjectId(employeeId));
1249
+ if (session) query = query.session(session);
1250
+ const employee2 = await query;
1251
+ if (!employee2) {
1252
+ throw new EmployeeNotFoundError(employeeId.toString());
1253
+ }
1254
+ assertPluginMethod(employee2, "terminate", "terminate()");
1255
+ employee2.terminate(reason, terminationDate);
1256
+ if (notes) {
1257
+ employee2.notes = (employee2.notes || "") + `
1258
+ Termination: ${notes}`;
1259
+ }
1260
+ await employee2.save({ session });
1261
+ this._events.emitSync("employee:terminated", {
1262
+ employee: {
1263
+ id: employee2._id,
1264
+ employeeId: employee2.employeeId
1265
+ },
1266
+ terminationDate,
1267
+ reason,
1268
+ organizationId: employee2.organizationId,
1269
+ context
1270
+ });
1271
+ getLogger().info("Employee terminated", {
1272
+ employeeId: employee2.employeeId,
1273
+ reason
1274
+ });
1275
+ return employee2;
1276
+ }
1277
+ /**
1278
+ * Re-hire terminated employee
1279
+ */
1280
+ async reHire(params) {
1281
+ this.ensureInitialized();
1282
+ const { employeeId, hireDate = /* @__PURE__ */ new Date(), position, department, compensation, context } = params;
1283
+ const session = context?.session;
1284
+ if (!this.config.employment.allowReHiring) {
1285
+ throw new Error("Re-hiring is not enabled");
1286
+ }
1287
+ let query = this.models.EmployeeModel.findById(toObjectId(employeeId));
1288
+ if (session) query = query.session(session);
1289
+ const employee2 = await query;
1290
+ if (!employee2) {
1291
+ throw new EmployeeNotFoundError(employeeId.toString());
1292
+ }
1293
+ assertPluginMethod(employee2, "reHire", "reHire()");
1294
+ employee2.reHire(hireDate, position, department);
1295
+ if (compensation) {
1296
+ employee2.compensation = { ...employee2.compensation, ...compensation };
1297
+ }
1298
+ await employee2.save({ session });
1299
+ this._events.emitSync("employee:rehired", {
1300
+ employee: {
1301
+ id: employee2._id,
1302
+ employeeId: employee2.employeeId,
1303
+ position: employee2.position
1304
+ },
1305
+ organizationId: employee2.organizationId,
1306
+ context
1307
+ });
1308
+ getLogger().info("Employee re-hired", {
1309
+ employeeId: employee2.employeeId
1310
+ });
1311
+ return employee2;
1312
+ }
1313
+ /**
1314
+ * Get employee by ID
1315
+ */
1316
+ async getEmployee(params) {
1317
+ this.ensureInitialized();
1318
+ const { employeeId, populateUser = true, session } = params;
1319
+ let query = this.models.EmployeeModel.findById(toObjectId(employeeId));
1320
+ if (session) query = query.session(session);
1321
+ if (populateUser) query = query.populate("userId", "name email phone");
1322
+ const employee2 = await query;
1323
+ if (!employee2) {
1324
+ throw new EmployeeNotFoundError(employeeId.toString());
1325
+ }
1326
+ return employee2;
1327
+ }
1328
+ /**
1329
+ * List employees
1330
+ */
1331
+ async listEmployees(params) {
1332
+ this.ensureInitialized();
1333
+ const { organizationId, filters = {}, pagination = {} } = params;
1334
+ let queryBuilder = employee().forOrganization(organizationId);
1335
+ if (filters.status) queryBuilder = queryBuilder.withStatus(filters.status);
1336
+ if (filters.department) queryBuilder = queryBuilder.inDepartment(filters.department);
1337
+ if (filters.employmentType) queryBuilder = queryBuilder.withEmploymentType(filters.employmentType);
1338
+ if (filters.minSalary) queryBuilder = queryBuilder.withMinSalary(filters.minSalary);
1339
+ if (filters.maxSalary) queryBuilder = queryBuilder.withMaxSalary(filters.maxSalary);
1340
+ const query = queryBuilder.build();
1341
+ const page = pagination.page || 1;
1342
+ const limit = pagination.limit || 20;
1343
+ const sort = pagination.sort || { createdAt: -1 };
1344
+ const [docs, totalDocs] = await Promise.all([
1345
+ this.models.EmployeeModel.find(query).populate("userId", "name email phone").sort(sort).skip((page - 1) * limit).limit(limit),
1346
+ this.models.EmployeeModel.countDocuments(query)
1347
+ ]);
1348
+ return { docs, totalDocs, page, limit };
1349
+ }
1350
+ // ========================================
1351
+ // Compensation Management
1352
+ // ========================================
1353
+ /**
1354
+ * Update employee salary
1355
+ */
1356
+ async updateSalary(params) {
1357
+ this.ensureInitialized();
1358
+ const { employeeId, compensation, effectiveFrom = /* @__PURE__ */ new Date(), context } = params;
1359
+ const session = context?.session;
1360
+ let query = this.models.EmployeeModel.findById(toObjectId(employeeId));
1361
+ if (session) query = query.session(session);
1362
+ const employee2 = await query;
1363
+ if (!employee2) {
1364
+ throw new EmployeeNotFoundError(employeeId.toString());
1365
+ }
1366
+ if (employee2.status === "terminated") {
1367
+ throw new EmployeeTerminatedError(employee2.employeeId);
1368
+ }
1369
+ const oldSalary = employee2.compensation.netSalary;
1370
+ if (compensation.baseAmount !== void 0) {
1371
+ employee2.compensation.baseAmount = compensation.baseAmount;
1372
+ }
1373
+ if (compensation.frequency) {
1374
+ employee2.compensation.frequency = compensation.frequency;
1375
+ }
1376
+ if (compensation.currency) {
1377
+ employee2.compensation.currency = compensation.currency;
1378
+ }
1379
+ employee2.compensation.effectiveFrom = effectiveFrom;
1380
+ if (hasPluginMethod(employee2, "updateSalaryCalculations")) {
1381
+ employee2.updateSalaryCalculations();
1382
+ }
1383
+ await employee2.save({ session });
1384
+ this._events.emitSync("salary:updated", {
1385
+ employee: { id: employee2._id, employeeId: employee2.employeeId },
1386
+ previousSalary: oldSalary || 0,
1387
+ newSalary: employee2.compensation.netSalary || 0,
1388
+ effectiveFrom,
1389
+ organizationId: employee2.organizationId,
1390
+ context
1391
+ });
1392
+ getLogger().info("Salary updated", {
1393
+ employeeId: employee2.employeeId,
1394
+ oldSalary,
1395
+ newSalary: employee2.compensation.netSalary
1396
+ });
1397
+ return employee2;
1398
+ }
1399
+ /**
1400
+ * Add allowance to employee
1401
+ */
1402
+ async addAllowance(params) {
1403
+ this.ensureInitialized();
1404
+ const { employeeId, type, amount, taxable = true, recurring = true, effectiveFrom = /* @__PURE__ */ new Date(), effectiveTo, context } = params;
1405
+ const session = context?.session;
1406
+ let query = this.models.EmployeeModel.findById(toObjectId(employeeId));
1407
+ if (session) query = query.session(session);
1408
+ const employee2 = await query;
1409
+ if (!employee2) {
1410
+ throw new EmployeeNotFoundError(employeeId.toString());
1411
+ }
1412
+ if (employee2.status === "terminated") {
1413
+ throw new EmployeeTerminatedError(employee2.employeeId);
1414
+ }
1415
+ if (!employee2.compensation.allowances) {
1416
+ employee2.compensation.allowances = [];
1417
+ }
1418
+ employee2.compensation.allowances.push({
1419
+ type,
1420
+ name: type,
1421
+ amount,
1422
+ taxable,
1423
+ recurring,
1424
+ effectiveFrom,
1425
+ effectiveTo
1426
+ });
1427
+ if (hasPluginMethod(employee2, "updateSalaryCalculations")) {
1428
+ employee2.updateSalaryCalculations();
1429
+ }
1430
+ await employee2.save({ session });
1431
+ getLogger().info("Allowance added", {
1432
+ employeeId: employee2.employeeId,
1433
+ type,
1434
+ amount
1435
+ });
1436
+ return employee2;
1437
+ }
1438
+ /**
1439
+ * Remove allowance from employee
1440
+ */
1441
+ async removeAllowance(params) {
1442
+ this.ensureInitialized();
1443
+ const { employeeId, type, context } = params;
1444
+ const session = context?.session;
1445
+ let query = this.models.EmployeeModel.findById(toObjectId(employeeId));
1446
+ if (session) query = query.session(session);
1447
+ const employee2 = await query;
1448
+ if (!employee2) {
1449
+ throw new EmployeeNotFoundError(employeeId.toString());
1450
+ }
1451
+ const before = employee2.compensation.allowances?.length || 0;
1452
+ if (hasPluginMethod(employee2, "removeAllowance")) {
1453
+ employee2.removeAllowance(type);
1454
+ } else {
1455
+ if (employee2.compensation.allowances) {
1456
+ employee2.compensation.allowances = employee2.compensation.allowances.filter(
1457
+ (a) => a.type !== type
1458
+ );
1459
+ }
1460
+ }
1461
+ const after = employee2.compensation.allowances?.length || 0;
1462
+ if (before === after) {
1463
+ throw new Error(`Allowance type '${type}' not found`);
1464
+ }
1465
+ await employee2.save({ session });
1466
+ getLogger().info("Allowance removed", {
1467
+ employeeId: employee2.employeeId,
1468
+ type
1469
+ });
1470
+ return employee2;
1471
+ }
1472
+ /**
1473
+ * Add deduction to employee
1474
+ */
1475
+ async addDeduction(params) {
1476
+ this.ensureInitialized();
1477
+ const { employeeId, type, amount, auto = false, recurring = true, description, effectiveFrom = /* @__PURE__ */ new Date(), effectiveTo, context } = params;
1478
+ const session = context?.session;
1479
+ let query = this.models.EmployeeModel.findById(toObjectId(employeeId));
1480
+ if (session) query = query.session(session);
1481
+ const employee2 = await query;
1482
+ if (!employee2) {
1483
+ throw new EmployeeNotFoundError(employeeId.toString());
1484
+ }
1485
+ if (employee2.status === "terminated") {
1486
+ throw new EmployeeTerminatedError(employee2.employeeId);
1487
+ }
1488
+ if (!employee2.compensation.deductions) {
1489
+ employee2.compensation.deductions = [];
1490
+ }
1491
+ employee2.compensation.deductions.push({
1492
+ type,
1493
+ name: type,
1494
+ amount,
1495
+ auto,
1496
+ recurring,
1497
+ description,
1498
+ effectiveFrom,
1499
+ effectiveTo
1500
+ });
1501
+ if (hasPluginMethod(employee2, "updateSalaryCalculations")) {
1502
+ employee2.updateSalaryCalculations();
1503
+ }
1504
+ await employee2.save({ session });
1505
+ getLogger().info("Deduction added", {
1506
+ employeeId: employee2.employeeId,
1507
+ type,
1508
+ amount,
1509
+ auto
1510
+ });
1511
+ return employee2;
1512
+ }
1513
+ /**
1514
+ * Remove deduction from employee
1515
+ */
1516
+ async removeDeduction(params) {
1517
+ this.ensureInitialized();
1518
+ const { employeeId, type, context } = params;
1519
+ const session = context?.session;
1520
+ let query = this.models.EmployeeModel.findById(toObjectId(employeeId));
1521
+ if (session) query = query.session(session);
1522
+ const employee2 = await query;
1523
+ if (!employee2) {
1524
+ throw new EmployeeNotFoundError(employeeId.toString());
1525
+ }
1526
+ const before = employee2.compensation.deductions?.length || 0;
1527
+ if (hasPluginMethod(employee2, "removeDeduction")) {
1528
+ employee2.removeDeduction(type);
1529
+ } else {
1530
+ if (employee2.compensation.deductions) {
1531
+ employee2.compensation.deductions = employee2.compensation.deductions.filter(
1532
+ (d) => d.type !== type
1533
+ );
1534
+ }
1535
+ }
1536
+ const after = employee2.compensation.deductions?.length || 0;
1537
+ if (before === after) {
1538
+ throw new Error(`Deduction type '${type}' not found`);
1539
+ }
1540
+ await employee2.save({ session });
1541
+ getLogger().info("Deduction removed", {
1542
+ employeeId: employee2.employeeId,
1543
+ type
1544
+ });
1545
+ return employee2;
1546
+ }
1547
+ /**
1548
+ * Update bank details
1549
+ */
1550
+ async updateBankDetails(params) {
1551
+ this.ensureInitialized();
1552
+ const { employeeId, bankDetails, context } = params;
1553
+ const session = context?.session;
1554
+ let query = this.models.EmployeeModel.findById(toObjectId(employeeId));
1555
+ if (session) query = query.session(session);
1556
+ const employee2 = await query;
1557
+ if (!employee2) {
1558
+ throw new EmployeeNotFoundError(employeeId.toString());
1559
+ }
1560
+ employee2.bankDetails = { ...employee2.bankDetails, ...bankDetails };
1561
+ await employee2.save({ session });
1562
+ getLogger().info("Bank details updated", {
1563
+ employeeId: employee2.employeeId
1564
+ });
1565
+ return employee2;
1566
+ }
1567
+ // ========================================
1568
+ // Payroll Processing
1569
+ // ========================================
1570
+ /**
1571
+ * Process salary for single employee
1572
+ *
1573
+ * ATOMICITY: This method creates its own transaction if none provided.
1574
+ * All database operations (PayrollRecord, Transaction, Employee stats)
1575
+ * are atomic - either all succeed or all fail.
1576
+ */
1577
+ async processSalary(params) {
1578
+ this.ensureInitialized();
1579
+ const { employeeId, month, year, paymentDate = /* @__PURE__ */ new Date(), paymentMethod = "bank", context } = params;
1580
+ const providedSession = context?.session;
1581
+ const session = providedSession || await mongoose2.startSession();
1582
+ const shouldManageTransaction = !providedSession && session != null;
1583
+ try {
1584
+ if (shouldManageTransaction) {
1585
+ await session.startTransaction();
1586
+ }
1587
+ let query = this.models.EmployeeModel.findById(toObjectId(employeeId)).populate("userId", "name email");
1588
+ if (session) query = query.session(session);
1589
+ const employee2 = await query;
1590
+ if (!employee2) {
1591
+ throw new EmployeeNotFoundError(employeeId.toString());
1592
+ }
1593
+ const canReceive = hasPluginMethod(employee2, "canReceiveSalary") ? employee2.canReceiveSalary() : employee2.status === "active" && (employee2.compensation?.baseAmount || 0) > 0;
1594
+ if (!canReceive) {
1595
+ throw new NotEligibleError("Employee is not eligible to receive salary");
1596
+ }
1597
+ const existingQuery = payroll().forEmployee(employeeId).forPeriod(month, year).whereIn("status", ["paid", "processing"]).build();
1598
+ let existingRecordQuery = this.models.PayrollRecordModel.findOne(existingQuery);
1599
+ if (session) existingRecordQuery = existingRecordQuery.session(session);
1600
+ const existingRecord = await existingRecordQuery;
1601
+ if (existingRecord) {
1602
+ throw new DuplicatePayrollError(employee2.employeeId, month, year);
1603
+ }
1604
+ const period = { ...getPayPeriod(month, year), payDate: paymentDate };
1605
+ const breakdown = await this.calculateSalaryBreakdown(employee2, period, session);
1606
+ const userIdValue = employee2.userId ? typeof employee2.userId === "object" && "_id" in employee2.userId ? employee2.userId._id : employee2.userId : void 0;
1607
+ const [payrollRecord] = await this.models.PayrollRecordModel.create([{
1608
+ organizationId: employee2.organizationId,
1609
+ employeeId: employee2._id,
1610
+ userId: userIdValue,
1611
+ period,
1612
+ breakdown,
1613
+ status: "processing",
1614
+ paymentMethod,
1615
+ processedAt: /* @__PURE__ */ new Date(),
1616
+ processedBy: context?.userId ? toObjectId(context.userId) : void 0
1617
+ }], session ? { session } : {});
1618
+ const [transaction] = await this.models.TransactionModel.create([{
1619
+ organizationId: employee2.organizationId,
1620
+ type: "expense",
1621
+ category: HRM_TRANSACTION_CATEGORIES.SALARY,
1622
+ amount: breakdown.netSalary,
1623
+ method: paymentMethod,
1624
+ status: "completed",
1625
+ date: paymentDate,
1626
+ referenceId: employee2._id,
1627
+ referenceModel: "Employee",
1628
+ handledBy: context?.userId ? toObjectId(context.userId) : void 0,
1629
+ notes: `Salary payment - ${employee2.userId?.name || employee2.employeeId} (${month}/${year})`,
1630
+ metadata: {
1631
+ employeeId: employee2.employeeId,
1632
+ payrollRecordId: payrollRecord._id,
1633
+ period: { month, year },
1634
+ breakdown: {
1635
+ base: breakdown.baseAmount,
1636
+ allowances: sumAllowances(breakdown.allowances),
1637
+ deductions: sumDeductions(breakdown.deductions),
1638
+ tax: breakdown.taxAmount || 0,
1639
+ gross: breakdown.grossSalary,
1640
+ net: breakdown.netSalary
1641
+ }
1642
+ }
1643
+ }], session ? { session } : {});
1644
+ payrollRecord.transactionId = transaction._id;
1645
+ payrollRecord.status = "paid";
1646
+ payrollRecord.paidAt = paymentDate;
1647
+ await payrollRecord.save(session ? { session } : {});
1648
+ await this.updatePayrollStats(employee2, breakdown.netSalary, paymentDate, session);
1649
+ if (shouldManageTransaction) {
1650
+ await session.commitTransaction();
1651
+ }
1652
+ this._events.emitSync("salary:processed", {
1653
+ employee: {
1654
+ id: employee2._id,
1655
+ employeeId: employee2.employeeId,
1656
+ name: employee2.userId?.name
1657
+ },
1658
+ payroll: {
1659
+ id: payrollRecord._id,
1660
+ period: { month, year },
1661
+ grossAmount: breakdown.grossSalary,
1662
+ netAmount: breakdown.netSalary
1663
+ },
1664
+ transactionId: transaction._id,
1665
+ organizationId: employee2.organizationId,
1666
+ context
1667
+ });
1668
+ getLogger().info("Salary processed", {
1669
+ employeeId: employee2.employeeId,
1670
+ month,
1671
+ year,
1672
+ amount: breakdown.netSalary
1673
+ });
1674
+ return { payrollRecord, transaction, employee: employee2 };
1675
+ } catch (error) {
1676
+ if (shouldManageTransaction && session?.inTransaction()) {
1677
+ await session.abortTransaction();
1678
+ }
1679
+ throw error;
1680
+ } finally {
1681
+ if (shouldManageTransaction && session) {
1682
+ await session.endSession();
1683
+ }
1684
+ }
1685
+ }
1686
+ /**
1687
+ * Process bulk payroll
1688
+ *
1689
+ * ATOMICITY STRATEGY: Each employee is processed in its own transaction.
1690
+ * This allows partial success - some employees can succeed while others fail.
1691
+ * Failed employees don't affect successful ones.
1692
+ */
1693
+ async processBulkPayroll(params) {
1694
+ this.ensureInitialized();
1695
+ const { organizationId, month, year, employeeIds = [], paymentDate = /* @__PURE__ */ new Date(), paymentMethod = "bank", context } = params;
1696
+ const query = { organizationId: toObjectId(organizationId), status: "active" };
1697
+ if (employeeIds.length > 0) {
1698
+ query._id = { $in: employeeIds.map(toObjectId) };
1699
+ }
1700
+ const employees = await this.models.EmployeeModel.find(query);
1701
+ const results = {
1702
+ successful: [],
1703
+ failed: [],
1704
+ total: employees.length
1705
+ };
1706
+ for (const employee2 of employees) {
1707
+ try {
1708
+ const result = await this.processSalary({
1709
+ employeeId: employee2._id,
1710
+ month,
1711
+ year,
1712
+ paymentDate,
1713
+ paymentMethod,
1714
+ context: { ...context, session: void 0 }
1715
+ // Don't pass session - let processSalary create its own
1716
+ });
1717
+ results.successful.push({
1718
+ employeeId: employee2.employeeId,
1719
+ amount: result.payrollRecord.breakdown.netSalary,
1720
+ transactionId: result.transaction._id
1721
+ });
1722
+ } catch (error) {
1723
+ results.failed.push({
1724
+ employeeId: employee2.employeeId,
1725
+ error: error.message
1726
+ });
1727
+ getLogger().error("Failed to process salary", {
1728
+ employeeId: employee2.employeeId,
1729
+ error: error.message
1730
+ });
1731
+ }
1732
+ }
1733
+ this._events.emitSync("payroll:completed", {
1734
+ organizationId: toObjectId(organizationId),
1735
+ period: { month, year },
1736
+ summary: {
1737
+ total: results.total,
1738
+ successful: results.successful.length,
1739
+ failed: results.failed.length,
1740
+ totalAmount: results.successful.reduce((sum, r) => sum + r.amount, 0)
1741
+ },
1742
+ context
1743
+ });
1744
+ getLogger().info("Bulk payroll processed", {
1745
+ organizationId: organizationId.toString(),
1746
+ month,
1747
+ year,
1748
+ total: results.total,
1749
+ successful: results.successful.length,
1750
+ failed: results.failed.length
1751
+ });
1752
+ return results;
1753
+ }
1754
+ /**
1755
+ * Get payroll history
1756
+ */
1757
+ async payrollHistory(params) {
1758
+ this.ensureInitialized();
1759
+ const { employeeId, organizationId, month, year, status, pagination = {} } = params;
1760
+ let queryBuilder = payroll();
1761
+ if (employeeId) queryBuilder = queryBuilder.forEmployee(employeeId);
1762
+ if (organizationId) queryBuilder = queryBuilder.forOrganization(organizationId);
1763
+ if (month || year) queryBuilder = queryBuilder.forPeriod(month, year);
1764
+ if (status) queryBuilder = queryBuilder.withStatus(status);
1765
+ const query = queryBuilder.build();
1766
+ const page = pagination.page || 1;
1767
+ const limit = pagination.limit || 20;
1768
+ const sort = pagination.sort || { "period.year": -1, "period.month": -1 };
1769
+ 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);
1770
+ }
1771
+ /**
1772
+ * Get payroll summary
1773
+ */
1774
+ async payrollSummary(params) {
1775
+ this.ensureInitialized();
1776
+ const { organizationId, month, year } = params;
1777
+ const query = { organizationId: toObjectId(organizationId) };
1778
+ if (month) query["period.month"] = month;
1779
+ if (year) query["period.year"] = year;
1780
+ const [summary] = await this.models.PayrollRecordModel.aggregate([
1781
+ { $match: query },
1782
+ {
1783
+ $group: {
1784
+ _id: null,
1785
+ totalGross: { $sum: "$breakdown.grossSalary" },
1786
+ totalNet: { $sum: "$breakdown.netSalary" },
1787
+ totalDeductions: { $sum: { $sum: "$breakdown.deductions.amount" } },
1788
+ totalTax: { $sum: { $ifNull: ["$breakdown.taxAmount", 0] } },
1789
+ employeeCount: { $sum: 1 },
1790
+ paidCount: { $sum: { $cond: [{ $eq: ["$status", "paid"] }, 1, 0] } },
1791
+ pendingCount: { $sum: { $cond: [{ $eq: ["$status", "pending"] }, 1, 0] } }
1792
+ }
1793
+ }
1794
+ ]);
1795
+ return summary || {
1796
+ totalGross: 0,
1797
+ totalNet: 0,
1798
+ totalDeductions: 0,
1799
+ totalTax: 0,
1800
+ employeeCount: 0,
1801
+ paidCount: 0,
1802
+ pendingCount: 0
1803
+ };
1804
+ }
1805
+ /**
1806
+ * Export payroll data
1807
+ */
1808
+ async exportPayroll(params) {
1809
+ this.ensureInitialized();
1810
+ const { organizationId, startDate, endDate } = params;
1811
+ const query = {
1812
+ organizationId: toObjectId(organizationId),
1813
+ "period.payDate": { $gte: startDate, $lte: endDate }
1814
+ };
1815
+ 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 });
1816
+ await this.models.PayrollRecordModel.updateMany(query, {
1817
+ exported: true,
1818
+ exportedAt: /* @__PURE__ */ new Date()
1819
+ });
1820
+ this._events.emitSync("payroll:exported", {
1821
+ organizationId: toObjectId(organizationId),
1822
+ dateRange: { start: startDate, end: endDate },
1823
+ recordCount: records.length,
1824
+ format: "json"
1825
+ });
1826
+ getLogger().info("Payroll data exported", {
1827
+ organizationId: organizationId.toString(),
1828
+ count: records.length
1829
+ });
1830
+ return records;
1831
+ }
1832
+ // ========================================
1833
+ // Helper Methods
1834
+ // ========================================
1835
+ /**
1836
+ * Calculate salary breakdown with proper handling for:
1837
+ * - Effective dates on allowances/deductions
1838
+ * - Pro-rating for mid-period hires AND terminations
1839
+ * - Tax calculation
1840
+ * - Working days vs calendar days for attendance
1841
+ */
1842
+ async calculateSalaryBreakdown(employee2, period, session) {
1843
+ const comp = employee2.compensation;
1844
+ let baseAmount = comp.baseAmount;
1845
+ const workingDaysInMonth = getWorkingDaysInMonth(period.year, period.month);
1846
+ const proRating = this.calculateProRatingAdvanced(
1847
+ employee2.hireDate,
1848
+ employee2.terminationDate || null,
1849
+ period.startDate,
1850
+ period.endDate,
1851
+ workingDaysInMonth
1852
+ );
1853
+ if (proRating.isProRated && this.config.payroll.allowProRating) {
1854
+ baseAmount = Math.round(baseAmount * proRating.ratio);
1855
+ }
1856
+ const effectiveAllowances = (comp.allowances || []).filter((a) => isEffectiveForPeriod(a, period.startDate, period.endDate)).filter((a) => a.recurring !== false);
1857
+ const effectiveDeductions = (comp.deductions || []).filter((d) => isEffectiveForPeriod(d, period.startDate, period.endDate)).filter((d) => d.auto || d.recurring);
1858
+ const allowances = effectiveAllowances.map((a) => ({
1859
+ type: a.type,
1860
+ amount: proRating.isProRated && this.config.payroll.allowProRating ? Math.round(a.amount * proRating.ratio) : a.amount,
1861
+ taxable: a.taxable ?? true
1862
+ }));
1863
+ const deductions = effectiveDeductions.map((d) => ({
1864
+ type: d.type,
1865
+ amount: proRating.isProRated && this.config.payroll.allowProRating ? Math.round(d.amount * proRating.ratio) : d.amount,
1866
+ description: d.description
1867
+ }));
1868
+ let attendanceDeduction = 0;
1869
+ if (this.models.AttendanceModel && this.config.payroll.attendanceIntegration) {
1870
+ attendanceDeduction = await this.calculateAttendanceDeduction(
1871
+ employee2._id,
1872
+ employee2.organizationId,
1873
+ period,
1874
+ baseAmount / proRating.workingDays,
1875
+ // Daily rate based on working days
1876
+ proRating.workingDays,
1877
+ session
1878
+ );
1879
+ }
1880
+ if (attendanceDeduction > 0) {
1881
+ deductions.push({
1882
+ type: "absence",
1883
+ amount: attendanceDeduction,
1884
+ description: "Unpaid leave deduction"
1885
+ });
1886
+ }
1887
+ const grossSalary = calculateGross(baseAmount, allowances);
1888
+ const taxableAllowances = allowances.filter((a) => a.taxable);
1889
+ const taxableAmount = baseAmount + sumAllowances(taxableAllowances);
1890
+ let taxAmount = 0;
1891
+ const currency = comp.currency || this.config.payroll.defaultCurrency;
1892
+ const taxBrackets = TAX_BRACKETS[currency] || [];
1893
+ if (taxBrackets.length > 0 && this.config.payroll.autoDeductions) {
1894
+ const annualTaxable = taxableAmount * 12;
1895
+ const annualTax = applyTaxBrackets(annualTaxable, taxBrackets);
1896
+ taxAmount = Math.round(annualTax / 12);
1897
+ }
1898
+ if (taxAmount > 0) {
1899
+ deductions.push({
1900
+ type: "tax",
1901
+ amount: taxAmount,
1902
+ description: "Income tax"
1903
+ });
1904
+ }
1905
+ const netSalary = calculateNet(grossSalary, deductions);
1906
+ return {
1907
+ baseAmount,
1908
+ allowances,
1909
+ deductions,
1910
+ grossSalary,
1911
+ netSalary,
1912
+ taxableAmount,
1913
+ taxAmount,
1914
+ workingDays: proRating.workingDays,
1915
+ actualDays: proRating.actualDays,
1916
+ proRatedAmount: proRating.isProRated ? baseAmount : 0,
1917
+ attendanceDeduction
1918
+ };
1919
+ }
1920
+ /**
1921
+ * Advanced pro-rating calculation that handles:
1922
+ * - Mid-period hires
1923
+ * - Mid-period terminations
1924
+ * - Working days (not calendar days)
1925
+ */
1926
+ calculateProRatingAdvanced(hireDate, terminationDate, periodStart, periodEnd, workingDaysInMonth) {
1927
+ const hire = new Date(hireDate);
1928
+ const termination = terminationDate ? new Date(terminationDate) : null;
1929
+ const effectiveStart = hire > periodStart ? hire : periodStart;
1930
+ const effectiveEnd = termination && termination < periodEnd ? termination : periodEnd;
1931
+ if (effectiveStart > periodEnd || termination && termination < periodStart) {
1932
+ return {
1933
+ isProRated: true,
1934
+ ratio: 0,
1935
+ workingDays: workingDaysInMonth,
1936
+ actualDays: 0
1937
+ };
1938
+ }
1939
+ const totalDays = diffInDays(periodStart, periodEnd) + 1;
1940
+ const actualDays = diffInDays(effectiveStart, effectiveEnd) + 1;
1941
+ const ratio = actualDays / totalDays;
1942
+ const actualWorkingDays = Math.round(workingDaysInMonth * ratio);
1943
+ const isProRated = hire > periodStart || termination !== null && termination < periodEnd;
1944
+ return {
1945
+ isProRated,
1946
+ ratio,
1947
+ workingDays: workingDaysInMonth,
1948
+ actualDays: actualWorkingDays
1949
+ };
1950
+ }
1951
+ /**
1952
+ * Calculate attendance deduction using working days (not calendar days)
1953
+ */
1954
+ async calculateAttendanceDeduction(employeeId, organizationId, period, dailyRate, expectedWorkingDays, session) {
1955
+ try {
1956
+ if (!this.models.AttendanceModel) return 0;
1957
+ let query = this.models.AttendanceModel.findOne({
1958
+ tenantId: organizationId,
1959
+ targetId: employeeId,
1960
+ targetModel: "Employee",
1961
+ year: period.year,
1962
+ month: period.month
1963
+ });
1964
+ if (session) query = query.session(session);
1965
+ const attendance = await query;
1966
+ if (!attendance) return 0;
1967
+ const workedDays = attendance.totalWorkDays || 0;
1968
+ const absentDays = Math.max(0, expectedWorkingDays - workedDays);
1969
+ return Math.round(absentDays * dailyRate);
1970
+ } catch (error) {
1971
+ getLogger().warn("Failed to calculate attendance deduction", {
1972
+ employeeId: employeeId.toString(),
1973
+ error: error.message
1974
+ });
1975
+ return 0;
1976
+ }
1977
+ }
1978
+ async updatePayrollStats(employee2, amount, paymentDate, session) {
1979
+ if (!employee2.payrollStats) {
1980
+ employee2.payrollStats = {
1981
+ totalPaid: 0,
1982
+ paymentsThisYear: 0,
1983
+ averageMonthly: 0
1984
+ };
1985
+ }
1986
+ employee2.payrollStats.totalPaid = (employee2.payrollStats.totalPaid || 0) + amount;
1987
+ employee2.payrollStats.lastPaymentDate = paymentDate;
1988
+ employee2.payrollStats.paymentsThisYear = (employee2.payrollStats.paymentsThisYear || 0) + 1;
1989
+ employee2.payrollStats.averageMonthly = Math.round(
1990
+ employee2.payrollStats.totalPaid / employee2.payrollStats.paymentsThisYear
1991
+ );
1992
+ employee2.payrollStats.nextPaymentDate = addMonths(paymentDate, 1);
1993
+ await employee2.save(session ? { session } : {});
1994
+ }
1995
+ // ========================================
1996
+ // Static Factory
1997
+ // ========================================
1998
+ /**
1999
+ * Create a new Payroll instance
2000
+ */
2001
+ static create() {
2002
+ return new _Payroll();
2003
+ }
2004
+ };
2005
+ var PayrollBuilder = class {
2006
+ _models = null;
2007
+ _config;
2008
+ _singleTenant = null;
2009
+ _logger;
2010
+ /**
2011
+ * Set models
2012
+ */
2013
+ withModels(models) {
2014
+ this._models = models;
2015
+ return this;
2016
+ }
2017
+ /**
2018
+ * Set config overrides
2019
+ */
2020
+ withConfig(config) {
2021
+ this._config = config;
2022
+ return this;
2023
+ }
2024
+ /**
2025
+ * Enable single-tenant mode
2026
+ *
2027
+ * Use this when building a single-organization HRM (no organizationId needed)
2028
+ *
2029
+ * @example
2030
+ * ```typescript
2031
+ * const payroll = createPayrollInstance()
2032
+ * .withModels({ EmployeeModel, PayrollRecordModel, TransactionModel })
2033
+ * .withSingleTenant({ organizationId: 'my-company' })
2034
+ * .build();
2035
+ * ```
2036
+ */
2037
+ withSingleTenant(config) {
2038
+ this._singleTenant = config;
2039
+ return this;
2040
+ }
2041
+ /**
2042
+ * Enable single-tenant mode (shorthand)
2043
+ *
2044
+ * Alias for withSingleTenant() - consistent with @classytic/clockin API
2045
+ *
2046
+ * @example
2047
+ * ```typescript
2048
+ * const payroll = createPayrollInstance()
2049
+ * .withModels({ ... })
2050
+ * .forSingleTenant() // ← No organizationId needed!
2051
+ * .build();
2052
+ * ```
2053
+ */
2054
+ forSingleTenant(config = {}) {
2055
+ return this.withSingleTenant(config);
2056
+ }
2057
+ /**
2058
+ * Set custom logger
2059
+ */
2060
+ withLogger(logger) {
2061
+ this._logger = logger;
2062
+ return this;
2063
+ }
2064
+ /**
2065
+ * Build and initialize Payroll instance
2066
+ */
2067
+ build() {
2068
+ if (!this._models) {
2069
+ throw new Error("Models are required. Call withModels() first.");
2070
+ }
2071
+ const payroll3 = new Payroll();
2072
+ payroll3.initialize({
2073
+ EmployeeModel: this._models.EmployeeModel,
2074
+ PayrollRecordModel: this._models.PayrollRecordModel,
2075
+ TransactionModel: this._models.TransactionModel,
2076
+ AttendanceModel: this._models.AttendanceModel,
2077
+ config: this._config,
2078
+ singleTenant: this._singleTenant,
2079
+ logger: this._logger
2080
+ });
2081
+ return payroll3;
2082
+ }
2083
+ };
2084
+ function createPayrollInstance() {
2085
+ return new PayrollBuilder();
2086
+ }
2087
+ var payrollInstance = null;
2088
+ function getPayroll() {
2089
+ if (!payrollInstance) {
2090
+ payrollInstance = new Payroll();
2091
+ }
2092
+ return payrollInstance;
2093
+ }
2094
+ function resetPayroll() {
2095
+ payrollInstance = null;
2096
+ Container.resetInstance();
2097
+ }
2098
+ var payroll2 = getPayroll();
2099
+ var payroll_default = payroll2;
2100
+
2101
+ export { Payroll, PayrollBuilder, createPayrollInstance, payroll_default as default, getPayroll, payroll2 as payroll, resetPayroll };
2102
+ //# sourceMappingURL=payroll.js.map
2103
+ //# sourceMappingURL=payroll.js.map