@classytic/payroll 2.7.5 → 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.
- package/README.md +333 -323
- package/dist/attendance.calculator-BZcv2iii.d.ts +336 -0
- package/dist/calculators/index.d.ts +3 -299
- package/dist/calculators/index.js +154 -19
- package/dist/calculators/index.js.map +1 -1
- package/dist/core/index.d.ts +321 -0
- package/dist/core/index.js +1962 -0
- package/dist/core/index.js.map +1 -0
- package/dist/{employee-identity-Cq2wo9-2.d.ts → error-helpers-Bm6lMny2.d.ts} +257 -7
- package/dist/{index-DjB72l6e.d.ts → index-BKLkuSAs.d.ts} +248 -132
- package/dist/index.d.ts +418 -658
- package/dist/index.js +1179 -373
- package/dist/index.js.map +1 -1
- package/dist/payroll-states-DBt0XVm-.d.ts +598 -0
- package/dist/{prorating.calculator-C7sdFiG2.d.ts → prorating.calculator-C33fWBQf.d.ts} +2 -2
- package/dist/schemas/index.d.ts +2 -2
- package/dist/schemas/index.js +95 -75
- package/dist/schemas/index.js.map +1 -1
- package/dist/{types-BVDjiVGS.d.ts → types-bZdAJueH.d.ts} +427 -12
- package/dist/utils/index.d.ts +17 -5
- package/dist/utils/index.js +185 -25
- package/dist/utils/index.js.map +1 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
# @classytic/payroll
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
[](https://www.npmjs.com/package/@classytic/payroll)
|
|
6
|
-
[](https://www.typescriptlang.org/)
|
|
7
|
-
[](https://opensource.org/licenses/MIT)
|
|
3
|
+
HRM & Payroll for MongoDB. Multi-tenant, event-driven, type-safe.
|
|
8
4
|
|
|
9
5
|
## Install
|
|
10
6
|
|
|
@@ -21,130 +17,190 @@ const payroll = createPayrollInstance()
|
|
|
21
17
|
.withModels({ EmployeeModel, PayrollRecordModel, TransactionModel })
|
|
22
18
|
.build();
|
|
23
19
|
|
|
24
|
-
// Hire
|
|
20
|
+
// Hire
|
|
25
21
|
await payroll.hire({
|
|
26
22
|
organizationId,
|
|
27
|
-
employment: { email: 'dev@example.com', position: 'Engineer' },
|
|
28
|
-
compensation: {
|
|
23
|
+
employment: { email: 'dev@example.com', position: 'Engineer', hireDate: new Date() },
|
|
24
|
+
compensation: { baseAmount: 80000, currency: 'USD', frequency: 'monthly' },
|
|
29
25
|
});
|
|
30
26
|
|
|
31
27
|
// Process salary
|
|
32
28
|
await payroll.processSalary({
|
|
33
29
|
organizationId,
|
|
34
30
|
employeeId,
|
|
35
|
-
|
|
31
|
+
month: 1,
|
|
32
|
+
year: 2024,
|
|
36
33
|
});
|
|
37
34
|
```
|
|
38
35
|
|
|
39
|
-
##
|
|
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
|
|
36
|
+
## Package Exports
|
|
49
37
|
|
|
50
38
|
| Entry Point | Description |
|
|
51
39
|
|-------------|-------------|
|
|
52
|
-
| `@classytic/payroll` | Main API
|
|
53
|
-
| `@classytic/payroll/
|
|
40
|
+
| `@classytic/payroll` | Main API: Payroll class, types, schemas, errors |
|
|
41
|
+
| `@classytic/payroll/calculators` | Pure calculation functions (no DB required) |
|
|
54
42
|
| `@classytic/payroll/utils` | Date, money, validation utilities |
|
|
55
|
-
| `@classytic/payroll/
|
|
43
|
+
| `@classytic/payroll/schemas` | Mongoose schema factories |
|
|
44
|
+
|
|
45
|
+
---
|
|
56
46
|
|
|
57
|
-
## Employee
|
|
47
|
+
## Employee Operations
|
|
58
48
|
|
|
59
49
|
```typescript
|
|
60
50
|
// Hire
|
|
61
|
-
await payroll.hire({
|
|
51
|
+
await payroll.hire({
|
|
52
|
+
organizationId,
|
|
53
|
+
employment: { email, employeeId, position, department, hireDate },
|
|
54
|
+
compensation: { baseAmount, currency, frequency },
|
|
55
|
+
});
|
|
62
56
|
|
|
63
57
|
// Get employee
|
|
64
|
-
const
|
|
58
|
+
const emp = await payroll.getEmployee({ employeeId, organizationId });
|
|
65
59
|
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
60
|
+
// Update employment
|
|
61
|
+
await payroll.updateEmployment({
|
|
62
|
+
employeeId,
|
|
69
63
|
organizationId,
|
|
70
|
-
|
|
64
|
+
updates: { position: 'Senior Engineer', department: 'engineering' },
|
|
71
65
|
});
|
|
72
66
|
|
|
73
|
-
// Update
|
|
74
|
-
await payroll.updateEmployment({ employeeId, updates: { position: 'Lead' } });
|
|
75
|
-
|
|
76
67
|
// Terminate
|
|
77
|
-
await payroll.terminate({
|
|
68
|
+
await payroll.terminate({
|
|
69
|
+
employeeId,
|
|
70
|
+
organizationId,
|
|
71
|
+
terminationDate: new Date(),
|
|
72
|
+
reason: 'resignation',
|
|
73
|
+
});
|
|
78
74
|
|
|
79
75
|
// Re-hire
|
|
80
|
-
await payroll.reHire({ employeeId, hireDate: new Date() });
|
|
76
|
+
await payroll.reHire({ employeeId, organizationId, hireDate: new Date() });
|
|
81
77
|
```
|
|
82
78
|
|
|
83
|
-
##
|
|
84
|
-
|
|
85
|
-
Employee listing/queries are done at app level using your models directly:
|
|
79
|
+
## Compensation
|
|
86
80
|
|
|
87
81
|
```typescript
|
|
88
|
-
//
|
|
89
|
-
|
|
82
|
+
// Update salary
|
|
83
|
+
await payroll.updateSalary({
|
|
84
|
+
employeeId,
|
|
90
85
|
organizationId,
|
|
91
|
-
|
|
86
|
+
compensation: { baseAmount: 90000 },
|
|
87
|
+
effectiveFrom: new Date(),
|
|
92
88
|
});
|
|
93
89
|
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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 },
|
|
100
117
|
});
|
|
101
118
|
```
|
|
102
119
|
|
|
103
|
-
|
|
120
|
+
### Payment Frequencies
|
|
104
121
|
|
|
105
|
-
|
|
106
|
-
// Update salary
|
|
107
|
-
await payroll.updateSalary({ employeeId, compensation: { baseSalary: 90000 } });
|
|
122
|
+
Supports multiple payment frequencies with automatic tax annualization:
|
|
108
123
|
|
|
109
|
-
|
|
110
|
-
|
|
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 |
|
|
111
131
|
|
|
112
|
-
|
|
113
|
-
await payroll.addDeduction({ employeeId, deduction: { type: 'loan', amount: 500 } });
|
|
114
|
-
```
|
|
132
|
+
Tax is calculated consistently: same annual income = same annual tax, regardless of frequency.
|
|
115
133
|
|
|
116
|
-
##
|
|
134
|
+
## Payroll Processing
|
|
117
135
|
|
|
118
136
|
```typescript
|
|
119
|
-
|
|
137
|
+
// Single employee
|
|
138
|
+
const result = await payroll.processSalary({
|
|
120
139
|
organizationId,
|
|
121
|
-
|
|
122
|
-
|
|
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}%`),
|
|
123
158
|
});
|
|
159
|
+
// Returns: { successCount, failCount, totalAmount, successful[], failed[] }
|
|
124
160
|
```
|
|
125
161
|
|
|
126
|
-
|
|
162
|
+
### Duplicate Protection
|
|
163
|
+
|
|
164
|
+
The package provides database-level duplicate protection via a unique compound index:
|
|
127
165
|
|
|
128
166
|
```typescript
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
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({
|
|
132
186
|
organizationId,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
endDate: new Date('2024-01-17'),
|
|
187
|
+
startDate: new Date('2024-01-01'),
|
|
188
|
+
endDate: new Date('2024-01-31'),
|
|
136
189
|
});
|
|
137
190
|
|
|
138
|
-
//
|
|
139
|
-
|
|
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' });
|
|
140
198
|
```
|
|
141
199
|
|
|
142
200
|
## Void / Reverse / Restore
|
|
143
201
|
|
|
144
|
-
Payroll corrections with full state tracking:
|
|
145
|
-
|
|
146
202
|
```typescript
|
|
147
|
-
// Void unpaid payroll
|
|
203
|
+
// Void unpaid payroll (pending, processing, failed)
|
|
148
204
|
await payroll.voidPayroll({
|
|
149
205
|
organizationId,
|
|
150
206
|
payrollRecordId,
|
|
@@ -158,7 +214,7 @@ await payroll.reversePayroll({
|
|
|
158
214
|
reason: 'Duplicate payment',
|
|
159
215
|
});
|
|
160
216
|
|
|
161
|
-
// Restore voided payroll
|
|
217
|
+
// Restore voided payroll (blocked if replacement exists)
|
|
162
218
|
await payroll.restorePayroll({
|
|
163
219
|
organizationId,
|
|
164
220
|
payrollRecordId,
|
|
@@ -166,7 +222,7 @@ await payroll.restorePayroll({
|
|
|
166
222
|
});
|
|
167
223
|
```
|
|
168
224
|
|
|
169
|
-
**
|
|
225
|
+
**Status Flow:**
|
|
170
226
|
```
|
|
171
227
|
PENDING → PROCESSING → PAID → REVERSED
|
|
172
228
|
↓ ↓
|
|
@@ -175,350 +231,304 @@ PENDING → PROCESSING → PAID → REVERSED
|
|
|
175
231
|
PENDING (restore)
|
|
176
232
|
```
|
|
177
233
|
|
|
178
|
-
##
|
|
234
|
+
## Leave Management
|
|
179
235
|
|
|
180
236
|
```typescript
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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',
|
|
187
245
|
});
|
|
188
|
-
```
|
|
189
246
|
|
|
190
|
-
|
|
247
|
+
// Approve
|
|
248
|
+
await payroll.approveLeave({ leaveRequestId, organizationId, approverId: managerId });
|
|
191
249
|
|
|
192
|
-
|
|
193
|
-
await payroll.
|
|
250
|
+
// Reject
|
|
251
|
+
await payroll.rejectLeave({
|
|
252
|
+
leaveRequestId,
|
|
194
253
|
organizationId,
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
secret: 'your-secret',
|
|
254
|
+
rejectedBy: managerId,
|
|
255
|
+
rejectionReason: 'Insufficient leave balance',
|
|
198
256
|
});
|
|
257
|
+
|
|
258
|
+
// Get balance
|
|
259
|
+
const balance = await payroll.getLeaveBalance({ employeeId, organizationId });
|
|
260
|
+
// { annual: { total: 20, used: 5, remaining: 15 }, sick: {...}, ... }
|
|
199
261
|
```
|
|
200
262
|
|
|
201
|
-
|
|
263
|
+
---
|
|
202
264
|
|
|
203
|
-
|
|
265
|
+
## Pure Calculators (No DB Required)
|
|
204
266
|
|
|
205
|
-
|
|
267
|
+
Import from `@classytic/payroll/calculators` for client-side or serverless:
|
|
206
268
|
|
|
207
269
|
```typescript
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
270
|
+
import {
|
|
271
|
+
calculateSalaryBreakdown,
|
|
272
|
+
calculateProRating,
|
|
273
|
+
calculateAttendanceDeduction,
|
|
274
|
+
} from '@classytic/payroll/calculators';
|
|
275
|
+
```
|
|
212
276
|
|
|
213
|
-
|
|
214
|
-
await payroll.hire({
|
|
215
|
-
employment: { email: 'dev@example.com', position: 'Engineer' },
|
|
216
|
-
compensation: { baseSalary: 80000, currency: 'USD' },
|
|
217
|
-
});
|
|
277
|
+
### Salary Breakdown
|
|
218
278
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
+
],
|
|
222
318
|
});
|
|
223
319
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
+
}
|
|
227
334
|
```
|
|
228
335
|
|
|
229
|
-
###
|
|
230
|
-
|
|
231
|
-
For SaaS apps serving multiple organizations:
|
|
336
|
+
### Pro-Rating
|
|
232
337
|
|
|
233
338
|
```typescript
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
+
});
|
|
237
349
|
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
350
|
+
// Returns ProRatingResult
|
|
351
|
+
{
|
|
352
|
+
isProRated: true,
|
|
353
|
+
ratio: 0.545,
|
|
354
|
+
periodWorkingDays: 22,
|
|
355
|
+
effectiveWorkingDays: 12,
|
|
356
|
+
reason: 'new_hire',
|
|
357
|
+
}
|
|
242
358
|
```
|
|
243
359
|
|
|
244
|
-
|
|
360
|
+
---
|
|
245
361
|
|
|
246
|
-
|
|
362
|
+
## Events
|
|
247
363
|
|
|
248
364
|
```typescript
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
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
|
-
});
|
|
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 } */ });
|
|
269
370
|
```
|
|
270
371
|
|
|
271
|
-
##
|
|
272
|
-
|
|
273
|
-
Late penalties, overtime bonuses, night differentials:
|
|
372
|
+
## Webhooks
|
|
274
373
|
|
|
275
374
|
```typescript
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
},
|
|
375
|
+
// Register webhook
|
|
376
|
+
payroll.registerWebhook({
|
|
377
|
+
url: 'https://api.example.com/webhooks',
|
|
378
|
+
events: ['salary:processed', 'employee:hired'],
|
|
379
|
+
secret: 'your-secret',
|
|
288
380
|
});
|
|
289
381
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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');
|
|
293
387
|
```
|
|
294
388
|
|
|
389
|
+
---
|
|
390
|
+
|
|
295
391
|
## Configuration
|
|
296
392
|
|
|
393
|
+
### Multi-Tenant (Default)
|
|
394
|
+
|
|
297
395
|
```typescript
|
|
298
396
|
const payroll = createPayrollInstance()
|
|
299
397
|
.withModels({ EmployeeModel, PayrollRecordModel, TransactionModel })
|
|
300
398
|
.withConfig({
|
|
301
|
-
currency: 'USD',
|
|
302
399
|
payroll: {
|
|
400
|
+
defaultCurrency: 'USD',
|
|
303
401
|
attendanceIntegration: true,
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
leave: {
|
|
307
|
-
enabled: true,
|
|
308
|
-
defaultBalances: { annual: 20, sick: 10 },
|
|
402
|
+
allowProRating: true,
|
|
403
|
+
autoDeductions: true,
|
|
309
404
|
},
|
|
310
405
|
})
|
|
311
406
|
.build();
|
|
312
|
-
```
|
|
313
407
|
|
|
314
|
-
|
|
408
|
+
// organizationId required on all operations
|
|
409
|
+
await payroll.hire({ organizationId, employment, compensation });
|
|
410
|
+
```
|
|
315
411
|
|
|
316
|
-
|
|
412
|
+
### Single-Tenant
|
|
317
413
|
|
|
318
414
|
```typescript
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
employee.addTimelineEvent(
|
|
327
|
-
PAYROLL_EVENTS.EMPLOYEE.HIRED,
|
|
328
|
-
`Hired as ${data.employee.position}`,
|
|
329
|
-
request
|
|
330
|
-
);
|
|
331
|
-
await employee.save();
|
|
332
|
-
});
|
|
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 });
|
|
333
422
|
```
|
|
334
423
|
|
|
335
|
-
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## Key Types
|
|
336
427
|
|
|
337
428
|
```typescript
|
|
338
429
|
import type {
|
|
430
|
+
// Documents
|
|
339
431
|
EmployeeDocument,
|
|
340
432
|
PayrollRecordDocument,
|
|
341
433
|
LeaveRequestDocument,
|
|
434
|
+
|
|
435
|
+
// Core types
|
|
342
436
|
Compensation,
|
|
437
|
+
Allowance,
|
|
438
|
+
Deduction,
|
|
343
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'
|
|
344
462
|
} from '@classytic/payroll';
|
|
345
463
|
```
|
|
346
464
|
|
|
347
|
-
|
|
465
|
+
---
|
|
348
466
|
|
|
349
|
-
|
|
467
|
+
## Schemas
|
|
350
468
|
|
|
351
469
|
```typescript
|
|
352
470
|
import {
|
|
353
471
|
createEmployeeSchema,
|
|
354
472
|
createPayrollRecordSchema,
|
|
355
|
-
|
|
356
|
-
|
|
473
|
+
employeeIndexes,
|
|
474
|
+
payrollRecordIndexes,
|
|
357
475
|
} from '@classytic/payroll/schemas';
|
|
358
476
|
|
|
359
|
-
// Create
|
|
360
|
-
const employeeSchema = createEmployeeSchema(
|
|
361
|
-
|
|
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
|
|
477
|
+
// Create with custom fields
|
|
478
|
+
const employeeSchema = createEmployeeSchema({
|
|
479
|
+
skills: [String],
|
|
480
|
+
certifications: [{ name: String, date: Date }],
|
|
412
481
|
});
|
|
413
|
-
```
|
|
414
|
-
|
|
415
|
-
### Available Audit Plugins
|
|
416
482
|
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
]);
|
|
483
|
+
// Apply indexes
|
|
484
|
+
employeeIndexes.forEach(idx => employeeSchema.index(idx.fields, idx.options));
|
|
431
485
|
```
|
|
432
486
|
|
|
433
|
-
|
|
487
|
+
---
|
|
434
488
|
|
|
435
|
-
|
|
489
|
+
## Utilities
|
|
436
490
|
|
|
437
491
|
```typescript
|
|
438
|
-
import
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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);
|
|
492
|
+
import {
|
|
493
|
+
// Date
|
|
494
|
+
addDays, addMonths, diffInDays, startOfMonth, endOfMonth,
|
|
495
|
+
getPayPeriod, getWorkingDaysInMonth,
|
|
472
496
|
|
|
473
|
-
//
|
|
474
|
-
|
|
475
|
-
// All operations use the same session
|
|
476
|
-
const payroll = await payrollRepo.create(payrollData, { session });
|
|
477
|
-
const transaction = await transactionRepo.create(txData, { session });
|
|
497
|
+
// Money (banker's rounding)
|
|
498
|
+
roundMoney, percentageOf, prorateAmount,
|
|
478
499
|
|
|
479
|
-
//
|
|
480
|
-
|
|
481
|
-
}
|
|
500
|
+
// Query builders
|
|
501
|
+
toObjectId, isValidObjectId,
|
|
502
|
+
} from '@classytic/payroll/utils';
|
|
482
503
|
```
|
|
483
504
|
|
|
484
|
-
|
|
505
|
+
---
|
|
485
506
|
|
|
486
|
-
|
|
507
|
+
## Error Handling
|
|
487
508
|
|
|
488
509
|
```typescript
|
|
489
510
|
import {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
511
|
+
PayrollError,
|
|
512
|
+
DuplicatePayrollError,
|
|
513
|
+
EmployeeNotFoundError,
|
|
514
|
+
NotEligibleError,
|
|
515
|
+
ValidationError,
|
|
495
516
|
} from '@classytic/payroll';
|
|
496
517
|
|
|
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
518
|
try {
|
|
507
|
-
await payroll.
|
|
519
|
+
await payroll.processSalary({ organizationId, employeeId, month, year });
|
|
508
520
|
} catch (error) {
|
|
509
|
-
if (
|
|
510
|
-
|
|
511
|
-
|
|
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.)
|
|
512
527
|
}
|
|
513
528
|
}
|
|
514
529
|
```
|
|
515
530
|
|
|
516
|
-
|
|
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
|
|
531
|
+
---
|
|
522
532
|
|
|
523
533
|
## License
|
|
524
534
|
|