@classytic/payroll 1.0.0 → 2.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +525 -574
  2. package/dist/calculators/index.d.ts +300 -0
  3. package/dist/calculators/index.js +304 -0
  4. package/dist/calculators/index.js.map +1 -0
  5. package/dist/employee-identity-Cq2wo9-2.d.ts +490 -0
  6. package/dist/index-DjB72l6e.d.ts +3742 -0
  7. package/dist/index.d.ts +2924 -0
  8. package/dist/index.js +10648 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/prorating.calculator-C7sdFiG2.d.ts +135 -0
  11. package/dist/schemas/index.d.ts +4 -0
  12. package/dist/schemas/index.js +1452 -0
  13. package/dist/schemas/index.js.map +1 -0
  14. package/dist/types-BVDjiVGS.d.ts +1856 -0
  15. package/dist/utils/index.d.ts +995 -0
  16. package/dist/utils/index.js +1629 -0
  17. package/dist/utils/index.js.map +1 -0
  18. package/package.json +77 -24
  19. package/src/config.js +0 -177
  20. package/src/core/compensation.manager.js +0 -242
  21. package/src/core/employment.manager.js +0 -224
  22. package/src/core/payroll.manager.js +0 -499
  23. package/src/enums.js +0 -141
  24. package/src/factories/compensation.factory.js +0 -198
  25. package/src/factories/employee.factory.js +0 -173
  26. package/src/factories/payroll.factory.js +0 -247
  27. package/src/hrm.orchestrator.js +0 -139
  28. package/src/index.js +0 -172
  29. package/src/init.js +0 -41
  30. package/src/models/payroll-record.model.js +0 -126
  31. package/src/plugins/employee.plugin.js +0 -157
  32. package/src/schemas/employment.schema.js +0 -126
  33. package/src/services/compensation.service.js +0 -231
  34. package/src/services/employee.service.js +0 -162
  35. package/src/services/payroll.service.js +0 -213
  36. package/src/utils/calculation.utils.js +0 -91
  37. package/src/utils/date.utils.js +0 -120
  38. package/src/utils/logger.js +0 -36
  39. package/src/utils/query-builders.js +0 -185
  40. package/src/utils/validation.utils.js +0 -122
