@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 CHANGED
@@ -1,10 +1,6 @@
1
1
  # @classytic/payroll
2
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)
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 employee
20
+ // Hire
25
21
  await payroll.hire({
26
22
  organizationId,
27
- employment: { email: 'dev@example.com', position: 'Engineer' },
28
- compensation: { baseSalary: 80000, currency: 'USD' },
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
- period: { month: 1, year: 2024 },
31
+ month: 1,
32
+ year: 2024,
36
33
  });
37
34
  ```
38
35
 
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
36
+ ## Package Exports
49
37
 
50
38
  | Entry Point | Description |
51
39
  |-------------|-------------|
52
- | `@classytic/payroll` | Main API (Payroll class, types, errors) |
53
- | `@classytic/payroll/schemas` | Mongoose schemas for extending |
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/calculators` | Pure calculation functions (no DB) |
43
+ | `@classytic/payroll/schemas` | Mongoose schema factories |
44
+
45
+ ---
56
46
 
57
- ## Employee Management
47
+ ## Employee Operations
58
48
 
59
49
  ```typescript
60
50
  // Hire
61
- await payroll.hire({ organizationId, employment, compensation });
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 employee = await payroll.getEmployee({ employeeId, organizationId });
58
+ const emp = await payroll.getEmployee({ employeeId, organizationId });
65
59
 
66
- // Get by flexible identity (userId, employeeId, or email)
67
- const emp = await payroll.getEmployeeByIdentity({
68
- identity: 'EMP-001', // or userId or email
60
+ // Update employment
61
+ await payroll.updateEmployment({
62
+ employeeId,
69
63
  organizationId,
70
- mode: 'employeeId', // 'userId' | 'employeeId' | 'email' | 'any'
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({ employeeId, terminationDate, reason: 'resignation' });
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
- ## Listing Employees
84
-
85
- Employee listing/queries are done at app level using your models directly:
79
+ ## Compensation
86
80
 
87
81
  ```typescript
