@classytic/payroll 2.0.0 → 2.3.0

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

Potentially problematic release.


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

@@ -39,6 +39,16 @@ var PAYMENT_FREQUENCY = {
39
39
  DAILY: "daily"
40
40
  };
41
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);
42
52
  var ALLOWANCE_TYPE = {
43
53
  HOUSING: "housing",
44
54
  TRANSPORT: "transport",
@@ -77,6 +87,40 @@ var TERMINATION_REASON = {
77
87
  OTHER: "other"
78
88
  };
79
89
  var TERMINATION_REASON_VALUES = Object.values(TERMINATION_REASON);
90
+ var LEAVE_TYPE = {
91
+ ANNUAL: "annual",
92
+ SICK: "sick",
93
+ UNPAID: "unpaid",
94
+ MATERNITY: "maternity",
95
+ PATERNITY: "paternity",
96
+ BEREAVEMENT: "bereavement",
97
+ COMPENSATORY: "compensatory",
98
+ OTHER: "other"
99
+ };
100
+ var LEAVE_TYPE_VALUES = Object.values(LEAVE_TYPE);
101
+ var LEAVE_REQUEST_STATUS = {
102
+ PENDING: "pending",
103
+ APPROVED: "approved",
104
+ REJECTED: "rejected",
105
+ CANCELLED: "cancelled"
106
+ };
107
+ var LEAVE_REQUEST_STATUS_VALUES = Object.values(LEAVE_REQUEST_STATUS);
108
+ var TAX_TYPE = {
109
+ INCOME_TAX: "income_tax",
110
+ SOCIAL_SECURITY: "social_security",
111
+ HEALTH_INSURANCE: "health_insurance",
112
+ PENSION: "pension",
113
+ EMPLOYMENT_INSURANCE: "employment_insurance",
114
+ LOCAL_TAX: "local_tax",
115
+ OTHER: "other"
116
+ };
117
+ var TAX_TYPE_VALUES = Object.values(TAX_TYPE);
118
+ var TAX_STATUS = {
119
+ PENDING: "pending",
120
+ SUBMITTED: "submitted",
121
+ PAID: "paid"
122
+ };
123
+ var TAX_STATUS_VALUES = Object.values(TAX_STATUS);
80
124
 
81
125
  // src/config.ts
82
126
  var HRM_CONFIG = {
@@ -115,6 +159,267 @@ var ORG_ROLES = {
115
159
  }
116
160
  };
117
161
  Object.values(ORG_ROLES).map((role) => role.key);
162
+ var periodSchema = new Schema(
163
+ {
164
+ month: { type: Number, required: true, min: 1, max: 12 },
165
+ year: { type: Number, required: true, min: 2020 },
166
+ startDate: { type: Date, required: true },
167
+ endDate: { type: Date, required: true },
168
+ payDate: { type: Date, required: true }
169
+ },
170
+ { _id: false }
171
+ );
172
+ var leaveBalanceSchema = new Schema(
173
+ {
174
+ type: {
175
+ type: String,
176
+ enum: LEAVE_TYPE_VALUES,
177
+ required: true
178
+ },
179
+ allocated: { type: Number, default: 0, min: 0 },
180
+ used: { type: Number, default: 0, min: 0 },
181
+ pending: { type: Number, default: 0, min: 0 },
182
+ carriedOver: { type: Number, default: 0, min: 0 },
183
+ expiresAt: { type: Date },
184
+ year: { type: Number, required: true }
185
+ },
186
+ { _id: false }
187
+ );
188
+ var leaveBalanceFields = {
189
+ leaveBalances: [leaveBalanceSchema]
190
+ };
191
+ var leaveRequestFields = {
192
+ organizationId: {
193
+ type: Schema.Types.ObjectId,
194
+ ref: "Organization",
195
+ required: false
196
+ // Optional for single-tenant mode
197
+ },
198
+ employeeId: {
199
+ type: Schema.Types.ObjectId,
200
+ required: true
201
+ },
202
+ userId: {
203
+ type: Schema.Types.ObjectId,
204
+ ref: "User",
205
+ required: true
206
+ },
207
+ type: {
208
+ type: String,
209
+ enum: LEAVE_TYPE_VALUES,
210
+ required: true
211
+ },
212
+ startDate: { type: Date, required: true },
213
+ endDate: { type: Date, required: true },
214
+ days: { type: Number, required: true, min: 0.5 },
215
+ halfDay: { type: Boolean, default: false },
216
+ reason: { type: String },
217
+ status: {
218
+ type: String,
219
+ enum: LEAVE_REQUEST_STATUS_VALUES,
220
+ default: "pending"
221
+ },
222
+ reviewedBy: { type: Schema.Types.ObjectId, ref: "User" },
223
+ reviewedAt: { type: Date },
224
+ reviewNotes: { type: String },
225
+ attachments: [{ type: String }],
226
+ metadata: { type: Schema.Types.Mixed, default: {} }
227
+ };
228
+ var leaveRequestIndexes = [
229
+ { fields: { organizationId: 1, employeeId: 1, startDate: -1 } },
230
+ { fields: { organizationId: 1, status: 1, createdAt: -1 } },
231
+ { fields: { employeeId: 1, status: 1 } },
232
+ // Most relevant for single-tenant
233
+ { fields: { organizationId: 1, type: 1, status: 1 } }
234
+ ];
235
+ var leaveRequestTTLIndex = {
236
+ fields: { createdAt: 1 },
237
+ options: {
238
+ expireAfterSeconds: 63072e3,
239
+ // 2 years default
240
+ partialFilterExpression: {
241
+ status: { $in: ["approved", "rejected", "cancelled"] }
242
+ }
243
+ }
244
+ };
245
+ function applyLeaveRequestIndexes(schema, options = {}) {
246
+ if (!options.createIndexes) return;
247
+ for (const { fields } of leaveRequestIndexes) {
248
+ schema.index(fields);
249
+ }
250
+ if (options.enableTTL) {
251
+ schema.index(leaveRequestTTLIndex.fields, {
252
+ ...leaveRequestTTLIndex.options,
253
+ expireAfterSeconds: options.ttlSeconds ?? leaveRequestTTLIndex.options.expireAfterSeconds
254
+ });
255
+ }
256
+ }
257
+ function createLeaveRequestSchema(additionalFields = {}, options = {}) {
258
+ const fields = { ...leaveRequestFields };
259
+ if (options.requireOrganizationId) {
260
+ fields.organizationId = {
261
+ ...fields.organizationId,
262
+ required: true
263
+ };
264
+ }
265
+ const schema = new Schema(
266
+ {
267
+ ...fields,
268
+ ...additionalFields
269
+ },
270
+ { timestamps: true }
271
+ );
272
+ applyLeaveRequestIndexes(schema, options);
273
+ schema.virtual("isPending").get(function() {
274
+ return this.status === "pending";
275
+ });
276
+ schema.virtual("isApproved").get(function() {
277
+ return this.status === "approved";
278
+ });
279
+ schema.virtual("isRejected").get(function() {
280
+ return this.status === "rejected";
281
+ });
282
+ schema.virtual("isCancelled").get(function() {
283
+ return this.status === "cancelled";
284
+ });
285
+ schema.virtual("durationInDays").get(function() {
286
+ return this.days;
287
+ });
288
+ return schema;
289
+ }
290
+ var taxWithholdingFields = {
291
+ organizationId: {
292
+ type: Schema.Types.ObjectId,
293
+ required: true,
294
+ ref: "Organization",
295
+ index: true
296
+ },
297
+ employeeId: {
298
+ type: Schema.Types.ObjectId,
299
+ required: true,
300
+ ref: "Employee",
301
+ index: true
302
+ },
303
+ userId: {
304
+ type: Schema.Types.ObjectId,
305
+ required: false,
306
+ ref: "User"
307
+ },
308
+ payrollRecordId: {
309
+ type: Schema.Types.ObjectId,
310
+ required: true,
311
+ ref: "PayrollRecord",
312
+ index: true
313
+ },
314
+ transactionId: {
315
+ type: Schema.Types.ObjectId,
316
+ required: true,
317
+ ref: "Transaction",
318
+ index: true
319
+ },
320
+ period: {
321
+ type: periodSchema,
322
+ required: true
323
+ },
324
+ amount: {
325
+ type: Number,
326
+ required: true,
327
+ min: 0
328
+ },
329
+ currency: {
330
+ type: String,
331
+ default: "BDT"
332
+ },
333
+ taxType: {
334
+ type: String,
335
+ enum: TAX_TYPE_VALUES,
336
+ required: true
337
+ },
338
+ taxRate: {
339
+ type: Number,
340
+ required: true,
341
+ min: 0,
342
+ max: 1
343
+ },
344
+ taxableAmount: {
345
+ type: Number,
346
+ required: true,
347
+ min: 0
348
+ },
349
+ status: {
350
+ type: String,
351
+ enum: TAX_STATUS_VALUES,
352
+ default: "pending"
353
+ },
354
+ submittedAt: Date,
355
+ paidAt: Date,
356
+ governmentTransactionId: {
357
+ type: Schema.Types.ObjectId,
358
+ ref: "Transaction"
359
+ },
360
+ referenceNumber: String,
361
+ notes: String,
362
+ metadata: {
363
+ type: Schema.Types.Mixed,
364
+ default: {}
365
+ }
366
+ };
367
+ var taxWithholdingIndexes = [
368
+ {
369
+ fields: { organizationId: 1, status: 1, "period.year": 1, "period.month": 1 }
370
+ },
371
+ {
372
+ fields: { employeeId: 1, "period.year": -1, "period.month": -1 }
373
+ },
374
+ {
375
+ fields: { payrollRecordId: 1 }
376
+ },
377
+ {
378
+ fields: { transactionId: 1 }
379
+ },
380
+ {
381
+ fields: { organizationId: 1, taxType: 1, status: 1 }
382
+ },
383
+ {
384
+ fields: { governmentTransactionId: 1 },
385
+ options: { sparse: true }
386
+ }
387
+ ];
388
+ function applyTaxWithholdingIndexes(schema) {
389
+ for (const { fields, options } of taxWithholdingIndexes) {
390
+ schema.index(fields, options);
391
+ }
392
+ }
393
+ function createTaxWithholdingSchema(additionalFields = {}) {
394
+ const schema = new Schema(
395
+ {
396
+ ...taxWithholdingFields,
397
+ ...additionalFields
398
+ },
399
+ { timestamps: true }
400
+ );
401
+ applyTaxWithholdingIndexes(schema);
402
+ schema.virtual("isPending").get(function() {
403
+ return this.status === "pending";
404
+ });
405
+ schema.virtual("isPaid").get(function() {
406
+ return this.status === "paid";
407
+ });
408
+ schema.virtual("isSubmitted").get(function() {
409
+ return this.status === "submitted";
410
+ });
411
+ schema.methods.markAsSubmitted = function(submittedAt = /* @__PURE__ */ new Date()) {
412
+ this.status = "submitted";
413
+ this.submittedAt = submittedAt;
414
+ };
415
+ schema.methods.markAsPaid = function(transactionId, referenceNumber, paidAt = /* @__PURE__ */ new Date()) {
416
+ this.status = "paid";
417
+ this.governmentTransactionId = transactionId;
418
+ this.referenceNumber = referenceNumber;
419
+ this.paidAt = paidAt;
420
+ };
421
+ return schema;
422
+ }
118
423
 
119
424
  // src/schemas/index.ts
120
425
  var allowanceSchema = new Schema(
@@ -219,7 +524,15 @@ var employmentFields = {
219
524
  userId: {
220
525
  type: Schema.Types.ObjectId,
221
526
  ref: "User",
222
- required: true
527
+ required: false
528
+ // Allow guest employees (no user account)
529
+ },
530
+ email: {
531
+ type: String,
532
+ trim: true,
533
+ lowercase: true,
534
+ required: false
535
+ // For guest employees without user account
223
536
  },
224
537
  organizationId: {
225
538
  type: Schema.Types.ObjectId,
@@ -253,20 +566,22 @@ var payrollBreakdownSchema = new Schema(
253
566
  baseAmount: { type: Number, required: true, min: 0 },
254
567
  allowances: [
255
568
  {
256
- type: { type: String },
257
- amount: { type: Number, min: 0 },
569
+ type: { type: String, required: true },
570
+ amount: { type: Number, required: true, min: 0 },
258
571
  taxable: { type: Boolean, default: true }
259
572
  }
260
573
  ],
261
574
  deductions: [
262
575
  {
263
- type: { type: String },
264
- amount: { type: Number, min: 0 },
576
+ type: { type: String, required: true },
577
+ amount: { type: Number, required: true, min: 0 },
265
578
  description: { type: String }
266
579
  }
267
580
  ],
268
581
  grossSalary: { type: Number, required: true, min: 0 },
269
582
  netSalary: { type: Number, required: true, min: 0 },
583
+ taxableAmount: { type: Number, default: 0, min: 0 },
584
+ taxAmount: { type: Number, default: 0, min: 0 },
270
585
  workingDays: { type: Number, min: 0 },
271
586
  actualDays: { type: Number, min: 0 },
272
587
  proRatedAmount: { type: Number, default: 0, min: 0 },
@@ -276,32 +591,21 @@ var payrollBreakdownSchema = new Schema(
276
591
  },
277
592
  { _id: false }
278
593
  );
279
- var periodSchema = new Schema(
280
- {
281
- month: { type: Number, required: true, min: 1, max: 12 },
282
- year: { type: Number, required: true, min: 2020 },
283
- startDate: { type: Date, required: true },
284
- endDate: { type: Date, required: true },
285
- payDate: { type: Date, required: true }
286
- },
287
- { _id: false }
288
- );
289
594
  var payrollRecordFields = {
290
595
  organizationId: {
291
596
  type: Schema.Types.ObjectId,
292
597
  ref: "Organization",
293
- required: true,
294
- index: true
598
+ required: true
295
599
  },
296
600
  employeeId: {
297
601
  type: Schema.Types.ObjectId,
298
- required: true,
299
- index: true
602
+ required: true
300
603
  },
301
604
  userId: {
302
605
  type: Schema.Types.ObjectId,
303
606
  ref: "User",
304
- index: true
607
+ required: false
608
+ // Optional for guest employees
305
609
  },
306
610
  period: { type: periodSchema, required: true },
307
611
  breakdown: { type: payrollBreakdownSchema, required: true },
@@ -309,20 +613,56 @@ var payrollRecordFields = {
309
613
  status: {
310
614
  type: String,
311
615
  enum: PAYROLL_STATUS_VALUES,
312
- default: "pending",
313
- index: true
616
+ default: "pending"
314
617
  },
618
+ processedAt: { type: Date },
315
619
  paidAt: { type: Date },
316
- paymentMethod: { type: String },
620
+ paymentMethod: {
621
+ type: String,
622
+ enum: PAYMENT_METHOD_VALUES
623
+ },
317
624
  processedBy: { type: Schema.Types.ObjectId, ref: "User" },
318
625
  notes: { type: String },
319
626
  payslipUrl: { type: String },
627
+ metadata: {
628
+ type: Schema.Types.Mixed,
629
+ default: {}
630
+ },
320
631
  exported: { type: Boolean, default: false },
321
- exportedAt: { type: Date }
632
+ exportedAt: { type: Date },
633
+ corrections: [
634
+ {
635
+ previousAmount: Number,
636
+ newAmount: Number,
637
+ reason: String,
638
+ correctedBy: { type: Schema.Types.ObjectId, ref: "User" },
639
+ correctedAt: { type: Date, default: Date.now }
640
+ }
641
+ ]
322
642
  };
323
643
  var employeeIndexes = [
324
644
  { fields: { organizationId: 1, employeeId: 1 }, options: { unique: true } },
325
- { fields: { userId: 1, organizationId: 1 }, options: { unique: true } },
645
+ // Partial unique index: Only includes docs with userId field (excludes guest employees)
646
+ // Uses partialFilterExpression instead of sparse for compound indexes
647
+ {
648
+ fields: { userId: 1, organizationId: 1 },
649
+ options: {
650
+ unique: true,
651
+ partialFilterExpression: { userId: { $exists: true } }
652
+ }
653
+ },
654
+ // Partial unique index: Only includes non-terminated docs with email
655
+ // This allows email reuse when employees are terminated and rehired
656
+ {
657
+ fields: { email: 1, organizationId: 1 },
658
+ options: {
659
+ unique: true,
660
+ partialFilterExpression: {
661
+ email: { $exists: true },
662
+ status: { $in: ["active", "on_leave", "suspended"] }
663
+ }
664
+ }
665
+ },
326
666
  { fields: { organizationId: 1, status: 1 } },
327
667
  { fields: { organizationId: 1, department: 1 } },
328
668
  { fields: { organizationId: 1, "compensation.netSalary": -1 } }
@@ -339,8 +679,8 @@ var payrollRecordIndexes = [
339
679
  {
340
680
  fields: { createdAt: 1 },
341
681
  options: {
342
- expireAfterSeconds: HRM_CONFIG.dataRetention.payrollRecordsTTL,
343
- partialFilterExpression: { exported: true }
682
+ expireAfterSeconds: HRM_CONFIG.dataRetention.payrollRecordsTTL
683
+ // TTL applies to ALL records (user handles backups/exports at app level)
344
684
  }
345
685
  }
346
686
  ];
@@ -435,6 +775,6 @@ var schemas_default = {
435
775
  createPayrollRecordSchema
436
776
  };
437
777
 
438
- export { allowanceSchema, applyEmployeeIndexes, applyPayrollRecordIndexes, bankDetailsSchema, compensationSchema, createEmployeeSchema, createPayrollRecordSchema, deductionSchema, schemas_default as default, employeeIndexes, employmentFields, employmentHistorySchema, payrollBreakdownSchema, payrollRecordFields, payrollRecordIndexes, payrollStatsSchema, periodSchema, workScheduleSchema };
778
+ export { allowanceSchema, applyEmployeeIndexes, applyLeaveRequestIndexes, applyPayrollRecordIndexes, applyTaxWithholdingIndexes, bankDetailsSchema, compensationSchema, createEmployeeSchema, createLeaveRequestSchema, createPayrollRecordSchema, createTaxWithholdingSchema, deductionSchema, schemas_default as default, employeeIndexes, employmentFields, employmentHistorySchema, leaveBalanceFields, leaveBalanceSchema, leaveRequestFields, leaveRequestIndexes, leaveRequestTTLIndex, payrollBreakdownSchema, payrollRecordFields, payrollRecordIndexes, payrollStatsSchema, periodSchema, taxWithholdingFields, taxWithholdingIndexes, workScheduleSchema };
439
779
  //# sourceMappingURL=index.js.map
440
780
  //# sourceMappingURL=index.js.map