@classytic/payroll 1.0.0 → 2.7.5

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 (40) hide show
  1. package/README.md +525 -574
  2. package/dist/calculators/index.d.ts +300 -0
  3. package/dist/calculators/index.js +304 -0
  4. package/dist/calculators/index.js.map +1 -0
  5. package/dist/employee-identity-Cq2wo9-2.d.ts +490 -0
  6. package/dist/index-DjB72l6e.d.ts +3742 -0
  7. package/dist/index.d.ts +2924 -0
  8. package/dist/index.js +10648 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/prorating.calculator-C7sdFiG2.d.ts +135 -0
  11. package/dist/schemas/index.d.ts +4 -0
  12. package/dist/schemas/index.js +1452 -0
  13. package/dist/schemas/index.js.map +1 -0
  14. package/dist/types-BVDjiVGS.d.ts +1856 -0
  15. package/dist/utils/index.d.ts +995 -0
  16. package/dist/utils/index.js +1629 -0
  17. package/dist/utils/index.js.map +1 -0
  18. package/package.json +77 -24
  19. package/src/config.js +0 -177
  20. package/src/core/compensation.manager.js +0 -242
  21. package/src/core/employment.manager.js +0 -224
  22. package/src/core/payroll.manager.js +0 -499
  23. package/src/enums.js +0 -141
  24. package/src/factories/compensation.factory.js +0 -198
  25. package/src/factories/employee.factory.js +0 -173
  26. package/src/factories/payroll.factory.js +0 -247
  27. package/src/hrm.orchestrator.js +0 -139
  28. package/src/index.js +0 -172
  29. package/src/init.js +0 -41
  30. package/src/models/payroll-record.model.js +0 -126
  31. package/src/plugins/employee.plugin.js +0 -157
  32. package/src/schemas/employment.schema.js +0 -126
  33. package/src/services/compensation.service.js +0 -231
  34. package/src/services/employee.service.js +0 -162
  35. package/src/services/payroll.service.js +0 -213
  36. package/src/utils/calculation.utils.js +0 -91
  37. package/src/utils/date.utils.js +0 -120
  38. package/src/utils/logger.js +0 -36
  39. package/src/utils/query-builders.js +0 -185
  40. package/src/utils/validation.utils.js +0 -122
