@classytic/payroll 1.0.0 → 2.8.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.
Files changed (45) hide show
  1. package/README.md +535 -574
  2. package/dist/attendance.calculator-BZcv2iii.d.ts +336 -0
  3. package/dist/calculators/index.d.ts +4 -0
  4. package/dist/calculators/index.js +439 -0
  5. package/dist/calculators/index.js.map +1 -0
  6. package/dist/core/index.d.ts +321 -0
  7. package/dist/core/index.js +1962 -0
  8. package/dist/core/index.js.map +1 -0
  9. package/dist/error-helpers-Bm6lMny2.d.ts +740 -0
  10. package/dist/index-BKLkuSAs.d.ts +3858 -0
  11. package/dist/index.d.ts +2684 -0
  12. package/dist/index.js +11454 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/payroll-states-DBt0XVm-.d.ts +598 -0
  15. package/dist/prorating.calculator-C33fWBQf.d.ts +135 -0
  16. package/dist/schemas/index.d.ts +4 -0
  17. package/dist/schemas/index.js +1472 -0
  18. package/dist/schemas/index.js.map +1 -0
  19. package/dist/types-bZdAJueH.d.ts +2271 -0
  20. package/dist/utils/index.d.ts +1007 -0
  21. package/dist/utils/index.js +1789 -0
  22. package/dist/utils/index.js.map +1 -0
  23. package/package.json +81 -24
  24. package/src/config.js +0 -177
  25. package/src/core/compensation.manager.js +0 -242
  26. package/src/core/employment.manager.js +0 -224
  27. package/src/core/payroll.manager.js +0 -499
  28. package/src/enums.js +0 -141
  29. package/src/factories/compensation.factory.js +0 -198
  30. package/src/factories/employee.factory.js +0 -173
  31. package/src/factories/payroll.factory.js +0 -247
  32. package/src/hrm.orchestrator.js +0 -139
  33. package/src/index.js +0 -172
  34. package/src/init.js +0 -41
  35. package/src/models/payroll-record.model.js +0 -126
  36. package/src/plugins/employee.plugin.js +0 -157
  37. package/src/schemas/employment.schema.js +0 -126
  38. package/src/services/compensation.service.js +0 -231
  39. package/src/services/employee.service.js +0 -162
  40. package/src/services/payroll.service.js +0 -213
  41. package/src/utils/calculation.utils.js +0 -91
  42. package/src/utils/date.utils.js +0 -120
  43. package/src/utils/logger.js +0 -36
  44. package/src/utils/query-builders.js +0 -185
  45. package/src/utils/validation.utils.js +0 -122