package/README.md CHANGED
@@ -1,574 +1,525 @@
1
- # 🎯 HRM Library - Human Resource Management
2
-
3
- Modern, flexible, production-ready HRM system following Stripe/Passport.js architecture patterns.
4
-
5
- ## 🌟 Key Features
6
-
7
- ### Multi-Tenant Support
8
- - Same user can be employee in multiple organizations
9
- - Complete data isolation per tenant
10
- - Re-hiring support with full employment history
11
-
12
- ### Smart Payroll
13
- - **Pro-rated calculations** (mid-month hires)
14
- - **Attendance integration** (unpaid leave auto-deduction)
15
- - **Automatic deductions** (loans, advances, tax)
16
- - **Bulk payroll processing**
17
- - **Transaction integration** (seamless with existing system)
18
-
19
- ### Data Retention
20
- - **Auto-deletion**: PayrollRecords expire after 2 years (MongoDB TTL)
21
- - **Export before deletion**: Required export for compliance
22
- - **Configurable retention**: Adjust via `HRM_CONFIG`
23
-
24
- ### Flexible Architecture
25
- - **Reusable schemas**: Merge with your custom fields
26
- - **Plugin system**: Adds methods, virtuals, indexes
27
- - **Clean DSL**: `hrm.hire()`, `hrm.processSalary()`, `hrm.terminate()`
28
- - **Dependency injection**: Models injected at bootstrap
29
-
30
- ## 📁 Structure
31
-
32
- ```
33
- lib/hrm/
34
- ├── index.js # Public exports
35
- ├── init.js # Bootstrap initialization
36
- ├── hrm.orchestrator.js # Clean API (Stripe-like)
37
- ├── enums.js # Single source of truth
38
- ├── config.js # Configurable settings
39
-
40
- ├── models/
41
- │ └── payroll-record.model.js # Universal payroll ledger
42
-
43
- ├── schemas/
44
- │ └── employment.schema.js # Reusable mongoose schemas
45
-
46
- ├── plugins/
47
- │ └── employee.plugin.js # Mongoose plugin (methods/virtuals)
48
-
49
- ├── core/ # Domain business logic
50
- │ ├── employment.manager.js # Hire/terminate operations
51
- │ ├── compensation.manager.js # Salary/allowance operations
52
- │ └── payroll.manager.js # Payroll processing
53
-
54
- ├── factories/ # Clean object creation
55
- │ ├── employee.factory.js # Employee creation with defaults
56
- │ ├── payroll.factory.js # Payroll generation
57
- │ └── compensation.factory.js # Compensation breakdown
58
-
59
- ├── services/ # High-level operations
60
- │ ├── employee.service.js # Employee CRUD + queries
61
- │ ├── payroll.service.js # Batch payroll processing
62
- │ └── compensation.service.js # Compensation calculations
63
-
64
- └── utils/ # Pure, reusable functions
65
- ├── date.utils.js # Date calculations
66
- ├── calculation.utils.js # Salary calculations
67
- ├── validation.utils.js # Validators
68
- └── query-builders.js # Fluent query API
69
- ```
70
-
71
- ## 🚀 Quick Start
72
-
73
- ### 1. Create Your Employee Model
74
-
75
- ```javascript
76
- // modules/employee/employee.model.js
77
- import mongoose from 'mongoose';
78
- import { employmentFields, employeePlugin } from '#lib/hrm/index.js';
79
-
80
- const employeeSchema = new mongoose.Schema({
81
- // Core HRM fields (required)
82
- ...employmentFields,
83
-
84
- // Your custom fields
85
- certifications: [{ name: String, issuedDate: Date }],
86
- specializations: [String],
87
- emergencyContact: { name: String, phone: String },
88
- // ... any other fields you need
89
- });
90
-
91
- // Apply HRM plugin (adds methods, virtuals, indexes)
92
- employeeSchema.plugin(employeePlugin);
93
-
94
- export default mongoose.model('Employee', employeeSchema);
95
- ```
96
-
97
- ### 2. Bootstrap Integration
98
-
99
- ```javascript
100
- // bootstrap/hrm.js
101
- import { initializeHRM } from '#lib/hrm/index.js';
102
- import Employee from '../modules/employee/employee.model.js';
103
- import PayrollRecord from '#lib/hrm/models/payroll-record.model.js';
104
- import Transaction from '../modules/transaction/transaction.model.js';
105
- import Attendance from '#lib/attendance/models/attendance.model.js';
106
-
107
- export async function loadHRM() {
108
- initializeHRM({
109
- EmployeeModel: Employee,
110
- PayrollRecordModel: PayrollRecord,
111
- TransactionModel: Transaction,
112
- AttendanceModel: Attendance, // Optional
113
- });
114
- }
115
- ```
116
-
117
- ### 3. Use the HRM API
118
-
119
- ```javascript
120
- import { hrm } from '#lib/hrm/index.js';
121
-
122
- // Hire employee
123
- const employee = await hrm.hire({
124
- organizationId,
125
- userId,
126
- employment: {
127
- employeeId: 'EMP-001',
128
- type: 'full_time',
129
- department: 'training',
130
- position: 'Senior Trainer',
131
- hireDate: new Date(),
132
- },
133
- compensation: {
134
- baseAmount: 50000,
135
- frequency: 'monthly',
136
- allowances: [
137
- { type: 'housing', amount: 10000 },
138
- { type: 'transport', amount: 5000 }
139
- ]
140
- },
141
- bankDetails: {
142
- accountName: 'John Doe',
143
- accountNumber: '1234567890',
144
- bankName: 'Example Bank'
145
- },
146
- context: { userId: hrManagerId }
147
- });
148
-
149
- // Process salary (creates Transaction automatically)
150
- const result = await hrm.processSalary({
151
- employeeId: employee._id,
152
- month: 11,
153
- year: 2025,
154
- paymentDate: new Date(),
155
- paymentMethod: 'bank',
156
- context: { userId: hrManagerId }
157
- });
158
-
159
- // Bulk payroll (all active employees)
160
- const results = await hrm.processBulkPayroll({
161
- organizationId,
162
- month: 11,
163
- year: 2025,
164
- context: { userId: hrManagerId }
165
- });
166
- ```
167
-
168
-
169
- ## 🎨 Complete API Reference
170
-
171
- ### Employment Lifecycle
172
-
173
- ```javascript
174
- // Hire
175
- await hrm.hire({ organizationId, userId, employment, compensation, bankDetails, context });
176
-
177
- // Update employment details
178
- await hrm.updateEmployment({ employeeId, updates: { department: 'management' }, context });
179
-
180
- // Terminate
181
- await hrm.terminate({ employeeId, terminationDate, reason: 'resignation', notes, context });
182
-
183
- // Re-hire (same employee, new stint)
184
- await hrm.reHire({ employeeId, hireDate, position, compensation, context });
185
-
186
- // List employees
187
- await hrm.listEmployees({
188
- organizationId,
189
- filters: { status: 'active', department: 'training', minSalary: 40000 },
190
- pagination: { page: 1, limit: 20 }
191
- });
192
-
193
- // Get single employee
194
- await hrm.getEmployee({ employeeId, populateUser: true });
195
- ```
196
-
197
- ### Compensation Management
198
-
199
- ```javascript
200
- // Update salary
201
- await hrm.updateSalary({
202
- employeeId,
203
- compensation: { baseAmount: 60000 },
204
- effectiveFrom: new Date(),
205
- context
206
- });
207
-
208
- // Add allowance
209
- await hrm.addAllowance({
210
- employeeId,
211
- type: 'meal',
212
- amount: 3000,
213
- taxable: true,
214
- recurring: true,
215
- context
216
- });
217
-
218
- // Remove allowance
219
- await hrm.removeAllowance({ employeeId, type: 'meal', context });
220
-
221
- // Add deduction
222
- await hrm.addDeduction({
223
- employeeId,
224
- type: 'loan',
225
- amount: 5000,
226
- auto: true, // Auto-deduct from salary
227
- description: 'Personal loan repayment',
228
- context
229
- });
230
-
231
- // Remove deduction
232
- await hrm.removeDeduction({ employeeId, type: 'loan', context });
233
-
234
- // Update bank details
235
- await hrm.updateBankDetails({
236
- employeeId,
237
- bankDetails: { accountNumber: '9876543210', bankName: 'New Bank' },
238
- context
239
- });
240
- ```
241
-
242
- ### Payroll Processing
243
-
244
- ```javascript
245
- // Process single salary
246
- await hrm.processSalary({
247
- employeeId,
248
- month: 11,
249
- year: 2025,
250
- paymentDate: new Date(),
251
- paymentMethod: 'bank',
252
- context
253
- });
254
-
255
- // Bulk payroll
256
- await hrm.processBulkPayroll({
257
- organizationId,
258
- month: 11,
259
- year: 2025,
260
- employeeIds: [], // Empty = all active employees
261
- paymentDate: new Date(),
262
- paymentMethod: 'bank',
263
- context
264
- });
265
-
266
- // Payroll history
267
- await hrm.payrollHistory({
268
- employeeId,
269
- organizationId,
270
- month: 11,
271
- year: 2025,
272
- status: 'paid',
273
- pagination: { page: 1, limit: 20 }
274
- });
275
-
276
- // Payroll summary
277
- await hrm.payrollSummary({
278
- organizationId,
279
- month: 11,
280
- year: 2025
281
- });
282
-
283
- // Export payroll data (before auto-deletion)
284
- const records = await hrm.exportPayroll({
285
- organizationId,
286
- startDate: new Date('2023-01-01'),
287
- endDate: new Date('2023-12-31'),
288
- format: 'json'
289
- });
290
- ```
291
-
292
- ## 📊 Data Models
293
-
294
- ### Employee (Your Model + HRM Fields)
295
-
296
- ```javascript
297
- {
298
- // Identity & tenant
299
- userId: ObjectId, // Links to User
300
- organizationId: ObjectId, // Multi-tenant isolation
301
- employeeId: "EMP-001", // Custom ID (unique per org)
302
-
303
- // Employment
304
- employmentType: "full_time", // full_time, part_time, contract, intern
305
- status: "active", // active, on_leave, suspended, terminated
306
- department: "training",
307
- position: "Senior Trainer",
308
-
309
- // Dates
310
- hireDate: Date,
311
- terminationDate: Date,
312
- probationEndDate: Date,
313
-
314
- // Employment history (re-hiring support)
315
- employmentHistory: [{
316
- hireDate: Date,
317
- terminationDate: Date,
318
- reason: String,
319
- finalSalary: Number
320
- }],
321
-
322
- // Compensation
323
- compensation: {
324
- baseAmount: 50000,
325
- frequency: "monthly",
326
- currency: "BDT",
327
-
328
- allowances: [
329
- { type: "housing", amount: 10000, taxable: true },
330
- { type: "transport", amount: 5000, taxable: false }
331
- ],
332
-
333
- deductions: [
334
- { type: "loan", amount: 2000, auto: true }
335
- ],
336
-
337
- grossSalary: 65000, // Auto-calculated
338
- netSalary: 63000, // Auto-calculated
339
- },
340
-
341
- // Bank details
342
- bankDetails: {
343
- accountName: String,
344
- accountNumber: String,
345
- bankName: String
346
- },
347
-
348
- // Payroll stats (pre-calculated)
349
- payrollStats: {
350
- totalPaid: 500000,
351
- lastPaymentDate: Date,
352
- nextPaymentDate: Date,
353
- paymentsThisYear: 10,
354
- averageMonthly: 50000
355
- },
356
-
357
- // YOUR CUSTOM FIELDS
358
- certifications: [...],
359
- specializations: [...],
360
- emergencyContact: {...}
361
- }
362
- ```
363
-
364
- ### PayrollRecord (Universal Ledger)
365
-
366
- ```javascript
367
- {
368
- organizationId: ObjectId,
369
- employeeId: ObjectId,
370
- userId: ObjectId,
371
-
372
- period: {
373
- month: 11,
374
- year: 2025,
375
- startDate: Date,
376
- endDate: Date,
377
- payDate: Date
378
- },
379
-
380
- breakdown: {
381
- baseAmount: 50000,
382
- allowances: [...],
383
- deductions: [...],
384
- grossSalary: 65000,
385
- netSalary: 63000,
386
-
387
- // Smart calculations
388
- workingDays: 30,
389
- actualDays: 25, // If joined mid-month
390
- proRatedAmount: 41667, // Pro-rated salary
391
- attendanceDeduction: 0, // From attendance integration
392
- overtimeAmount: 0,
393
- bonusAmount: 0
394
- },
395
-
396
- transactionId: ObjectId, // Links to Transaction
397
- status: "paid",
398
- paidAt: Date,
399
-
400
- // Export tracking
401
- exported: false, // Must export before TTL deletion
402
- exportedAt: Date
403
- }
404
- ```
405
-
406
- ## ⚙️ Configuration
407
-
408
- ```javascript
409
- // lib/hrm/config.js
410
- export const HRM_CONFIG = {
411
- dataRetention: {
412
- payrollRecordsTTL: 63072000, // 2 years in seconds
413
- exportWarningDays: 30, // Warn before deletion
414
- archiveBeforeDeletion: true,
415
- },
416
-
417
- payroll: {
418
- defaultCurrency: 'BDT',
419
- allowProRating: true, // Mid-month hire calculations
420
- attendanceIntegration: true, // Unpaid leave deductions
421
- autoDeductions: true, // Auto-deduct loans/advances
422
- },
423
-
424
- employment: {
425
- defaultProbationMonths: 3,
426
- allowReHiring: true, // Re-hire terminated employees
427
- trackEmploymentHistory: true,
428
- },
429
-
430
- validation: {
431
- requireBankDetails: false,
432
- allowMultiTenantEmployees: true, // Same user in multiple orgs
433
- },
434
- };
435
- ```
436
-
437
- ## 🔑 Key Concepts
438
-
439
- ### Multi-Tenant Architecture
440
-
441
- Same user can work at multiple gyms:
442
- ```javascript
443
- // User "john@example.com" (userId: 123)
444
- // Works at Gym A
445
- { userId: 123, organizationId: "gymA", employeeId: "EMP-001", status: "active" }
446
-
447
- // Also works at Gym B
448
- { userId: 123, organizationId: "gymB", employeeId: "STAFF-05", status: "active" }
449
- ```
450
-
451
- Indexes ensure uniqueness:
452
- - `{ userId: 1, organizationId: 1 }` unique
453
- - `{ organizationId: 1, employeeId: 1 }` unique
454
-
455
- ### Re-Hiring Flow
456
-
457
- ```javascript
458
- // Employee leaves
459
- await hrm.terminate({
460
- employeeId,
461
- reason: 'resignation',
462
- terminationDate: new Date()
463
- });
464
- // status: 'terminated', data preserved
465
-
466
- // Employee comes back
467
- await hrm.reHire({
468
- employeeId,
469
- hireDate: new Date(),
470
- position: 'Manager', // Optional: new position
471
- compensation: { baseAmount: 60000 } // Optional: new salary
472
- });
473
- // status: 'active', previous stint added to employmentHistory[]
474
- ```
475
-
476
- ### Smart Payroll Calculations
477
-
478
- **Pro-Rating (Mid-Month Hire)**:
479
- ```javascript
480
- // Employee hired on Nov 15
481
- // Working days: 15 out of 30
482
- // Base salary: 60,000
483
- // Pro-rated: 60,000 × (15/30) = 30,000
484
- ```
485
-
486
- **Attendance Integration**:
487
- ```javascript
488
- // Monthly salary: 60,000
489
- // Working days: 30
490
- // Attended days: 25
491
- // Absent days: 5
492
- // Daily rate: 60,000 / 30 = 2,000
493
- // Deduction: 5 × 2,000 = 10,000
494
- // Final: 60,000 - 10,000 = 50,000
495
- ```
496
-
497
- **Auto Deductions**:
498
- ```javascript
499
- compensation: {
500
- baseAmount: 60000,
501
- allowances: [{ type: 'housing', amount: 10000 }],
502
- deductions: [
503
- { type: 'loan', amount: 5000, auto: true }, // Auto-deduct
504
- { type: 'tax', amount: 3000, auto: true }
505
- ],
506
- grossSalary: 70000,
507
- netSalary: 62000 // 70000 - 5000 - 3000
508
- }
509
- ```
510
-
511
- ### Transaction Integration
512
-
513
- Every salary payment creates a Transaction:
514
- ```javascript
515
- {
516
- organizationId,
517
- type: 'expense',
518
- category: 'salary',
519
- amount: 63000,
520
- method: 'bank',
521
- status: 'completed',
522
- referenceId: employeeId,
523
- referenceModel: 'Employee',
524
- metadata: {
525
- employeeId: 'EMP-001',
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
+ [![npm](https://img.shields.io/npm/v/@classytic/payroll)](https://www.npmjs.com/package/@classytic/payroll)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue)](https://www.typescriptlang.org/)
7
+ [![MIT](https://img.shields.io/badge/License-MIT-yellow)](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