@@ -0,0 +1,1452 @@
1
+ import mongoose, { Schema } from 'mongoose';
2
+
3
+ // src/schemas/index.ts
4
+
5
+ // src/enums.ts
6
+ var EMPLOYMENT_TYPE = {
7
+ FULL_TIME: "full_time",
8
+ PART_TIME: "part_time",
9
+ CONTRACT: "contract",
10
+ INTERN: "intern",
11
+ CONSULTANT: "consultant"
12
+ };
13
+ var EMPLOYMENT_TYPE_VALUES = Object.values(EMPLOYMENT_TYPE);
14
+ var EMPLOYEE_STATUS = {
15
+ ACTIVE: "active",
16
+ ON_LEAVE: "on_leave",
17
+ SUSPENDED: "suspended",
18
+ TERMINATED: "terminated"
19
+ };
20
+ var EMPLOYEE_STATUS_VALUES = Object.values(EMPLOYEE_STATUS);
21
+ var DEPARTMENT = {
22
+ MANAGEMENT: "management",
23
+ TRAINING: "training",
24
+ SALES: "sales",
25
+ OPERATIONS: "operations",
26
+ SUPPORT: "support",
27
+ HR: "hr",
28
+ MAINTENANCE: "maintenance",
29
+ MARKETING: "marketing",
30
+ FINANCE: "finance",
31
+ IT: "it"
32
+ };
33
+ var DEPARTMENT_VALUES = Object.values(DEPARTMENT);
34
+ var PAYMENT_FREQUENCY = {
35
+ MONTHLY: "monthly",
36
+ BI_WEEKLY: "bi_weekly",
37
+ WEEKLY: "weekly",
38
+ HOURLY: "hourly",
39
+ DAILY: "daily"
40
+ };
41
+ var PAYMENT_FREQUENCY_VALUES = Object.values(PAYMENT_FREQUENCY);
42
+ var PAYMENT_METHOD = {
43
+ BANK: "bank",
44
+ CASH: "cash",
45
+ MOBILE: "mobile",
46
+ BKASH: "bkash",
47
+ NAGAD: "nagad",
48
+ ROCKET: "rocket",
49
+ CHECK: "check"
50
+ };
51
+ var PAYMENT_METHOD_VALUES = Object.values(PAYMENT_METHOD);
52
+ var ALLOWANCE_TYPE = {
53
+ HOUSING: "housing",
54
+ TRANSPORT: "transport",
55
+ MEAL: "meal",
56
+ MOBILE: "mobile",
57
+ MEDICAL: "medical",
58
+ EDUCATION: "education",
59
+ BONUS: "bonus",
60
+ OTHER: "other"
61
+ };
62
+ var ALLOWANCE_TYPE_VALUES = Object.values(ALLOWANCE_TYPE);
63
+ var DEDUCTION_TYPE = {
64
+ TAX: "tax",
65
+ LOAN: "loan",
66
+ ADVANCE: "advance",
67
+ PROVIDENT_FUND: "provident_fund",
68
+ INSURANCE: "insurance",
69
+ ABSENCE: "absence",
70
+ OTHER: "other"
71
+ };
72
+ var DEDUCTION_TYPE_VALUES = Object.values(DEDUCTION_TYPE);
73
+ var PAYROLL_STATUS = {
74
+ PENDING: "pending",
75
+ PROCESSING: "processing",
76
+ PAID: "paid",
77
+ FAILED: "failed",
78
+ VOIDED: "voided",
79
+ REVERSED: "reversed"
80
+ };
81
+ var PAYROLL_STATUS_VALUES = Object.values(PAYROLL_STATUS);
82
+ var TERMINATION_REASON = {
83
+ RESIGNATION: "resignation",
84
+ RETIREMENT: "retirement",
85
+ TERMINATION: "termination",
86
+ CONTRACT_END: "contract_end",
87
+ MUTUAL_AGREEMENT: "mutual_agreement",
88
+ OTHER: "other"
89
+ };
90
+ var TERMINATION_REASON_VALUES = Object.values(TERMINATION_REASON);
91
+ var LEAVE_TYPE = {
92
+ ANNUAL: "annual",
93
+ SICK: "sick",
94
+ UNPAID: "unpaid",
95
+ MATERNITY: "maternity",
96
+ PATERNITY: "paternity",
97
+ BEREAVEMENT: "bereavement",
98
+ COMPENSATORY: "compensatory",
99
+ OTHER: "other"
100
+ };
101
+ var LEAVE_TYPE_VALUES = Object.values(LEAVE_TYPE);
102
+ var LEAVE_REQUEST_STATUS = {
103
+ PENDING: "pending",
104
+ APPROVED: "approved",
105
+ REJECTED: "rejected",
106
+ CANCELLED: "cancelled"
107
+ };
108
+ var LEAVE_REQUEST_STATUS_VALUES = Object.values(LEAVE_REQUEST_STATUS);
109
+ var TAX_TYPE = {
110
+ INCOME_TAX: "income_tax",
111
+ SOCIAL_SECURITY: "social_security",
112
+ HEALTH_INSURANCE: "health_insurance",
113
+ PENSION: "pension",
114
+ EMPLOYMENT_INSURANCE: "employment_insurance",
115
+ LOCAL_TAX: "local_tax",
116
+ OTHER: "other"
117
+ };
118
+ var TAX_TYPE_VALUES = Object.values(TAX_TYPE);
119
+ var TAX_STATUS = {
120
+ PENDING: "pending",
121
+ SUBMITTED: "submitted",
122
+ PAID: "paid",
123
+ CANCELLED: "cancelled"
124
+ };
125
+ var TAX_STATUS_VALUES = Object.values(TAX_STATUS);
126
+
127
+ // src/config.ts
128
+ var HRM_CONFIG = {
129
+ dataRetention: {
130
+ /**
131
+ * Default retention period for payroll records in seconds
132
+ *
133
+ * STANDARD APPROACH: expireAt field + configurable TTL index
134
+ *
135
+ * ## How It Works:
136
+ * 1. Set expireAt date on each payroll record
137
+ * 2. Call PayrollRecord.configureRetention() at app startup
138
+ * 3. MongoDB deletes documents when expireAt is reached
139
+ *
140
+ * ## Usage:
141
+ *
142
+ * @example Configure at initialization
143
+ * ```typescript
144
+ * await payroll.init({ ... });
145
+ * await PayrollRecord.configureRetention(0); // 0 = delete when expireAt reached
146
+ * ```
147
+ *
148
+ * @example Set expireAt per record
149
+ * ```typescript
150
+ * const expireAt = PayrollRecord.calculateExpireAt(7); // 7 years
151
+ * await PayrollRecord.updateOne({ _id }, { expireAt });
152
+ * ```
153
+ *
154
+ * ## Jurisdiction Requirements:
155
+ * - USA: 7 years → 220752000 seconds
156
+ * - EU/UK: 6 years → 189216000 seconds
157
+ * - Germany: 10 years → 315360000 seconds
158
+ * - India: 8 years → 252288000 seconds
159
+ *
160
+ * Set to 0 to disable TTL
161
+ */
162
+ payrollRecordsTTL: 63072e3}};
163
+ var ORG_ROLES = {
164
+ OWNER: {
165
+ key: "owner",
166
+ label: "Owner",
167
+ description: "Full organization access (set by Organization model)"
168
+ },
169
+ MANAGER: {
170
+ key: "manager",
171
+ label: "Manager",
172
+ description: "Management and administrative features"
173
+ },
174
+ TRAINER: {
175
+ key: "trainer",
176
+ label: "Trainer",
177
+ description: "Training and coaching features"
178
+ },
179
+ STAFF: {
180
+ key: "staff",
181
+ label: "Staff",
182
+ description: "General staff access to basic features"
183
+ },
184
+ INTERN: {
185
+ key: "intern",
186
+ label: "Intern",
187
+ description: "Limited access for interns"
188
+ },
189
+ CONSULTANT: {
190
+ key: "consultant",
191
+ label: "Consultant",
192
+ description: "Project-based consultant access"
193
+ }
194
+ };
195
+ Object.values(ORG_ROLES).map((role) => role.key);
196
+ var periodSchema = new Schema(
197
+ {
198
+ month: { type: Number, required: true, min: 1, max: 12 },
199
+ year: { type: Number, required: true, min: 2020 },
200
+ startDate: { type: Date, required: true },
201
+ endDate: { type: Date, required: true },
202
+ payDate: { type: Date, required: true }
203
+ },
204
+ { _id: false }
205
+ );
206
+
207
+ // src/utils/logger.ts
208
+ var createConsoleLogger = () => ({
209
+ info: (message, meta) => {
210
+ if (meta) {
211
+ console.log(`[Payroll] INFO: ${message}`, meta);
212
+ } else {
213
+ console.log(`[Payroll] INFO: ${message}`);
214
+ }
215
+ },
216
+ error: (message, meta) => {
217
+ if (meta) {
218
+ console.error(`[Payroll] ERROR: ${message}`, meta);
219
+ } else {
220
+ console.error(`[Payroll] ERROR: ${message}`);
221
+ }
222
+ },
223
+ warn: (message, meta) => {
224
+ if (meta) {
225
+ console.warn(`[Payroll] WARN: ${message}`, meta);
226
+ } else {
227
+ console.warn(`[Payroll] WARN: ${message}`);
228
+ }
229
+ },
230
+ debug: (message, meta) => {
231
+ if (process.env.NODE_ENV !== "production") {
232
+ if (meta) {
233
+ console.log(`[Payroll] DEBUG: ${message}`, meta);
234
+ } else {
235
+ console.log(`[Payroll] DEBUG: ${message}`);
236
+ }
237
+ }
238
+ }
239
+ });
240
+ var currentLogger = createConsoleLogger();
241
+ var logger = {
242
+ info: (message, meta) => {
243
+ currentLogger.info(message, meta);
244
+ },
245
+ error: (message, meta) => {
246
+ currentLogger.error(message, meta);
247
+ },
248
+ warn: (message, meta) => {
249
+ currentLogger.warn(message, meta);
250
+ },
251
+ debug: (message, meta) => {
252
+ currentLogger.debug(message, meta);
253
+ }
254
+ };
255
+
256
+ // src/models/leave-request.model.ts
257
+ var leaveRequestSchema = new Schema(
258
+ {
259
+ organizationId: {
260
+ type: Schema.Types.ObjectId,
261
+ required: false,
262
+ // Optional for single-tenant mode
263
+ ref: "Organization"
264
+ },
265
+ employeeId: {
266
+ type: Schema.Types.ObjectId,
267
+ required: true,
268
+ ref: "Employee"
269
+ },
270
+ userId: {
271
+ type: Schema.Types.ObjectId,
272
+ required: false,
273
+ // Optional for guest employees
274
+ ref: "User"
275
+ },
276
+ type: {
277
+ type: String,
278
+ enum: LEAVE_TYPE_VALUES,
279
+ required: true
280
+ },
281
+ startDate: {
282
+ type: Date,
283
+ required: true,
284
+ validate: {
285
+ validator: function(value) {
286
+ return !this.endDate || value <= this.endDate;
287
+ },
288
+ message: "Start date must be before or equal to end date"
289
+ }
290
+ },
291
+ endDate: {
292
+ type: Date,
293
+ required: true,
294
+ validate: {
295
+ validator: function(value) {
296
+ return !this.startDate || value >= this.startDate;
297
+ },
298
+ message: "End date must be after or equal to start date"
299
+ }
300
+ },
301
+ days: {
302
+ type: Number,
303
+ required: true,
304
+ min: [0.5, "Days must be at least 0.5"]
305
+ },
306
+ halfDay: { type: Boolean, default: false },
307
+ reason: String,
308
+ status: {
309
+ type: String,
310
+ enum: LEAVE_REQUEST_STATUS_VALUES,
311
+ default: "pending"
312
+ },
313
+ reviewedBy: { type: Schema.Types.ObjectId, ref: "User" },
314
+ reviewedAt: Date,
315
+ reviewNotes: String,
316
+ attachments: [String],
317
+ metadata: { type: Schema.Types.Mixed, default: {} }
318
+ },
319
+ { timestamps: true }
320
+ );
321
+ leaveRequestSchema.virtual("isPending").get(function() {
322
+ return this.status === LEAVE_REQUEST_STATUS.PENDING;
323
+ });
324
+ leaveRequestSchema.virtual("isApproved").get(function() {
325
+ return this.status === LEAVE_REQUEST_STATUS.APPROVED;
326
+ });
327
+ leaveRequestSchema.virtual("isRejected").get(function() {
328
+ return this.status === LEAVE_REQUEST_STATUS.REJECTED;
329
+ });
330
+ leaveRequestSchema.virtual("isCancelled").get(function() {
331
+ return this.status === LEAVE_REQUEST_STATUS.CANCELLED;
332
+ });
333
+ leaveRequestSchema.methods.approve = function(reviewerId, notes) {
334
+ if (this.status !== LEAVE_REQUEST_STATUS.PENDING) {
335
+ throw new Error("Can only approve pending requests");
336
+ }
337
+ this.status = LEAVE_REQUEST_STATUS.APPROVED;
338
+ this.reviewedBy = reviewerId;
339
+ this.reviewedAt = /* @__PURE__ */ new Date();
340
+ if (notes) this.reviewNotes = notes;
341
+ logger.info("Leave request approved", {
342
+ requestId: this._id.toString(),
343
+ employeeId: this.employeeId.toString(),
344
+ type: this.type,
345
+ days: this.days
346
+ });
347
+ };
348
+ leaveRequestSchema.methods.reject = function(reviewerId, notes) {
349
+ if (this.status !== LEAVE_REQUEST_STATUS.PENDING) {
350
+ throw new Error("Can only reject pending requests");
351
+ }
352
+ this.status = LEAVE_REQUEST_STATUS.REJECTED;
353
+ this.reviewedBy = reviewerId;
354
+ this.reviewedAt = /* @__PURE__ */ new Date();
355
+ if (notes) this.reviewNotes = notes;
356
+ logger.info("Leave request rejected", {
357
+ requestId: this._id.toString(),
358
+ employeeId: this.employeeId.toString(),
359
+ type: this.type,
360
+ days: this.days
361
+ });
362
+ };
363
+ leaveRequestSchema.methods.cancel = function() {
364
+ if (this.status !== LEAVE_REQUEST_STATUS.PENDING) {
365
+ throw new Error("Can only cancel pending requests");
366
+ }
367
+ this.status = LEAVE_REQUEST_STATUS.CANCELLED;
368
+ logger.info("Leave request cancelled", {
369
+ requestId: this._id.toString(),
370
+ employeeId: this.employeeId.toString(),
371
+ type: this.type,
372
+ days: this.days
373
+ });
374
+ };
375
+ leaveRequestSchema.statics.findByEmployee = function(employeeId, options = {}) {
376
+ const query = { employeeId };
377
+ if (options.status) query.status = options.status;
378
+ if (options.year) {
379
+ query.startDate = {
380
+ $gte: new Date(options.year, 0, 1),
381
+ $lt: new Date(options.year + 1, 0, 1)
382
+ };
383
+ }
384
+ return this.find(query).sort({ startDate: -1 }).limit(options.limit || 50);
385
+ };
386
+ leaveRequestSchema.statics.findPendingByOrganization = function(organizationId) {
387
+ const query = {
388
+ status: LEAVE_REQUEST_STATUS.PENDING
389
+ };
390
+ if (organizationId) {
391
+ query.organizationId = organizationId;
392
+ }
393
+ return this.find(query).sort({ createdAt: -1 });
394
+ };
395
+ leaveRequestSchema.statics.findByPeriod = function(organizationId, startDate, endDate, options = {}) {
396
+ const query = {
397
+ $or: [
398
+ { startDate: { $gte: startDate, $lte: endDate } },
399
+ { endDate: { $gte: startDate, $lte: endDate } },
400
+ {
401
+ startDate: { $lte: startDate },
402
+ endDate: { $gte: endDate }
403
+ }
404
+ ]
405
+ };
406
+ if (organizationId) {
407
+ query.organizationId = organizationId;
408
+ }
409
+ if (options.status) query.status = options.status;
410
+ if (options.type) query.type = options.type;
411
+ return this.find(query).sort({ startDate: 1 });
412
+ };
413
+ leaveRequestSchema.statics.getLeaveStats = function(employeeId, year) {
414
+ return this.aggregate([
415
+ {
416
+ $match: {
417
+ employeeId,
418
+ status: LEAVE_REQUEST_STATUS.APPROVED,
419
+ startDate: {
420
+ $gte: new Date(year, 0, 1),
421
+ $lt: new Date(year + 1, 0, 1)
422
+ }
423
+ }
424
+ },
425
+ {
426
+ $group: {
427
+ _id: "$type",
428
+ totalDays: { $sum: "$days" },
429
+ count: { $sum: 1 }
430
+ }
431
+ }
432
+ ]).then(
433
+ (results) => results
434
+ );
435
+ };
436
+ leaveRequestSchema.statics.getOrganizationSummary = function(organizationId, year) {
437
+ const matchStage = {
438
+ startDate: {
439
+ $gte: new Date(year, 0, 1),
440
+ $lt: new Date(year + 1, 0, 1)
441
+ }
442
+ };
443
+ if (organizationId) {
444
+ matchStage.organizationId = organizationId;
445
+ }
446
+ return this.aggregate([
447
+ {
448
+ $match: matchStage
449
+ },
450
+ {
451
+ $group: {
452
+ _id: { status: "$status", type: "$type" },
453
+ totalDays: { $sum: "$days" },
454
+ count: { $sum: 1 }
455
+ }
456
+ }
457
+ ]);
458
+ };
459
+ leaveRequestSchema.statics.findOverlapping = function(employeeId, startDate, endDate, excludeRequestId) {
460
+ const query = {
461
+ employeeId,
462
+ status: { $in: [LEAVE_REQUEST_STATUS.PENDING, LEAVE_REQUEST_STATUS.APPROVED] },
463
+ // Overlapping condition: new request overlaps with existing
464
+ startDate: { $lte: endDate },
465
+ endDate: { $gte: startDate }
466
+ };
467
+ if (excludeRequestId) {
468
+ query._id = { $ne: excludeRequestId };
469
+ }
470
+ return this.find(query).sort({ startDate: 1 });
471
+ };
472
+ leaveRequestSchema.statics.hasOverlap = async function(employeeId, startDate, endDate, excludeRequestId) {
473
+ const query = {
474
+ employeeId,
475
+ status: { $in: [LEAVE_REQUEST_STATUS.PENDING, LEAVE_REQUEST_STATUS.APPROVED] },
476
+ startDate: { $lte: endDate },
477
+ endDate: { $gte: startDate }
478
+ };
479
+ if (excludeRequestId) {
480
+ query._id = { $ne: excludeRequestId };
481
+ }
482
+ const count = await this.countDocuments(query);
483
+ return count > 0;
484
+ };
485
+ function getLeaveRequestModel(connection = mongoose.connection) {
486
+ const modelName = "LeaveRequest";
487
+ if (connection.models[modelName]) {
488
+ return connection.models[modelName];
489
+ }
490
+ return connection.model(
491
+ modelName,
492
+ leaveRequestSchema
493
+ );
494
+ }
495
+
496
+ // src/schemas/leave.ts
497
+ var leaveBalanceSchema = new Schema(
498
+ {
499
+ type: {
500
+ type: String,
501
+ enum: LEAVE_TYPE_VALUES,
502
+ required: true
503
+ },
504
+ allocated: { type: Number, default: 0, min: 0 },
505
+ used: { type: Number, default: 0, min: 0 },
506
+ pending: { type: Number, default: 0, min: 0 },
507
+ carriedOver: { type: Number, default: 0, min: 0 },
508
+ expiresAt: { type: Date },
509
+ year: { type: Number, required: true }
510
+ },
511
+ { _id: false }
512
+ );
513
+ var leaveBalanceFields = {
514
+ leaveBalances: [leaveBalanceSchema]
515
+ };
516
+ var leaveRequestIndexes = [
517
+ { fields: { organizationId: 1, employeeId: 1, startDate: -1 } },
518
+ { fields: { organizationId: 1, status: 1, createdAt: -1 } },
519
+ { fields: { employeeId: 1, status: 1 } },
520
+ { fields: { organizationId: 1, type: 1, status: 1 } }
521
+ ];
522
+ var leaveRequestTTLIndex = {
523
+ fields: { createdAt: 1 },
524
+ options: {
525
+ expireAfterSeconds: 63072e3,
526
+ // 2 years
527
+ partialFilterExpression: {
528
+ status: { $in: ["approved", "rejected", "cancelled"] }
529
+ }
530
+ }
531
+ };
532
+ function applyLeaveRequestIndexes(schema, options = {}) {
533
+ if (!options.createIndexes) return;
534
+ for (const { fields } of leaveRequestIndexes) {
535
+ schema.index(fields);
536
+ }
537
+ if (options.enableTTL) {
538
+ schema.index(leaveRequestTTLIndex.fields, {
539
+ ...leaveRequestTTLIndex.options,
540
+ expireAfterSeconds: options.ttlSeconds ?? leaveRequestTTLIndex.options.expireAfterSeconds
541
+ });
542
+ }
543
+ }
544
+ function getLeaveRequestFields() {
545
+ const paths = leaveRequestSchema.paths;
546
+ const fields = {};
547
+ for (const [key, pathObj] of Object.entries(paths)) {
548
+ if (key === "_id" || key === "__v" || key === "createdAt" || key === "updatedAt") {
549
+ continue;
550
+ }
551
+ fields[key] = pathObj.options || {};
552
+ }
553
+ return fields;
554
+ }
555
+
556
+ // src/core/state-machine.ts
557
+ var StateMachine = class {
558
+ constructor(config) {
559
+ this.config = config;
560
+ this.validTransitions = /* @__PURE__ */ new Map();
561
+ for (const state of config.states) {
562
+ this.validTransitions.set(state, /* @__PURE__ */ new Set());
563
+ }
564
+ for (const transition of config.transitions) {
565
+ const fromStates = Array.isArray(transition.from) ? transition.from : [transition.from];
566
+ for (const from of fromStates) {
567
+ this.validTransitions.get(from)?.add(transition.to);
568
+ }
569
+ }
570
+ this.terminalStates = new Set(config.terminal || []);
571
+ }
572
+ validTransitions;
573
+ terminalStates;
574
+ /**
575
+ * Get the initial state
576
+ */
577
+ get initial() {
578
+ return this.config.initial;
579
+ }
580
+ /**
581
+ * Get all valid states
582
+ */
583
+ get states() {
584
+ return this.config.states;
585
+ }
586
+ /**
587
+ * Check if a state is valid
588
+ */
589
+ isValidState(state) {
590
+ return this.config.states.includes(state);
591
+ }
592
+ /**
593
+ * Check if a state is terminal (no outgoing transitions)
594
+ */
595
+ isTerminal(state) {
596
+ return this.terminalStates.has(state);
597
+ }
598
+ /**
599
+ * Check if transition from one state to another is valid
600
+ */
601
+ canTransition(from, to) {
602
+ return this.validTransitions.get(from)?.has(to) ?? false;
603
+ }
604
+ /**
605
+ * Get all valid next states from current state
606
+ */
607
+ getNextStates(from) {
608
+ return Array.from(this.validTransitions.get(from) || []);
609
+ }
610
+ /**
611
+ * Validate a transition and return result
612
+ */
613
+ validateTransition(from, to) {
614
+ if (!this.isValidState(from)) {
615
+ return {
616
+ success: false,
617
+ from,
618
+ to,
619
+ error: `Invalid current state: '${from}'`
620
+ };
621
+ }
622
+ if (!this.isValidState(to)) {
623
+ return {
624
+ success: false,
625
+ from,
626
+ to,
627
+ error: `Invalid target state: '${to}'`
628
+ };
629
+ }
630
+ if (this.isTerminal(from)) {
631
+ return {
632
+ success: false,
633
+ from,
634
+ to,
635
+ error: `Cannot transition from terminal state '${from}'`
636
+ };
637
+ }
638
+ if (!this.canTransition(from, to)) {
639
+ const validNext = this.getNextStates(from);
640
+ return {
641
+ success: false,
642
+ from,
643
+ to,
644
+ error: `Invalid transition: '${from}' \u2192 '${to}'. Valid transitions from '${from}': [${validNext.join(", ")}]`
645
+ };
646
+ }
647
+ return { success: true, from, to };
648
+ }
649
+ /**
650
+ * Assert a transition is valid, throw if not
651
+ */
652
+ assertTransition(from, to) {
653
+ const result = this.validateTransition(from, to);
654
+ if (!result.success) {
655
+ throw new Error(result.error);
656
+ }
657
+ }
658
+ };
659
+ function createStateMachine(config) {
660
+ return new StateMachine(config);
661
+ }
662
+
663
+ // src/core/payroll-states.ts
664
+ createStateMachine({
665
+ states: ["pending", "processing", "paid", "failed", "voided", "reversed"],
666
+ initial: "pending",
667
+ transitions: [
668
+ // Normal flow
669
+ { from: "pending", to: "processing" },
670
+ { from: "processing", to: "paid" },
671
+ // Direct payment (skip processing for single salary)
672
+ { from: "pending", to: "paid" },
673
+ // Failure handling
674
+ { from: "processing", to: "failed" },
675
+ { from: "failed", to: "pending" },
676
+ // Retry
677
+ // Void (unpaid only - pending, processing, or failed)
678
+ { from: ["pending", "processing", "failed"], to: "voided" },
679
+ // Reversal (paid only)
680
+ { from: "paid", to: "reversed" },
681
+ // Restore voided (back to pending for re-processing)
682
+ { from: "voided", to: "pending" }
683
+ ],
684
+ terminal: ["reversed"]
685
+ // Only reversed is truly terminal
686
+ });
687
+ var TaxStatusMachine = createStateMachine({
688
+ states: ["pending", "submitted", "paid", "cancelled"],
689
+ initial: "pending",
690
+ transitions: [
691
+ { from: "pending", to: "submitted" },
692
+ { from: "submitted", to: "paid" },
693
+ // Direct payment (some jurisdictions)
694
+ { from: "pending", to: "paid" },
695
+ // Cancellation (from any non-terminal state)
696
+ { from: ["pending", "submitted"], to: "cancelled" }
697
+ ],
698
+ terminal: ["paid", "cancelled"]
699
+ });
700
+ createStateMachine({
701
+ states: ["pending", "approved", "rejected", "cancelled"],
702
+ initial: "pending",
703
+ transitions: [
704
+ { from: "pending", to: "approved" },
705
+ { from: "pending", to: "rejected" },
706
+ { from: "pending", to: "cancelled" },
707
+ // Cancel approved leave (before it starts)
708
+ { from: "approved", to: "cancelled" }
709
+ ],
710
+ terminal: ["rejected", "cancelled"]
711
+ });
712
+ createStateMachine({
713
+ states: ["active", "on_leave", "suspended", "terminated"],
714
+ initial: "active",
715
+ transitions: [
716
+ // Leave management
717
+ { from: "active", to: "on_leave" },
718
+ { from: "on_leave", to: "active" },
719
+ // Suspension
720
+ { from: ["active", "on_leave"], to: "suspended" },
721
+ { from: "suspended", to: "active" },
722
+ // Termination (from any state)
723
+ { from: ["active", "on_leave", "suspended"], to: "terminated" },
724
+ // Re-hire (back to active)
725
+ { from: "terminated", to: "active" }
726
+ ],
727
+ terminal: []
728
+ // No terminal states (re-hire possible)
729
+ });
730
+
731
+ // src/models/tax-withholding.model.ts
732
+ var taxWithholdingSchema = new Schema(
733
+ {
734
+ organizationId: {
735
+ type: Schema.Types.ObjectId,
736
+ required: true,
737
+ ref: "Organization"
738
+ },
739
+ employeeId: {
740
+ type: Schema.Types.ObjectId,
741
+ required: true,
742
+ ref: "Employee"
743
+ },
744
+ userId: {
745
+ type: Schema.Types.ObjectId,
746
+ required: false,
747
+ ref: "User"
748
+ },
749
+ payrollRecordId: {
750
+ type: Schema.Types.ObjectId,
751
+ required: true,
752
+ ref: "PayrollRecord"
753
+ },
754
+ transactionId: {
755
+ type: Schema.Types.ObjectId,
756
+ required: true,
757
+ ref: "Transaction"
758
+ },
759
+ period: {
760
+ type: periodSchema,
761
+ required: true
762
+ },
763
+ amount: {
764
+ type: Number,
765
+ required: true,
766
+ min: 0
767
+ },
768
+ currency: {
769
+ type: String,
770
+ default: "USD"
771
+ },
772
+ taxType: {
773
+ type: String,
774
+ enum: TAX_TYPE_VALUES,
775
+ required: true
776
+ },
777
+ taxRate: {
778
+ type: Number,
779
+ required: true,
780
+ min: 0,
781
+ max: 1
782
+ },
783
+ taxableAmount: {
784
+ type: Number,
785
+ required: true,
786
+ min: 0
787
+ },
788
+ status: {
789
+ type: String,
790
+ enum: TAX_STATUS_VALUES,
791
+ default: "pending"
792
+ },
793
+ submittedAt: Date,
794
+ paidAt: Date,
795
+ governmentTransactionId: {
796
+ type: Schema.Types.ObjectId,
797
+ ref: "Transaction"
798
+ },
799
+ referenceNumber: String,
800
+ // Void metadata (when payroll is voided/reversed)
801
+ voidedAt: { type: Date },
802
+ voidedBy: { type: Schema.Types.ObjectId, ref: "User" },
803
+ voidReason: { type: String },
804
+ voidMetadata: { type: Schema.Types.Mixed },
805
+ notes: String,
806
+ metadata: { type: Schema.Types.Mixed, default: {} }
807
+ },
808
+ { timestamps: true }
809
+ );
810
+ taxWithholdingSchema.index({ organizationId: 1, status: 1, "period.year": 1, "period.month": 1 });
811
+ taxWithholdingSchema.index({ employeeId: 1, "period.year": -1, "period.month": -1 });
812
+ taxWithholdingSchema.index({ payrollRecordId: 1 });
813
+ taxWithholdingSchema.index({ transactionId: 1 });
814
+ taxWithholdingSchema.index({ organizationId: 1, taxType: 1, status: 1 });
815
+ taxWithholdingSchema.index({ governmentTransactionId: 1 }, { sparse: true });
816
+ taxWithholdingSchema.virtual("isPending").get(function() {
817
+ return this.status === TAX_STATUS.PENDING;
818
+ });
819
+ taxWithholdingSchema.virtual("isPaid").get(function() {
820
+ return this.status === TAX_STATUS.PAID;
821
+ });
822
+ taxWithholdingSchema.virtual("isSubmitted").get(function() {
823
+ return this.status === TAX_STATUS.SUBMITTED;
824
+ });
825
+ taxWithholdingSchema.methods.markAsSubmitted = function(submittedAt = /* @__PURE__ */ new Date()) {
826
+ const transition = TaxStatusMachine.validateTransition(this.status, TAX_STATUS.SUBMITTED);
827
+ if (!transition.success) {
828
+ throw new Error(transition.error);
829
+ }
830
+ this.status = TAX_STATUS.SUBMITTED;
831
+ this.submittedAt = submittedAt;
832
+ logger.info("Tax withholding marked as submitted", {
833
+ withholdingId: this._id.toString(),
834
+ employeeId: this.employeeId.toString(),
835
+ taxType: this.taxType,
836
+ amount: this.amount
837
+ });
838
+ };
839
+ taxWithholdingSchema.methods.markAsPaid = function(transactionId, referenceNumber, paidAt = /* @__PURE__ */ new Date()) {
840
+ const transition = TaxStatusMachine.validateTransition(this.status, TAX_STATUS.PAID);
841
+ if (!transition.success) {
842
+ throw new Error(transition.error);
843
+ }
844
+ this.status = TAX_STATUS.PAID;
845
+ this.governmentTransactionId = transactionId;
846
+ this.referenceNumber = referenceNumber;
847
+ this.paidAt = paidAt;
848
+ logger.info("Tax withholding marked as paid", {
849
+ withholdingId: this._id.toString(),
850
+ employeeId: this.employeeId.toString(),
851
+ taxType: this.taxType,
852
+ amount: this.amount,
853
+ referenceNumber
854
+ });
855
+ };
856
+ taxWithholdingSchema.statics.findByPeriod = function(organizationId, month, year) {
857
+ return this.find({
858
+ organizationId,
859
+ "period.month": month,
860
+ "period.year": year
861
+ });
862
+ };
863
+ taxWithholdingSchema.statics.findByEmployee = function(employeeId, options = {}) {
864
+ const query = { employeeId };
865
+ if (options.year) {
866
+ query["period.year"] = options.year;
867
+ }
868
+ if (options.taxType) {
869
+ query.taxType = options.taxType;
870
+ }
871
+ if (options.status) {
872
+ query.status = options.status;
873
+ }
874
+ return this.find(query).sort({ "period.year": -1, "period.month": -1 }).limit(options.limit || 50);
875
+ };
876
+ taxWithholdingSchema.statics.findPending = function(organizationId, options = {}) {
877
+ const query = {
878
+ organizationId,
879
+ status: TAX_STATUS.PENDING
880
+ };
881
+ if (options.taxType) {
882
+ query.taxType = options.taxType;
883
+ }
884
+ if (options.fromMonth && options.fromYear) {
885
+ query.$or = query.$or || [];
886
+ query.$or.push({
887
+ $and: [
888
+ { "period.year": { $gt: options.fromYear } }
889
+ ]
890
+ });
891
+ query.$or.push({
892
+ $and: [
893
+ { "period.year": options.fromYear },
894
+ { "period.month": { $gte: options.fromMonth } }
895
+ ]
896
+ });
897
+ }
898
+ if (options.toMonth && options.toYear) {
899
+ const existingOr = query.$or;
900
+ delete query.$or;
901
+ query.$and = query.$and || [];
902
+ if (existingOr) {
903
+ query.$and.push({ $or: existingOr });
904
+ }
905
+ query.$and.push({
906
+ $or: [
907
+ { "period.year": { $lt: options.toYear } },
908
+ {
909
+ $and: [
910
+ { "period.year": options.toYear },
911
+ { "period.month": { $lte: options.toMonth } }
912
+ ]
913
+ }
914
+ ]
915
+ });
916
+ }
917
+ return this.find(query).sort({ "period.year": 1, "period.month": 1 });
918
+ };
919
+ taxWithholdingSchema.statics.getSummaryByType = function(organizationId, fromPeriod, toPeriod) {
920
+ return this.aggregate([
921
+ {
922
+ $match: {
923
+ organizationId,
924
+ $or: [
925
+ { "period.year": { $gt: fromPeriod.year } },
926
+ {
927
+ $and: [
928
+ { "period.year": fromPeriod.year },
929
+ { "period.month": { $gte: fromPeriod.month } }
930
+ ]
931
+ }
932
+ ],
933
+ $and: [
934
+ {
935
+ $or: [
936
+ { "period.year": { $lt: toPeriod.year } },
937
+ {
938
+ $and: [
939
+ { "period.year": toPeriod.year },
940
+ { "period.month": { $lte: toPeriod.month } }
941
+ ]
942
+ }
943
+ ]
944
+ }
945
+ ]
946
+ }
947
+ },
948
+ {
949
+ $group: {
950
+ _id: "$taxType",
951
+ totalAmount: { $sum: "$amount" },
952
+ count: { $sum: 1 },
953
+ withholdingIds: { $push: "$_id" }
954
+ }
955
+ },
956
+ {
957
+ $project: {
958
+ _id: 0,
959
+ taxType: "$_id",
960
+ totalAmount: 1,
961
+ count: 1,
962
+ withholdingIds: 1
963
+ }
964
+ }
965
+ ]).then(
966
+ (results) => results.map((r) => ({
967
+ taxType: r.taxType,
968
+ totalAmount: r.totalAmount,
969
+ count: r.count,
970
+ withholdingIds: r.withholdingIds
971
+ }))
972
+ );
973
+ };
974
+ taxWithholdingSchema.statics.getByPayrollRecord = function(payrollRecordId) {
975
+ return this.find({ payrollRecordId });
976
+ };
977
+ taxWithholdingSchema.statics.getTotalByOrganization = function(organizationId, options = {}) {
978
+ const match = { organizationId };
979
+ if (options.status) {
980
+ match.status = options.status;
981
+ }
982
+ if (options.year) {
983
+ match["period.year"] = options.year;
984
+ }
985
+ return this.aggregate([
986
+ { $match: match },
987
+ {
988
+ $group: {
989
+ _id: null,
990
+ totalAmount: { $sum: "$amount" },
991
+ count: { $sum: 1 }
992
+ }
993
+ }
994
+ ]).then(
995
+ (results) => results[0] || { totalAmount: 0, count: 0 }
996
+ );
997
+ };
998
+ taxWithholdingSchema.statics.addTTLIndex = async function(fieldName, ttlSeconds, options = {}) {
999
+ const collection = this.collection;
1000
+ const indexName = `${fieldName}_ttl_1`;
1001
+ try {
1002
+ const indexes = await collection.indexes();
1003
+ const hasTTLIndex = indexes.some((idx) => idx.name === indexName);
1004
+ if (hasTTLIndex) {
1005
+ await collection.dropIndex(indexName);
1006
+ logger.info("Dropped existing TTL index", { indexName, fieldName });
1007
+ }
1008
+ const indexOptions = {
1009
+ name: indexName,
1010
+ expireAfterSeconds: ttlSeconds
1011
+ };
1012
+ indexOptions.partialFilterExpression = {
1013
+ [fieldName]: { $exists: true },
1014
+ ...options.partialFilter
1015
+ };
1016
+ await collection.createIndex(
1017
+ { [fieldName]: 1 },
1018
+ indexOptions
1019
+ );
1020
+ logger.info("Added TTL index for auto-cleanup", {
1021
+ fieldName,
1022
+ indexName,
1023
+ expireAfterSeconds: ttlSeconds,
1024
+ retentionDays: Math.round(ttlSeconds / (24 * 60 * 60)),
1025
+ partialFilter: indexOptions.partialFilterExpression
1026
+ });
1027
+ } catch (error) {
1028
+ logger.error("Failed to add TTL index", {
1029
+ fieldName,
1030
+ error: error.message
1031
+ });
1032
+ throw error;
1033
+ }
1034
+ };
1035
+ taxWithholdingSchema.statics.removeTTLIndex = async function(fieldName) {
1036
+ const collection = this.collection;
1037
+ const indexName = `${fieldName}_ttl_1`;
1038
+ try {
1039
+ const indexes = await collection.indexes();
1040
+ const hasTTLIndex = indexes.some((idx) => idx.name === indexName);
1041
+ if (hasTTLIndex) {
1042
+ await collection.dropIndex(indexName);
1043
+ logger.info("Removed TTL index", { fieldName, indexName });
1044
+ } else {
1045
+ logger.warn("TTL index not found", { fieldName, indexName });
1046
+ }
1047
+ } catch (error) {
1048
+ logger.error("Failed to remove TTL index", {
1049
+ fieldName,
1050
+ error: error.message
1051
+ });
1052
+ throw error;
1053
+ }
1054
+ };
1055
+ function getTaxWithholdingModel(connection = mongoose.connection) {
1056
+ const modelName = "TaxWithholding";
1057
+ if (connection.models[modelName]) {
1058
+ return connection.models[modelName];
1059
+ }
1060
+ return connection.model(
1061
+ modelName,
1062
+ taxWithholdingSchema
1063
+ );
1064
+ }
1065
+
1066
+ // src/schemas/tax-withholding.ts
1067
+ var taxWithholdingIndexes = [
1068
+ { fields: { organizationId: 1, status: 1, "period.year": 1, "period.month": 1 } },
1069
+ { fields: { employeeId: 1, "period.year": -1, "period.month": -1 } },
1070
+ { fields: { payrollRecordId: 1 } },
1071
+ { fields: { transactionId: 1 } },
1072
+ { fields: { organizationId: 1, taxType: 1, status: 1 } },
1073
+ { fields: { governmentTransactionId: 1 }, options: { sparse: true } }
1074
+ ];
1075
+ function applyTaxWithholdingIndexes(schema) {
1076
+ for (const { fields, options } of taxWithholdingIndexes) {
1077
+ schema.index(fields, options);
1078
+ }
1079
+ }
1080
+ function getTaxWithholdingFields() {
1081
+ const paths = taxWithholdingSchema.paths;
1082
+ const fields = {};
1083
+ for (const [key, pathObj] of Object.entries(paths)) {
1084
+ if (key === "_id" || key === "__v" || key === "createdAt" || key === "updatedAt") {
1085
+ continue;
1086
+ }
1087
+ fields[key] = pathObj.options || {};
1088
+ }
1089
+ return fields;
1090
+ }
1091
+
1092
+ // src/schemas/index.ts
1093
+ var allowanceSchema = new Schema(
1094
+ {
1095
+ type: {
1096
+ type: String,
1097
+ enum: ALLOWANCE_TYPE_VALUES,
1098
+ required: true
1099
+ },
1100
+ name: { type: String },
1101
+ amount: { type: Number, required: true, min: 0 },
1102
+ isPercentage: { type: Boolean, default: false },
1103
+ value: { type: Number },
1104
+ taxable: { type: Boolean, default: true },
1105
+ recurring: { type: Boolean, default: true },
1106
+ effectiveFrom: { type: Date, default: () => /* @__PURE__ */ new Date() },
1107
+ effectiveTo: { type: Date }
1108
+ },
1109
+ { _id: false }
1110
+ );
1111
+ var deductionSchema = new Schema(
1112
+ {
1113
+ type: {
1114
+ type: String,
1115
+ enum: DEDUCTION_TYPE_VALUES,
1116
+ required: true
1117
+ },
1118
+ name: { type: String },
1119
+ amount: { type: Number, required: true, min: 0 },
1120
+ isPercentage: { type: Boolean, default: false },
1121
+ value: { type: Number },
1122
+ auto: { type: Boolean, default: false },
1123
+ recurring: { type: Boolean, default: true },
1124
+ effectiveFrom: { type: Date, default: () => /* @__PURE__ */ new Date() },
1125
+ effectiveTo: { type: Date },
1126
+ description: { type: String }
1127
+ },
1128
+ { _id: false }
1129
+ );
1130
+ var compensationSchema = new Schema(
1131
+ {
1132
+ baseAmount: { type: Number, required: true, min: 0 },
1133
+ frequency: {
1134
+ type: String,
1135
+ enum: PAYMENT_FREQUENCY_VALUES,
1136
+ default: "monthly"
1137
+ },
1138
+ currency: { type: String },
1139
+ // No default - use config or USD fallback in application logic
1140
+ allowances: [allowanceSchema],
1141
+ deductions: [deductionSchema],
1142
+ grossSalary: { type: Number, default: 0 },
1143
+ netSalary: { type: Number, default: 0 },
1144
+ effectiveFrom: { type: Date, default: () => /* @__PURE__ */ new Date() },
1145
+ lastModified: { type: Date, default: () => /* @__PURE__ */ new Date() }
1146
+ },
1147
+ { _id: false }
1148
+ );
1149
+ var workScheduleSchema = new Schema(
1150
+ {
1151
+ hoursPerWeek: { type: Number, min: 0, max: 168 },
1152
+ hoursPerDay: { type: Number, min: 0, max: 24 },
1153
+ workingDays: [{ type: Number, min: 0, max: 6 }],
1154
+ shiftStart: { type: String },
1155
+ shiftEnd: { type: String }
1156
+ },
1157
+ { _id: false }
1158
+ );
1159
+ var bankDetailsSchema = new Schema(
1160
+ {
1161
+ accountName: { type: String },
1162
+ accountNumber: { type: String },
1163
+ bankName: { type: String },
1164
+ branchName: { type: String },
1165
+ routingNumber: { type: String }
1166
+ },
1167
+ { _id: false }
1168
+ );
1169
+ var employmentHistorySchema = new Schema(
1170
+ {
1171
+ hireDate: { type: Date, required: true },
1172
+ terminationDate: { type: Date, required: true },
1173
+ reason: { type: String, enum: TERMINATION_REASON_VALUES },
1174
+ finalSalary: { type: Number },
1175
+ position: { type: String },
1176
+ department: { type: String },
1177
+ notes: { type: String }
1178
+ },
1179
+ { timestamps: true }
1180
+ );
1181
+ var payrollStatsSchema = new Schema(
1182
+ {
1183
+ totalPaid: { type: Number, default: 0, min: 0 },
1184
+ lastPaymentDate: { type: Date },
1185
+ nextPaymentDate: { type: Date },
1186
+ paymentsThisYear: { type: Number, default: 0, min: 0 },
1187
+ averageMonthly: { type: Number, default: 0, min: 0 },
1188
+ updatedAt: { type: Date, default: () => /* @__PURE__ */ new Date() }
1189
+ },
1190
+ { _id: false }
1191
+ );
1192
+ function createEmploymentFields(options = {}) {
1193
+ const { organizationRef = "Organization", userRef = "User" } = options;
1194
+ return {
1195
+ userId: {
1196
+ type: Schema.Types.ObjectId,
1197
+ ref: userRef,
1198
+ required: false
1199
+ // Allow guest employees (no user account)
1200
+ },
1201
+ email: {
1202
+ type: String,
1203
+ trim: true,
1204
+ lowercase: true,
1205
+ required: false
1206
+ // For guest employees without user account
1207
+ },
1208
+ organizationId: {
1209
+ type: Schema.Types.ObjectId,
1210
+ ref: organizationRef,
1211
+ // Configurable: 'Branch', 'Company', 'Tenant', etc.
1212
+ required: true
1213
+ },
1214
+ employeeId: { type: String, required: true },
1215
+ employmentType: {
1216
+ type: String,
1217
+ enum: EMPLOYMENT_TYPE_VALUES,
1218
+ default: "full_time"
1219
+ },
1220
+ status: {
1221
+ type: String,
1222
+ enum: EMPLOYEE_STATUS_VALUES,
1223
+ default: "active"
1224
+ },
1225
+ department: { type: String, enum: DEPARTMENT_VALUES },
1226
+ position: { type: String, required: true },
1227
+ hireDate: { type: Date, required: true },
1228
+ terminationDate: { type: Date },
1229
+ probationEndDate: { type: Date },
1230
+ employmentHistory: [employmentHistorySchema],
1231
+ compensation: { type: compensationSchema, required: true },
1232
+ workSchedule: workScheduleSchema,
1233
+ bankDetails: bankDetailsSchema,
1234
+ payrollStats: { type: payrollStatsSchema, default: () => ({}) }
1235
+ };
1236
+ }
1237
+ var payrollBreakdownSchema = new Schema(
1238
+ {
1239
+ baseAmount: { type: Number, required: true, min: 0 },
1240
+ allowances: [
1241
+ {
1242
+ type: { type: String, required: true },
1243
+ amount: { type: Number, required: true, min: 0 },
1244
+ taxable: { type: Boolean, default: true }
1245
+ }
1246
+ ],
1247
+ deductions: [
1248
+ {
1249
+ type: { type: String, required: true },
1250
+ amount: { type: Number, required: true, min: 0 },
1251
+ description: { type: String }
1252
+ }
1253
+ ],
1254
+ grossSalary: { type: Number, required: true, min: 0 },
1255
+ netSalary: { type: Number, required: true, min: 0 },
1256
+ taxableAmount: { type: Number, default: 0, min: 0 },
1257
+ taxAmount: { type: Number, default: 0, min: 0 },
1258
+ workingDays: { type: Number, min: 0 },
1259
+ actualDays: { type: Number, min: 0 },
1260
+ proRatedAmount: { type: Number, default: 0, min: 0 },
1261
+ attendanceDeduction: { type: Number, default: 0, min: 0 },
1262
+ overtimeAmount: { type: Number, default: 0, min: 0 },
1263
+ bonusAmount: { type: Number, default: 0, min: 0 }
1264
+ },
1265
+ { _id: false }
1266
+ );
1267
+ function createPayrollRecordFields(options = {}) {
1268
+ const { organizationRef = "Organization", userRef = "User" } = options;
1269
+ return {
1270
+ organizationId: {
1271
+ type: Schema.Types.ObjectId,
1272
+ ref: organizationRef,
1273
+ // Configurable: 'Branch', 'Company', 'Tenant', etc.
1274
+ required: true
1275
+ },
1276
+ employeeId: {
1277
+ type: Schema.Types.ObjectId,
1278
+ required: true
1279
+ },
1280
+ userId: {
1281
+ type: Schema.Types.ObjectId,
1282
+ ref: userRef,
1283
+ required: false
1284
+ // Optional for guest employees
1285
+ },
1286
+ period: { type: periodSchema, required: true },
1287
+ breakdown: { type: payrollBreakdownSchema, required: true },
1288
+ transactionId: { type: Schema.Types.ObjectId },
1289
+ status: {
1290
+ type: String,
1291
+ enum: PAYROLL_STATUS_VALUES,
1292
+ default: "pending"
1293
+ },
1294
+ paidAt: { type: Date },
1295
+ processedAt: { type: Date },
1296
+ paymentMethod: { type: String, enum: PAYMENT_METHOD_VALUES },
1297
+ metadata: { type: Schema.Types.Mixed },
1298
+ processedBy: { type: Schema.Types.ObjectId, ref: userRef },
1299
+ notes: { type: String },
1300
+ payslipUrl: { type: String },
1301
+ exported: { type: Boolean, default: false },
1302
+ exportedAt: { type: Date },
1303
+ // Void / Reversal fields (v2.4.0+)
1304
+ isVoided: { type: Boolean, default: false },
1305
+ voidedAt: { type: Date },
1306
+ voidedBy: { type: Schema.Types.ObjectId, ref: userRef },
1307
+ voidReason: { type: String },
1308
+ reversedAt: { type: Date },
1309
+ reversedBy: { type: Schema.Types.ObjectId, ref: userRef },
1310
+ reversalReason: { type: String },
1311
+ reversalTransactionId: { type: Schema.Types.ObjectId },
1312
+ originalPayrollId: { type: Schema.Types.ObjectId },
1313
+ // TTL expiration (per-document)
1314
+ expireAt: { type: Date }
1315
+ };
1316
+ }
1317
+ var employeeIndexes = [
1318
+ { fields: { organizationId: 1, employeeId: 1 }, options: { unique: true } },
1319
+ // Partial unique index: Only includes docs with userId field (excludes guest employees)
1320
+ // Uses partialFilterExpression instead of sparse for compound indexes
1321
+ {
1322
+ fields: { userId: 1, organizationId: 1 },
1323
+ options: {
1324
+ unique: true,
1325
+ partialFilterExpression: { userId: { $exists: true } }
1326
+ }
1327
+ },
1328
+ // Partial unique index: Only includes non-terminated docs with email
1329
+ // This allows email reuse when employees are terminated and rehired
1330
+ {
1331
+ fields: { email: 1, organizationId: 1 },
1332
+ options: {
1333
+ unique: true,
1334
+ partialFilterExpression: {
1335
+ email: { $exists: true },
1336
+ status: { $in: ["active", "on_leave", "suspended"] }
1337
+ }
1338
+ }
1339
+ },
1340
+ { fields: { organizationId: 1, status: 1 } },
1341
+ { fields: { organizationId: 1, department: 1 } },
1342
+ { fields: { organizationId: 1, "compensation.netSalary": -1 } }
1343
+ ];
1344
+ var payrollRecordIndexes = [
1345
+ // Composite index for common queries (not unique - app handles duplicates)
1346
+ { fields: { organizationId: 1, employeeId: 1, "period.month": 1, "period.year": 1 } },
1347
+ { fields: { organizationId: 1, "period.year": 1, "period.month": 1 } },
1348
+ { fields: { employeeId: 1, "period.year": -1, "period.month": -1 } },
1349
+ { fields: { status: 1, createdAt: -1 } },
1350
+ { fields: { organizationId: 1, status: 1, "period.payDate": 1 } },
1351
+ {
1352
+ fields: { createdAt: 1 },
1353
+ options: {
1354
+ expireAfterSeconds: HRM_CONFIG.dataRetention.payrollRecordsTTL
1355
+ // TTL applies to ALL records (user handles backups/exports at app level)
1356
+ }
1357
+ }
1358
+ ];
1359
+ function applyEmployeeIndexes(schema) {
1360
+ for (const { fields, options } of employeeIndexes) {
1361
+ schema.index(fields, options);
1362
+ }
1363
+ }
1364
+ function applyPayrollRecordIndexes(schema) {
1365
+ for (const { fields, options } of payrollRecordIndexes) {
1366
+ schema.index(fields, options);
1367
+ }
1368
+ }
1369
+ function createEmployeeSchema(additionalFields = {}, options = {}) {
1370
+ const schema = new Schema(
1371
+ {
1372
+ ...createEmploymentFields(options),
1373
+ ...additionalFields
1374
+ },
1375
+ { timestamps: true }
1376
+ );
1377
+ applyEmployeeIndexes(schema);
1378
+ return schema;
1379
+ }
1380
+ function createPayrollRecordSchema(additionalFields = {}, options = {}) {
1381
+ const schema = new Schema(
1382
+ {
1383
+ ...createPayrollRecordFields(options),
1384
+ ...additionalFields
1385
+ },
1386
+ { timestamps: true }
1387
+ );
1388
+ applyPayrollRecordIndexes(schema);
1389
+ schema.virtual("totalAmount").get(function() {
1390
+ return this.breakdown?.netSalary || 0;
1391
+ });
1392
+ schema.virtual("isPaid").get(function() {
1393
+ return this.status === "paid";
1394
+ });
1395
+ schema.virtual("periodLabel").get(function() {
1396
+ const months = [
1397
+ "Jan",
1398
+ "Feb",
1399
+ "Mar",
1400
+ "Apr",
1401
+ "May",
1402
+ "Jun",
1403
+ "Jul",
1404
+ "Aug",
1405
+ "Sep",
1406
+ "Oct",
1407
+ "Nov",
1408
+ "Dec"
1409
+ ];
1410
+ return `${months[this.period.month - 1]} ${this.period.year}`;
1411
+ });
1412
+ schema.methods.markAsPaid = function(transactionId, paidAt = /* @__PURE__ */ new Date()) {
1413
+ this.status = "paid";
1414
+ this.transactionId = transactionId;
1415
+ this.paidAt = paidAt;
1416
+ };
1417
+ schema.methods.markAsExported = function() {
1418
+ this.exported = true;
1419
+ this.exportedAt = /* @__PURE__ */ new Date();
1420
+ };
1421
+ schema.methods.canBeDeleted = function() {
1422
+ return this.exported && this.status === "paid";
1423
+ };
1424
+ return schema;
1425
+ }
1426
+ var schemas_default = {
1427
+ // Sub-schemas
1428
+ allowanceSchema,
1429
+ deductionSchema,
1430
+ compensationSchema,
1431
+ workScheduleSchema,
1432
+ bankDetailsSchema,
1433
+ employmentHistorySchema,
1434
+ payrollStatsSchema,
1435
+ payrollBreakdownSchema,
1436
+ periodSchema,
1437
+ // Field creators (configurable references)
1438
+ createEmploymentFields,
1439
+ createPayrollRecordFields,
1440
+ // Indexes
1441
+ employeeIndexes,
1442
+ payrollRecordIndexes,
1443
+ applyEmployeeIndexes,
1444
+ applyPayrollRecordIndexes,
1445
+ // Schema creators
1446
+ createEmployeeSchema,
1447
+ createPayrollRecordSchema
1448
+ };
1449
+
1450
+ export { allowanceSchema, applyEmployeeIndexes, applyLeaveRequestIndexes, applyPayrollRecordIndexes, applyTaxWithholdingIndexes, bankDetailsSchema, compensationSchema, createEmployeeSchema, createEmploymentFields, createPayrollRecordFields, createPayrollRecordSchema, deductionSchema, schemas_default as default, employeeIndexes, employmentHistorySchema, getLeaveRequestFields, getLeaveRequestModel, getTaxWithholdingFields, getTaxWithholdingModel, leaveBalanceFields, leaveBalanceSchema, leaveRequestIndexes, leaveRequestSchema, leaveRequestTTLIndex, payrollBreakdownSchema, payrollRecordIndexes, payrollStatsSchema, periodSchema, taxWithholdingIndexes, taxWithholdingSchema, workScheduleSchema };
1451
+ //# sourceMappingURL=index.js.map
1452
+ //# sourceMappingURL=index.js.map