package/README.md CHANGED
@@ -1,574 +1,535 @@
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
+ HRM & Payroll for MongoDB. Multi-tenant, event-driven, type-safe.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @classytic/payroll mongoose @classytic/mongokit
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { createPayrollInstance } from '@classytic/payroll';
15
+
16
+ const payroll = createPayrollInstance()
17
+ .withModels({ EmployeeModel, PayrollRecordModel, TransactionModel })
18
+ .build();
19
+
20
+ // Hire
21
+ await payroll.hire({
22
+ organizationId,
23
+ employment: { email: 'dev@example.com', position: 'Engineer', hireDate: new Date() },
24
+ compensation: { baseAmount: 80000, currency: 'USD', frequency: 'monthly' },
25
+ });
26
+
27
+ // Process salary
28
+ await payroll.processSalary({
29
+ organizationId,
30
+ employeeId,
31
+ month: 1,
32
+ year: 2024,
33
+ });
34
+ ```
35
+
36
+ ## Package Exports
37
+
38
+ | Entry Point | Description |
39
+ |-------------|-------------|
40
+ | `@classytic/payroll` | Main API: Payroll class, types, schemas, errors |
41
+ | `@classytic/payroll/calculators` | Pure calculation functions (no DB required) |
42
+ | `@classytic/payroll/utils` | Date, money, validation utilities |
43
+ | `@classytic/payroll/schemas` | Mongoose schema factories |
44
+
45
+ ---
46
+
47
+ ## Employee Operations
48
+
49
+ ```typescript
50
+ // Hire
51
+ await payroll.hire({
52
+ organizationId,
53
+ employment: { email, employeeId, position, department, hireDate },
54
+ compensation: { baseAmount, currency, frequency },
55
+ });
56
+
57
+ // Get employee
58
+ const emp = await payroll.getEmployee({ employeeId, organizationId });
59
+
60
+ // Update employment
61
+ await payroll.updateEmployment({
62
+ employeeId,
63
+ organizationId,
64
+ updates: { position: 'Senior Engineer', department: 'engineering' },
65
+ });
66
+
67
+ // Terminate
68
+ await payroll.terminate({
69
+ employeeId,
70
+ organizationId,
71
+ terminationDate: new Date(),
72
+ reason: 'resignation',
73
+ });
74
+
75
+ // Re-hire
76
+ await payroll.reHire({ employeeId, organizationId, hireDate: new Date() });
77
+ ```
78
+
79
+ ## Compensation
80
+
81
+ ```typescript
82
+ // Update salary
83
+ await payroll.updateSalary({
84
+ employeeId,
85
+ organizationId,
86
+ compensation: { baseAmount: 90000 },
87
+ effectiveFrom: new Date(),
88
+ });
89
+
90
+ // Add allowance
91
+ await payroll.addAllowance({
92
+ employeeId,
93
+ organizationId,
94
+ allowance: {
95
+ type: 'housing', // 'housing' | 'transport' | 'meal' | 'mobile' | 'medical' | 'bonus' | 'other'
96
+ amount: 2000,
97
+ taxable: true,
98
+ },
99
+ });
100
+
101
+ // Add deduction
102
+ await payroll.addDeduction({
103
+ employeeId,
104
+ organizationId,
105
+ deduction: {
106
+ type: 'provident_fund', // 'tax' | 'loan' | 'advance' | 'provident_fund' | 'insurance'
107
+ amount: 500,
108
+ auto: true,
109
+ },
110
+ });
111
+
112
+ // Update bank details
113
+ await payroll.updateBankDetails({
114
+ employeeId,
115
+ organizationId,
116
+ bankDetails: { accountNumber, bankName, routingNumber },
117
+ });
118
+ ```
119
+
120
+ ### Payment Frequencies
121
+
122
+ Supports multiple payment frequencies with automatic tax annualization:
123
+
124
+ | Frequency | baseAmount | Periods/Year | Example ($104k/year) |
125
+ |-----------|------------|--------------|----------------------|
126
+ | `monthly` | Monthly salary | 12 | $8,666.67/month |
127
+ | `bi_weekly` | Bi-weekly wage | 26 | $4,000/bi-week |
128
+ | `weekly` | Weekly wage | 52 | $2,000/week |
129
+ | `daily` | Daily rate | 365 | $285/day |
130
+ | `hourly` | Hourly rate | 2080 | $50/hour |
131
+
132
+ Tax is calculated consistently: same annual income = same annual tax, regardless of frequency.
133
+
134
+ ## Payroll Processing
135
+
136
+ ```typescript
137
+ // Single employee
138
+ const result = await payroll.processSalary({
139
+ organizationId,
140
+ employeeId,
141
+ month: 1,
142
+ year: 2024,
143
+ paymentDate: new Date(),
144
+ paymentMethod: 'bank',
145
+ payrollRunType: 'regular', // 'regular' | 'supplemental' | 'retroactive' | 'off-cycle'
146
+ });
147
+ // Returns: { employee, payrollRecord, transaction }
148
+
149
+ // Bulk processing
150
+ const bulk = await payroll.processBulkPayroll({
151
+ organizationId, // Optional in single-tenant mode or with context.organizationId
152
+ month: 1,
153
+ year: 2024,
154
+ employeeIds: [], // Optional: specific employees (default: all active + on_leave)
155
+ batchSize: 50,
156
+ concurrency: 5,
157
+ onProgress: (p) => console.log(`${p.percentage}%`),
158
+ });
159
+ // Returns: { successCount, failCount, totalAmount, successful[], failed[] }
160
+ ```
161
+
162
+ ### Duplicate Protection
163
+
164
+ The package provides database-level duplicate protection via a unique compound index:
165
+
166
+ ```typescript
167
+ // Unique index on: (organizationId, employeeId, period.month, period.year, payrollRunType)
168
+ // With partial filter: { isVoided: { $eq: false } }
169
+
170
+ // This allows:
171
+ // - One active record per employee per period per run type
172
+ // - Multiple run types in same period (regular + supplemental)
173
+ // - Re-processing after voiding (requires restorePayroll() first)
174
+ // - Re-processing after reversing
175
+ ```
176
+
177
+ **Important**: Voided records require `restorePayroll()` before re-processing. Voided is a terminal state that preserves audit trail.
178
+
179
+ ## Two-Phase Export
180
+
181
+ Safe export that only marks records after downstream confirms receipt:
182
+
183
+ ```typescript
184
+ // Phase 1: Prepare (records NOT marked)
185
+ const { records, exportId } = await payroll.prepareExport({
186
+ organizationId,
187
+ startDate: new Date('2024-01-01'),
188
+ endDate: new Date('2024-01-31'),
189
+ });
190
+
191
+ // Send to external system...
192
+
193
+ // Phase 2a: Confirm success (marks records)
194
+ await payroll.confirmExport({ organizationId, exportId });
195
+
196
+ // Phase 2b: Cancel if failed (records stay unmarked)
197
+ await payroll.cancelExport({ organizationId, exportId, reason: 'API error' });
198
+ ```
199
+
200
+ ## Void / Reverse / Restore
201
+
202
+ ```typescript
203
+ // Void unpaid payroll (pending, processing, failed)
204
+ await payroll.voidPayroll({
205
+ organizationId,
206
+ payrollRecordId,
207
+ reason: 'Test payroll',
208
+ });
209
+
210
+ // Reverse paid payroll (creates reversal transaction)
211
+ await payroll.reversePayroll({
212
+ organizationId,
213
+ payrollRecordId,
214
+ reason: 'Duplicate payment',
215
+ });
216
+
217
+ // Restore voided payroll (blocked if replacement exists)
218
+ await payroll.restorePayroll({
219
+ organizationId,
220
+ payrollRecordId,
221
+ reason: 'Voided in error',
222
+ });
223
+ ```
224
+
225
+ **Status Flow:**
226
+ ```
227
+ PENDING PROCESSING → PAID → REVERSED
228
+ ↓ ↓
229
+ └──→ VOIDED ←── FAILED
230
+
231
+ PENDING (restore)
232
+ ```
233
+
234
+ ## Leave Management
235
+
236
+ ```typescript
237
+ // Request leave
238
+ await payroll.requestLeave({
239
+ employeeId,
240
+ organizationId,
241
+ leaveType: 'annual', // 'annual' | 'sick' | 'unpaid' | 'maternity' | 'paternity'
242
+ startDate: new Date('2024-03-01'),
243
+ endDate: new Date('2024-03-05'),
244
+ reason: 'Vacation',
245
+ });
246
+
247
+ // Approve
248
+ await payroll.approveLeave({ leaveRequestId, organizationId, approverId: managerId });
249
+
250
+ // Reject
251
+ await payroll.rejectLeave({
252
+ leaveRequestId,
253
+ organizationId,
254
+ rejectedBy: managerId,
255
+ rejectionReason: 'Insufficient leave balance',
256
+ });
257
+
258
+ // Get balance
259
+ const balance = await payroll.getLeaveBalance({ employeeId, organizationId });
260
+ // { annual: { total: 20, used: 5, remaining: 15 }, sick: {...}, ... }
261
+ ```
262
+
263
+ ---
264
+
265
+ ## Pure Calculators (No DB Required)
266
+
267
+ Import from `@classytic/payroll/calculators` for client-side or serverless:
268
+
269
+ ```typescript
270
+ import {
271
+ calculateSalaryBreakdown,
272
+ calculateProRating,
273
+ calculateAttendanceDeduction,
274
+ } from '@classytic/payroll/calculators';
275
+ ```
276
+
277
+ ### Salary Breakdown
278
+
279
+ ```typescript
280
+ const breakdown = calculateSalaryBreakdown({
281
+ employee: {
282
+ hireDate: new Date('2024-01-01'),
283
+ terminationDate: null,
284
+ compensation: {
285
+ baseAmount: 100000,
286
+ frequency: 'monthly',
287
+ currency: 'USD',
288
+ allowances: [
289
+ { type: 'housing', amount: 20000, taxable: true },
290
+ { type: 'transport', amount: 5000, taxable: true },
291
+ ],
292
+ deductions: [
293
+ { type: 'provident_fund', amount: 5000, auto: true },
294
+ ],
295
+ },
296
+ },
297
+ period: {
298
+ month: 3,
299
+ year: 2024,
300
+ startDate: new Date('2024-03-01'),
301
+ endDate: new Date('2024-03-31'),
302
+ },
303
+ attendance: {
304
+ expectedDays: 22,
305
+ actualDays: 20,
306
+ },
307
+ config: {
308
+ allowProRating: true,
309
+ autoDeductions: true,
310
+ defaultCurrency: 'USD',
311
+ attendanceIntegration: true,
312
+ },
313
+ taxBrackets: [
314
+ { min: 0, max: 600000, rate: 0 },
315
+ { min: 600000, max: 1200000, rate: 0.1 },
316
+ { min: 1200000, max: Infinity, rate: 0.2 },
317
+ ],
318
+ });
319
+
320
+ // Returns PayrollBreakdown
321
+ {
322
+ baseAmount: number,
323
+ allowances: Array<{ type, amount, taxable }>,
324
+ deductions: Array<{ type, amount, description }>,
325
+ grossSalary: number,
326
+ netSalary: number,
327
+ taxableAmount: number,
328
+ taxAmount: number,
329
+ workingDays: number,
330
+ actualDays: number,
331
+ proRatedAmount: number,
332
+ attendanceDeduction: number,
333
+ }
334
+ ```
335
+
336
+ ### Pro-Rating
337
+
338
+ ```typescript
339
+ import { calculateProRating } from '@classytic/payroll/calculators';
340
+
341
+ const result = calculateProRating({
342
+ hireDate: new Date('2024-03-15'),
343
+ terminationDate: null,
344
+ periodStart: new Date('2024-03-01'),
345
+ periodEnd: new Date('2024-03-31'),
346
+ workingDays: [1, 2, 3, 4, 5],
347
+ holidays: [],
348
+ });
349
+
350
+ // Returns ProRatingResult
351
+ {
352
+ isProRated: true,
353
+ ratio: 0.545,
354
+ periodWorkingDays: 22,
355
+ effectiveWorkingDays: 12,
356
+ reason: 'new_hire',
357
+ }
358
+ ```
359
+
360
+ ---
361
+
362
+ ## Events
363
+
364
+ ```typescript
365
+ payroll.on('employee:hired', (payload) => { /* { employee, organizationId } */ });
366
+ payroll.on('employee:terminated', (payload) => { /* { employee, reason } */ });
367
+ payroll.on('salary:processed', (payload) => { /* { payrollRecord, transaction } */ });
368
+ payroll.on('payroll:completed', (payload) => { /* { summary, period } */ });
369
+ payroll.on('payroll:exported', (payload) => { /* { exportId, recordCount } */ });
370
+ ```
371
+
372
+ ## Webhooks
373
+
374
+ ```typescript
375
+ // Register webhook
376
+ payroll.registerWebhook({
377
+ url: 'https://api.example.com/webhooks',
378
+ events: ['salary:processed', 'employee:hired'],
379
+ secret: 'your-secret',
380
+ });
381
+
382
+ // Verify signature in handler
383
+ const signature = req.headers['x-payroll-signature'];
384
+ const timestamp = req.headers['x-payroll-timestamp'];
385
+ const signedPayload = `${timestamp}.${JSON.stringify(req.body)}`;
386
+ const expected = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex');
387
+ ```
388
+
389
+ ---
390
+
391
+ ## Configuration
392
+
393
+ ### Multi-Tenant (Default)
394
+
395
+ ```typescript
396
+ const payroll = createPayrollInstance()
397
+ .withModels({ EmployeeModel, PayrollRecordModel, TransactionModel })
398
+ .withConfig({
399
+ payroll: {
400
+ defaultCurrency: 'USD',
401
+ attendanceIntegration: true,
402
+ allowProRating: true,
403
+ autoDeductions: true,
404
+ },
405
+ })
406
+ .build();
407
+
408
+ // organizationId required on all operations
409
+ await payroll.hire({ organizationId, employment, compensation });
410
+ ```
411
+
412
+ ### Single-Tenant
413
+
414
+ ```typescript
415
+ const payroll = createPayrollInstance()
416
+ .withModels({ EmployeeModel, PayrollRecordModel, TransactionModel })
417
+ .forSingleTenant({ organizationId: YOUR_ORG_ID, autoInject: true })
418
+ .build();
419
+
420
+ // organizationId auto-injected
421
+ await payroll.hire({ employment, compensation });
422
+ ```
423
+
424
+ ---
425
+
426
+ ## Key Types
427
+
428
+ ```typescript
429
+ import type {
430
+ // Documents
431
+ EmployeeDocument,
432
+ PayrollRecordDocument,
433
+ LeaveRequestDocument,
434
+
435
+ // Core types
436
+ Compensation,
437
+ Allowance,
438
+ Deduction,
439
+ PayrollBreakdown,
440
+ TaxBracket,
441
+ BankDetails,
442
+
443
+ // Params
444
+ HireEmployeeParams,
445
+ ProcessSalaryParams,
446
+ ProcessBulkPayrollParams,
447
+ ExportPayrollParams,
448
+
449
+ // Results
450
+ ProcessSalaryResult,
451
+ BulkPayrollResult,
452
+
453
+ // Enums
454
+ EmployeeStatus, // 'active' | 'on_leave' | 'suspended' | 'terminated'
455
+ PayrollStatus, // 'pending' | 'processing' | 'paid' | 'failed' | 'voided' | 'reversed'
456
+ PayrollRunType, // 'regular' | 'supplemental' | 'retroactive' | 'off-cycle'
457
+ LeaveType, // 'annual' | 'sick' | 'unpaid' | 'maternity' | 'paternity'
458
+ AllowanceType, // 'housing' | 'transport' | 'meal' | 'mobile' | 'medical' | 'bonus' | 'other'
459
+ DeductionType, // 'tax' | 'loan' | 'advance' | 'provident_fund' | 'insurance'
460
+ PaymentFrequency, // 'monthly' | 'bi_weekly' | 'weekly' | 'daily' | 'hourly'
461
+ PaymentMethod, // 'bank' | 'cash' | 'check'
462
+ } from '@classytic/payroll';
463
+ ```
464
+
465
+ ---
466
+
467
+ ## Schemas
468
+
469
+ ```typescript
470
+ import {
471
+ createEmployeeSchema,
472
+ createPayrollRecordSchema,
473
+ employeeIndexes,
474
+ payrollRecordIndexes,
475
+ } from '@classytic/payroll/schemas';
476
+
477
+ // Create with custom fields
478
+ const employeeSchema = createEmployeeSchema({
479
+ skills: [String],
480
+ certifications: [{ name: String, date: Date }],
481
+ });
482
+
483
+ // Apply indexes
484
+ employeeIndexes.forEach(idx => employeeSchema.index(idx.fields, idx.options));
485
+ ```
486
+
487
+ ---
488
+
489
+ ## Utilities
490
+
491
+ ```typescript
492
+ import {
493
+ // Date
494
+ addDays, addMonths, diffInDays, startOfMonth, endOfMonth,
495
+ getPayPeriod, getWorkingDaysInMonth,
496
+
497
+ // Money (banker's rounding)
498
+ roundMoney, percentageOf, prorateAmount,
499
+
500
+ // Query builders
501
+ toObjectId, isValidObjectId,
502
+ } from '@classytic/payroll/utils';
503
+ ```
504
+
505
+ ---
506
+
507
+ ## Error Handling
508
+
509
+ ```typescript
510
+ import {
511
+ PayrollError,
512
+ DuplicatePayrollError,
513
+ EmployeeNotFoundError,
514
+ NotEligibleError,
515
+ ValidationError,
516
+ } from '@classytic/payroll';
517
+
518
+ try {
519
+ await payroll.processSalary({ organizationId, employeeId, month, year });
520
+ } catch (error) {
521
+ if (error instanceof DuplicatePayrollError) {
522
+ // Already processed for this period + run type
523
+ } else if (error instanceof EmployeeNotFoundError) {
524
+ // Employee doesn't exist
525
+ } else if (error instanceof NotEligibleError) {
526
+ // Employee not eligible (terminated, etc.)
527
+ }
528
+ }
529
+ ```
530
+
531
+ ---
532
+
533
+ ## License
534
+
535
+ MIT