@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.
- package/README.md +525 -574
- package/dist/calculators/index.d.ts +300 -0
- package/dist/calculators/index.js +304 -0
- package/dist/calculators/index.js.map +1 -0
- package/dist/employee-identity-Cq2wo9-2.d.ts +490 -0
- package/dist/index-DjB72l6e.d.ts +3742 -0
- package/dist/index.d.ts +2924 -0
- package/dist/index.js +10648 -0
- package/dist/index.js.map +1 -0
- package/dist/prorating.calculator-C7sdFiG2.d.ts +135 -0
- package/dist/schemas/index.d.ts +4 -0
- package/dist/schemas/index.js +1452 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/types-BVDjiVGS.d.ts +1856 -0
- package/dist/utils/index.d.ts +995 -0
- package/dist/utils/index.js +1629 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +77 -24
- package/src/config.js +0 -177
- package/src/core/compensation.manager.js +0 -242
- package/src/core/employment.manager.js +0 -224
- package/src/core/payroll.manager.js +0 -499
- package/src/enums.js +0 -141
- package/src/factories/compensation.factory.js +0 -198
- package/src/factories/employee.factory.js +0 -173
- package/src/factories/payroll.factory.js +0 -247
- package/src/hrm.orchestrator.js +0 -139
- package/src/index.js +0 -172
- package/src/init.js +0 -41
- package/src/models/payroll-record.model.js +0 -126
- package/src/plugins/employee.plugin.js +0 -157
- package/src/schemas/employment.schema.js +0 -126
- package/src/services/compensation.service.js +0 -231
- package/src/services/employee.service.js +0 -162
- package/src/services/payroll.service.js +0 -213
- package/src/utils/calculation.utils.js +0 -91
- package/src/utils/date.utils.js +0 -120
- package/src/utils/logger.js +0 -36
- package/src/utils/query-builders.js +0 -185
- 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
|