@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
package/README.md
CHANGED
|
@@ -1,574 +1,525 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
await
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
//
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
```
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
payrollRecordId: ObjectId(...),
|
|
527
|
-
period: { month: 11, year: 2025 },
|
|
528
|
-
breakdown: { ... }
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
### Data Retention & Export
|
|
534
|
-
|
|
535
|
-
PayrollRecords auto-delete after 2 years:
|
|
536
|
-
```javascript
|
|
537
|
-
// TTL index on PayrollRecord
|
|
538
|
-
payrollRecordSchema.index(
|
|
539
|
-
{ createdAt: 1 },
|
|
540
|
-
{
|
|
541
|
-
expireAfterSeconds: 63072000, // 2 years
|
|
542
|
-
partialFilterExpression: { exported: true } // Only if exported
|
|
543
|
-
}
|
|
544
|
-
);
|
|
545
|
-
|
|
546
|
-
// Export before deletion
|
|
547
|
-
const records = await hrm.exportPayroll({
|
|
548
|
-
organizationId,
|
|
549
|
-
startDate: new Date('2023-01-01'),
|
|
550
|
-
endDate: new Date('2023-12-31')
|
|
551
|
-
});
|
|
552
|
-
// Marks records as exported, making them eligible for deletion
|
|
553
|
-
```
|
|
554
|
-
|
|
555
|
-
## 🎯 Design Philosophy
|
|
556
|
-
|
|
557
|
-
- **Stripe/Passport.js inspired**: Clean DSL, dependency injection, reusable components
|
|
558
|
-
- **Lightweight**: Not a complex ERP, gym-focused features only
|
|
559
|
-
- **Multi-tenant**: Same user can work at multiple organizations
|
|
560
|
-
- **Smart defaults**: Pro-rating, attendance integration, automatic calculations
|
|
561
|
-
- **Production-ready**: Transaction integration, data retention, comprehensive error handling
|
|
562
|
-
|
|
563
|
-
## ✅ Next Steps
|
|
564
|
-
|
|
565
|
-
1. Test bootstrap initialization
|
|
566
|
-
2. Create Fastify routes in `modules/employee/`
|
|
567
|
-
3. Add API handlers
|
|
568
|
-
4. Migrate existing staff from organization module
|
|
569
|
-
5. Deploy and monitor
|
|
570
|
-
|
|
571
|
-
---
|
|
572
|
-
|
|
573
|
-
**Built with ❤️ following world-class architecture patterns**
|
|
574
|
-
**Ready for multi-tenant gym management**
|
|
1
|
+
# @classytic/payroll
|
|
2
|
+
|
|
3
|
+
Enterprise HRM & Payroll for MongoDB. Clean architecture, multi-tenant, type-safe.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@classytic/payroll)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @classytic/payroll mongoose @classytic/mongokit
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { createPayrollInstance } from '@classytic/payroll';
|
|
19
|
+
|
|
20
|
+
const payroll = createPayrollInstance()
|
|
21
|
+
.withModels({ EmployeeModel, PayrollRecordModel, TransactionModel })
|
|
22
|
+
.build();
|
|
23
|
+
|
|
24
|
+
// Hire employee
|
|
25
|
+
await payroll.hire({
|
|
26
|
+
organizationId,
|
|
27
|
+
employment: { email: 'dev@example.com', position: 'Engineer' },
|
|
28
|
+
compensation: { baseSalary: 80000, currency: 'USD' },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Process salary
|
|
32
|
+
await payroll.processSalary({
|
|
33
|
+
organizationId,
|
|
34
|
+
employeeId,
|
|
35
|
+
period: { month: 1, year: 2024 },
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Features
|
|
40
|
+
|
|
41
|
+
- **Employee Lifecycle**: Hire, update, terminate, re-hire
|
|
42
|
+
- **Compensation**: Salary, allowances, deductions
|
|
43
|
+
- **Bulk Processing**: Handle 10k+ employees with streaming
|
|
44
|
+
- **Multi-tenant**: Automatic organization isolation
|
|
45
|
+
- **Events & Webhooks**: React to payroll events
|
|
46
|
+
- **Type-safe**: Full TypeScript support
|
|
47
|
+
|
|
48
|
+
## Exports
|
|
49
|
+
|
|
50
|
+
| Entry Point | Description |
|
|
51
|
+
|-------------|-------------|
|
|
52
|
+
| `@classytic/payroll` | Main API (Payroll class, types, errors) |
|
|
53
|
+
| `@classytic/payroll/schemas` | Mongoose schemas for extending |
|
|
54
|
+
| `@classytic/payroll/utils` | Date, money, validation utilities |
|
|
55
|
+
| `@classytic/payroll/calculators` | Pure calculation functions (no DB) |
|
|
56
|
+
|
|
57
|
+
## Employee Management
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// Hire
|
|
61
|
+
await payroll.hire({ organizationId, employment, compensation });
|
|
62
|
+
|
|
63
|
+
// Get employee
|
|
64
|
+
const employee = await payroll.getEmployee({ employeeId, organizationId });
|
|
65
|
+
|
|
66
|
+
// Get by flexible identity (userId, employeeId, or email)
|
|
67
|
+
const emp = await payroll.getEmployeeByIdentity({
|
|
68
|
+
identity: 'EMP-001', // or userId or email
|
|
69
|
+
organizationId,
|
|
70
|
+
mode: 'employeeId', // 'userId' | 'employeeId' | 'email' | 'any'
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Update
|
|
74
|
+
await payroll.updateEmployment({ employeeId, updates: { position: 'Lead' } });
|
|
75
|
+
|
|
76
|
+
// Terminate
|
|
77
|
+
await payroll.terminate({ employeeId, terminationDate, reason: 'resignation' });
|
|
78
|
+
|
|
79
|
+
// Re-hire
|
|
80
|
+
await payroll.reHire({ employeeId, hireDate: new Date() });
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Listing Employees
|
|
84
|
+
|
|
85
|
+
Employee listing/queries are done at app level using your models directly:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// Use your EmployeeModel with mongokit or mongoose directly
|
|
89
|
+
const employees = await EmployeeModel.find({
|
|
90
|
+
organizationId,
|
|
91
|
+
'employment.status': 'active'
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Or with mongokit repository
|
|
95
|
+
const repo = createRepository(EmployeeModel);
|
|
96
|
+
const result = await repo.getAll({
|
|
97
|
+
filters: { organizationId, 'employment.status': 'active' },
|
|
98
|
+
page: 1,
|
|
99
|
+
limit: 100,
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Compensation
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// Update salary
|
|
107
|
+
await payroll.updateSalary({ employeeId, compensation: { baseSalary: 90000 } });
|
|
108
|
+
|
|
109
|
+
// Add allowance
|
|
110
|
+
await payroll.addAllowance({ employeeId, allowance: { type: 'housing', amount: 2000 } });
|
|
111
|
+
|
|
112
|
+
// Add deduction
|
|
113
|
+
await payroll.addDeduction({ employeeId, deduction: { type: 'loan', amount: 500 } });
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Bulk Processing
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
await payroll.processBulkPayroll({
|
|
120
|
+
organizationId,
|
|
121
|
+
period: { month: 1, year: 2024 },
|
|
122
|
+
onProgress: ({ current, total }) => console.log(`${current}/${total}`),
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Leave Management
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// Request leave
|
|
130
|
+
await payroll.requestLeave({
|
|
131
|
+
employeeId,
|
|
132
|
+
organizationId,
|
|
133
|
+
leaveType: 'annual',
|
|
134
|
+
startDate: new Date('2024-01-15'),
|
|
135
|
+
endDate: new Date('2024-01-17'),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Approve
|
|
139
|
+
await payroll.approveLeave({ leaveRequestId, approverId });
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Void / Reverse / Restore
|
|
143
|
+
|
|
144
|
+
Payroll corrections with full state tracking:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// Void unpaid payroll
|
|
148
|
+
await payroll.voidPayroll({
|
|
149
|
+
organizationId,
|
|
150
|
+
payrollRecordId,
|
|
151
|
+
reason: 'Test payroll',
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Reverse paid payroll (creates reversal transaction)
|
|
155
|
+
await payroll.reversePayroll({
|
|
156
|
+
organizationId,
|
|
157
|
+
payrollRecordId,
|
|
158
|
+
reason: 'Duplicate payment',
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Restore voided payroll
|
|
162
|
+
await payroll.restorePayroll({
|
|
163
|
+
organizationId,
|
|
164
|
+
payrollRecordId,
|
|
165
|
+
reason: 'Voided in error',
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**State Flow:**
|
|
170
|
+
```
|
|
171
|
+
PENDING → PROCESSING → PAID → REVERSED
|
|
172
|
+
↓ ↓
|
|
173
|
+
└──→ VOIDED ←── FAILED
|
|
174
|
+
↓
|
|
175
|
+
PENDING (restore)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Events
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
payroll.on('employee:hired', (payload) => {
|
|
182
|
+
console.log(`New hire: ${payload.employee.email}`);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
payroll.on('payroll:processed', (payload) => {
|
|
186
|
+
console.log(`Salary processed: ${payload.payrollRecord.id}`);
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Webhooks
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
await payroll.registerWebhook({
|
|
194
|
+
organizationId,
|
|
195
|
+
url: 'https://api.example.com/webhooks',
|
|
196
|
+
events: ['payroll:processed'],
|
|
197
|
+
secret: 'your-secret',
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Tenant Modes
|
|
202
|
+
|
|
203
|
+
### Single-Tenant (Recommended for most apps)
|
|
204
|
+
|
|
205
|
+
For apps serving one organization:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
const payroll = createPayrollInstance()
|
|
209
|
+
.withModels({ EmployeeModel, PayrollRecordModel, TransactionModel })
|
|
210
|
+
.forSingleTenant({ organizationId: YOUR_ORG_ID, autoInject: true })
|
|
211
|
+
.build();
|
|
212
|
+
|
|
213
|
+
// No organizationId needed - auto-injected
|
|
214
|
+
await payroll.hire({
|
|
215
|
+
employment: { email: 'dev@example.com', position: 'Engineer' },
|
|
216
|
+
compensation: { baseSalary: 80000, currency: 'USD' },
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
await payroll.processSalary({
|
|
220
|
+
employeeId,
|
|
221
|
+
period: { month: 1, year: 2024 },
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await payroll.getEmployee({ employeeId });
|
|
225
|
+
await payroll.updateEmployment({ employeeId, updates: { position: 'Lead' } });
|
|
226
|
+
await payroll.terminate({ employeeId, terminationDate, reason: 'resignation' });
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Multi-Tenant
|
|
230
|
+
|
|
231
|
+
For SaaS apps serving multiple organizations:
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
const payroll = createPayrollInstance()
|
|
235
|
+
.withModels({ EmployeeModel, PayrollRecordModel, TransactionModel })
|
|
236
|
+
.build();
|
|
237
|
+
|
|
238
|
+
// organizationId required on all operations
|
|
239
|
+
await payroll.hire({ organizationId, employment, compensation });
|
|
240
|
+
await payroll.processSalary({ organizationId, employeeId, period });
|
|
241
|
+
await payroll.getEmployee({ organizationId, employeeId });
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Pure Calculators
|
|
245
|
+
|
|
246
|
+
No database required - works in browser/serverless:
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
import {
|
|
250
|
+
calculateSalaryBreakdown,
|
|
251
|
+
calculateProRating,
|
|
252
|
+
calculateAttendanceDeduction
|
|
253
|
+
} from '@classytic/payroll/calculators';
|
|
254
|
+
|
|
255
|
+
// Calculate salary breakdown
|
|
256
|
+
const breakdown = calculateSalaryBreakdown({
|
|
257
|
+
baseSalary: 5000,
|
|
258
|
+
allowances: [{ type: 'housing', amount: 500 }],
|
|
259
|
+
deductions: [{ type: 'tax', percentage: 10 }],
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Pro-rate for mid-month joins
|
|
263
|
+
const proRated = calculateProRating({
|
|
264
|
+
amount: 5000,
|
|
265
|
+
startDate: new Date('2024-01-15'),
|
|
266
|
+
endDate: new Date('2024-01-31'),
|
|
267
|
+
totalDays: 31,
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Shift Compliance
|
|
272
|
+
|
|
273
|
+
Late penalties, overtime bonuses, night differentials:
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
import {
|
|
277
|
+
calculateShiftCompliance,
|
|
278
|
+
DEFAULT_ATTENDANCE_POLICY
|
|
279
|
+
} from '@classytic/payroll';
|
|
280
|
+
|
|
281
|
+
const result = calculateShiftCompliance({
|
|
282
|
+
policy: DEFAULT_ATTENDANCE_POLICY,
|
|
283
|
+
baseSalary: 5000,
|
|
284
|
+
shiftData: {
|
|
285
|
+
lateArrivals: [{ minutes: 15 }],
|
|
286
|
+
overtime: [{ hours: 2, type: 'weekday' }],
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
console.log(result.penalties); // Late penalties
|
|
291
|
+
console.log(result.bonuses); // Overtime bonuses
|
|
292
|
+
console.log(result.netAdjustment);
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Configuration
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
const payroll = createPayrollInstance()
|
|
299
|
+
.withModels({ EmployeeModel, PayrollRecordModel, TransactionModel })
|
|
300
|
+
.withConfig({
|
|
301
|
+
currency: 'USD',
|
|
302
|
+
payroll: {
|
|
303
|
+
attendanceIntegration: true,
|
|
304
|
+
autoCreateTransaction: true,
|
|
305
|
+
},
|
|
306
|
+
leave: {
|
|
307
|
+
enabled: true,
|
|
308
|
+
defaultBalances: { annual: 20, sick: 10 },
|
|
309
|
+
},
|
|
310
|
+
})
|
|
311
|
+
.build();
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Timeline Audit
|
|
315
|
+
|
|
316
|
+
Integrate with `@classytic/mongoose-timeline-audit` for WHO/WHAT/WHEN tracking:
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
import timelineAuditPlugin from '@classytic/mongoose-timeline-audit';
|
|
320
|
+
import { EMPLOYEE_TIMELINE_CONFIG, PAYROLL_EVENTS } from '@classytic/payroll';
|
|
321
|
+
|
|
322
|
+
employeeSchema.plugin(timelineAuditPlugin, EMPLOYEE_TIMELINE_CONFIG);
|
|
323
|
+
|
|
324
|
+
payroll.on('employee:hired', async ({ data }) => {
|
|
325
|
+
const employee = await Employee.findById(data.employee.id);
|
|
326
|
+
employee.addTimelineEvent(
|
|
327
|
+
PAYROLL_EVENTS.EMPLOYEE.HIRED,
|
|
328
|
+
`Hired as ${data.employee.position}`,
|
|
329
|
+
request
|
|
330
|
+
);
|
|
331
|
+
await employee.save();
|
|
332
|
+
});
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## TypeScript
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
import type {
|
|
339
|
+
EmployeeDocument,
|
|
340
|
+
PayrollRecordDocument,
|
|
341
|
+
LeaveRequestDocument,
|
|
342
|
+
Compensation,
|
|
343
|
+
PayrollBreakdown,
|
|
344
|
+
} from '@classytic/payroll';
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## Schemas & Indexes
|
|
348
|
+
|
|
349
|
+
The package exports schema creators and recommended indexes:
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
import {
|
|
353
|
+
createEmployeeSchema,
|
|
354
|
+
createPayrollRecordSchema,
|
|
355
|
+
applyEmployeeIndexes,
|
|
356
|
+
applyPayrollRecordIndexes,
|
|
357
|
+
} from '@classytic/payroll/schemas';
|
|
358
|
+
|
|
359
|
+
// Create schemas
|
|
360
|
+
const employeeSchema = createEmployeeSchema();
|
|
361
|
+
const payrollRecordSchema = createPayrollRecordSchema();
|
|
362
|
+
|
|
363
|
+
// Apply recommended indexes (optional)
|
|
364
|
+
applyEmployeeIndexes(employeeSchema);
|
|
365
|
+
applyPayrollRecordIndexes(payrollRecordSchema);
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**Note on duplicate prevention**: The package handles duplicate payroll detection at the application level (idempotency cache + existing record checks). No unique index is enforced by default, giving you control over your indexing strategy.
|
|
369
|
+
|
|
370
|
+
If you need DB-level uniqueness:
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
// Add your own unique index if needed
|
|
374
|
+
payrollRecordSchema.index(
|
|
375
|
+
{ organizationId: 1, employeeId: 1, 'period.month': 1, 'period.year': 1 },
|
|
376
|
+
{ unique: true }
|
|
377
|
+
);
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## Mongokit Integration
|
|
381
|
+
|
|
382
|
+
The payroll package is built on [@classytic/mongokit](https://github.com/classytic/mongokit) for powerful repository patterns and plugins.
|
|
383
|
+
|
|
384
|
+
### Audit Trail Plugin
|
|
385
|
+
|
|
386
|
+
Automatically track who created/updated records with the built-in audit plugin:
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
import { Repository } from '@classytic/mongokit';
|
|
390
|
+
import { payrollAuditPlugin } from '@classytic/payroll';
|
|
391
|
+
import { EmployeeModel } from './models';
|
|
392
|
+
|
|
393
|
+
// Create repository with audit plugin
|
|
394
|
+
const employeeRepo = new Repository(EmployeeModel, [
|
|
395
|
+
payrollAuditPlugin({
|
|
396
|
+
userId: currentUser._id,
|
|
397
|
+
userName: currentUser.name,
|
|
398
|
+
organizationId: orgId,
|
|
399
|
+
}),
|
|
400
|
+
]);
|
|
401
|
+
|
|
402
|
+
// All creates/updates now auto-capture audit fields
|
|
403
|
+
await employeeRepo.create({
|
|
404
|
+
employment: { email: 'dev@example.com' },
|
|
405
|
+
compensation: { baseSalary: 80000 },
|
|
406
|
+
// createdBy, createdAt automatically added
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
await employeeRepo.update(employeeId, {
|
|
410
|
+
$set: { 'employment.position': 'Senior' },
|
|
411
|
+
// updatedBy, updatedAt automatically added
|
|
412
|
+
});
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Available Audit Plugins
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
import {
|
|
419
|
+
payrollAuditPlugin, // Tracks creates & updates
|
|
420
|
+
readAuditPlugin, // Tracks read access (compliance)
|
|
421
|
+
fullAuditPlugin, // Combines both + comprehensive events
|
|
422
|
+
} from '@classytic/payroll';
|
|
423
|
+
|
|
424
|
+
// Full audit with compliance tracking
|
|
425
|
+
const repo = new Repository(PayrollRecordModel, [
|
|
426
|
+
fullAuditPlugin({
|
|
427
|
+
userId: currentUser._id,
|
|
428
|
+
organizationId: orgId,
|
|
429
|
+
}),
|
|
430
|
+
]);
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Custom Mongokit Plugins
|
|
434
|
+
|
|
435
|
+
Create your own plugins for cross-cutting concerns:
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
import type { Repository } from '@classytic/mongokit';
|
|
439
|
+
|
|
440
|
+
// Example: Auto-encrypt sensitive fields
|
|
441
|
+
function encryptionPlugin(secretKey: string) {
|
|
442
|
+
return (repo: Repository) => {
|
|
443
|
+
repo.on('before:create', async (ctx) => {
|
|
444
|
+
if (ctx.data.ssn) {
|
|
445
|
+
ctx.data.ssn = encrypt(ctx.data.ssn, secretKey);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
repo.on('after:getById', async (ctx) => {
|
|
450
|
+
if (ctx.result?.ssn) {
|
|
451
|
+
ctx.result.ssn = decrypt(ctx.result.ssn, secretKey);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Apply to repository
|
|
458
|
+
const repo = new Repository(EmployeeModel, [
|
|
459
|
+
encryptionPlugin(process.env.SECRET_KEY),
|
|
460
|
+
payrollAuditPlugin({ userId, organizationId }),
|
|
461
|
+
]);
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### Transaction Management
|
|
465
|
+
|
|
466
|
+
Mongokit provides clean transaction handling:
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
import { Repository } from '@classytic/mongokit';
|
|
470
|
+
|
|
471
|
+
const payrollRepo = new Repository(PayrollRecordModel);
|
|
472
|
+
|
|
473
|
+
// Automatic transaction management
|
|
474
|
+
const result = await payrollRepo.withTransaction(async (session) => {
|
|
475
|
+
// All operations use the same session
|
|
476
|
+
const payroll = await payrollRepo.create(payrollData, { session });
|
|
477
|
+
const transaction = await transactionRepo.create(txData, { session });
|
|
478
|
+
|
|
479
|
+
// Automatic commit on success, rollback on error
|
|
480
|
+
return { payroll, transaction };
|
|
481
|
+
});
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### Type-Safe Utilities
|
|
485
|
+
|
|
486
|
+
Use the new type guards for cleaner code:
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
import {
|
|
490
|
+
getEmployeeEmail,
|
|
491
|
+
getEmployeeName,
|
|
492
|
+
isGuestEmployee,
|
|
493
|
+
isDuplicateKeyError,
|
|
494
|
+
parseDuplicateKeyError,
|
|
495
|
+
} from '@classytic/payroll';
|
|
496
|
+
|
|
497
|
+
// Type-safe employee identity access
|
|
498
|
+
const email = getEmployeeEmail(employee); // Works for guest & user-linked
|
|
499
|
+
const name = getEmployeeName(employee); // Fallback to employeeId
|
|
500
|
+
|
|
501
|
+
if (isGuestEmployee(employee)) {
|
|
502
|
+
console.log('Guest employee:', employee.employeeId);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Type-safe error handling
|
|
506
|
+
try {
|
|
507
|
+
await payroll.hire({ ... });
|
|
508
|
+
} catch (error) {
|
|
509
|
+
if (isDuplicateKeyError(error)) {
|
|
510
|
+
const field = parseDuplicateKeyError(error);
|
|
511
|
+
console.error(`Duplicate ${field}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
## Security
|
|
517
|
+
|
|
518
|
+
- **Multi-tenant isolation**: All queries scoped by `organizationId`
|
|
519
|
+
- **Repository plugin**: Auto-injects tenant filter on all operations
|
|
520
|
+
- **Secure lookups**: `findEmployeeSecure()` enforces org boundaries
|
|
521
|
+
- **State machines**: Prevent invalid status transitions
|
|
522
|
+
|
|
523
|
+
## License
|
|
524
|
+
|
|
525
|
+
MIT
|