88
- // Use your EmployeeModel with mongokit or mongoose directly
89
- const employees = await EmployeeModel.find({
82
+ // Update salary
83
+ await payroll.updateSalary({
84
+ employeeId,
90
85
  organizationId,
91
- 'employment.status': 'active'
86
+ compensation: { baseAmount: 90000 },
87
+ effectiveFrom: new Date(),
92
88
  });
93
89
 
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,
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
- ## Compensation
120
+ ### Payment Frequencies
104
121
 
105
- ```typescript
106
- // Update salary
107
- await payroll.updateSalary({ employeeId, compensation: { baseSalary: 90000 } });
122
+ Supports multiple payment frequencies with automatic tax annualization:
108
123
 
109
- // Add allowance
110
- await payroll.addAllowance({ employeeId, allowance: { type: 'housing', amount: 2000 } });
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
- // Add deduction
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
- ## Bulk Processing
134
+ ## Payroll Processing
117
135
 
118
136
  ```typescript
119
- await payroll.processBulkPayroll({
137
+ // Single employee
138
+ const result = await payroll.processSalary({
120
139
  organizationId,
121
- period: { month: 1, year: 2024 },
122
- onProgress: ({ current, total }) => console.log(`${current}/${total}`),
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
- ## Leave Management
162
+ ### Duplicate Protection
163
+
164
+ The package provides database-level duplicate protection via a unique compound index:
127
165
 
128
166
  ```typescript
129
- // Request leave
130
- await payroll.requestLeave({
131
- employeeId,
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
- leaveType: 'annual',
134
- startDate: new Date('2024-01-15'),
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
- // Approve
139
- await payroll.approveLeave({ leaveRequestId, approverId });
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
- **State Flow:**
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
- ## Events
234
+ ## Leave Management
179
235
 
180
236
  ```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}`);
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
- ## Webhooks
247
+ // Approve
248
+ await payroll.approveLeave({ leaveRequestId, organizationId, approverId: managerId });
191
249
 
192
- ```typescript
193
- await payroll.registerWebhook({
250
+ // Reject
251
+ await payroll.rejectLeave({
252
+ leaveRequestId,
194
253
  organizationId,
195
- url: 'https://api.example.com/webhooks',
196
- events: ['payroll:processed'],
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
- ## Tenant Modes
263
+ ---
202
264
 
203
- ### Single-Tenant (Recommended for most apps)
265
+ ## Pure Calculators (No DB Required)
204
266
 
205
- For apps serving one organization:
267
+ Import from `@classytic/payroll/calculators` for client-side or serverless:
206
268
 
207
269
  ```typescript
208
- const payroll = createPayrollInstance()
209
- .withModels({ EmployeeModel, PayrollRecordModel, TransactionModel })
210
- .forSingleTenant({ organizationId: YOUR_ORG_ID, autoInject: true })
211
- .build();
270
+ import {
271
+ calculateSalaryBreakdown,
272
+ calculateProRating,
273
+ calculateAttendanceDeduction,
274
+ } from '@classytic/payroll/calculators';
275
+ ```
212
276
 
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
- });
277
+ ### Salary Breakdown
218
278
 
219
- await payroll.processSalary({
220
- employeeId,
221
- period: { month: 1, year: 2024 },
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
- await payroll.getEmployee({ employeeId });
225
- await payroll.updateEmployment({ employeeId, updates: { position: 'Lead' } });
226
- await payroll.terminate({ employeeId, terminationDate, reason: 'resignation' });
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
- ### Multi-Tenant
230
-
231
- For SaaS apps serving multiple organizations:
336
+ ### Pro-Rating
232
337
 
233
338
  ```typescript
234
- const payroll = createPayrollInstance()
235
- .withModels({ EmployeeModel, PayrollRecordModel, TransactionModel })
236
- .build();
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
- // 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 });
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
- ## Pure Calculators
360
+ ---
245
361
 
246
- No database required - works in browser/serverless:
362
+ ## Events
247
363
 
248
364
  ```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
- });
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
- ## Shift Compliance
272
-
273
- Late penalties, overtime bonuses, night differentials:
372
+ ## Webhooks
274
373
 
275
374
  ```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
- },
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
- console.log(result.penalties); // Late penalties
291
- console.log(result.bonuses); // Overtime bonuses
292
- console.log(result.netAdjustment);
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
- autoCreateTransaction: true,
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
- ## Timeline Audit
408
+ // organizationId required on all operations
409
+ await payroll.hire({ organizationId, employment, compensation });
410
+ ```
315
411
 
316
- Integrate with `@classytic/mongoose-timeline-audit` for WHO/WHAT/WHEN tracking:
412
+ ### Single-Tenant
317
413
 
318
414
  ```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
- });
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
- ## TypeScript
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
- ## Schemas & Indexes
465
+ ---
348
466
 
349
- The package exports schema creators and recommended indexes:
467
+ ## Schemas
350
468
 
351
469
  ```typescript
352
470
  import {
353
471
  createEmployeeSchema,
354
472
  createPayrollRecordSchema,
355
- applyEmployeeIndexes,
356
- applyPayrollRecordIndexes,
473
+ employeeIndexes,
474
+ payrollRecordIndexes,
357
475
  } from '@classytic/payroll/schemas';
358
476
 
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
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
- ```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
- ]);
483
+ // Apply indexes
484
+ employeeIndexes.forEach(idx => employeeSchema.index(idx.fields, idx.options));
431
485
  ```
432
486
 
433
- ### Custom Mongokit Plugins
487
+ ---
434
488
 
435
- Create your own plugins for cross-cutting concerns:
489
+ ## Utilities
436
490
 
437
491
  ```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);
492
+ import {
493
+ // Date
494
+ addDays, addMonths, diffInDays, startOfMonth, endOfMonth,
495
+ getPayPeriod, getWorkingDaysInMonth,
472
496
 
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 });
497
+ // Money (banker's rounding)
498
+ roundMoney, percentageOf, prorateAmount,
478
499
 
479
- // Automatic commit on success, rollback on error
480
- return { payroll, transaction };
481
- });
500
+ // Query builders
501
+ toObjectId, isValidObjectId,
502
+ } from '@classytic/payroll/utils';
482
503
  ```
483
504
 
484
- ### Type-Safe Utilities
505
+ ---
485
506
 
486
- Use the new type guards for cleaner code:
507
+ ## Error Handling
487
508
 
488
509
  ```typescript
489
510
  import {
490
- getEmployeeEmail,
491
- getEmployeeName,
492
- isGuestEmployee,
493
- isDuplicateKeyError,
494
- parseDuplicateKeyError,
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.hire({ ... });
519
+ await payroll.processSalary({ organizationId, employeeId, month, year });
508
520
  } catch (error) {
509
- if (isDuplicateKeyError(error)) {
510
- const field = parseDuplicateKeyError(error);
511
- console.error(`Duplicate ${field}`);
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
- ## 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
531
+ ---
522
532
 
523
533
  ## License
524
534