JSPM

@classytic/payroll

2.7.5
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 14
  • Score
    100M100P100Q46645F
  • License MIT

Modern HRM and payroll management for Mongoose - Plugin-based, event-driven, multi-tenant ready. Salary processing, compensation management, tax calculations, and employee lifecycle management.

Package Exports

  • @classytic/payroll
  • @classytic/payroll/calculators
  • @classytic/payroll/schemas
  • @classytic/payroll/utils

Readme

@classytic/payroll

Enterprise HRM & Payroll for MongoDB. Clean architecture, multi-tenant, type-safe.

npm TypeScript MIT

Install

npm install @classytic/payroll mongoose @classytic/mongokit

Quick Start

import { createPayrollInstance } from '@classytic/payroll';

const payroll = createPayrollInstance()
  .withModels({ EmployeeModel, PayrollRecordModel, TransactionModel })
  .build();

// Hire employee
await payroll.hire({
  organizationId,
  employment: { email: 'dev@example.com', position: 'Engineer' },
  compensation: { baseSalary: 80000, currency: 'USD' },
});

// Process salary
await payroll.processSalary({
  organizationId,
  employeeId,
  period: { month: 1, year: 2024 },
});

Features

  • Employee Lifecycle: Hire, update, terminate, re-hire
  • Compensation: Salary, allowances, deductions
  • Bulk Processing: Handle 10k+ employees with streaming
  • Multi-tenant: Automatic organization isolation
  • Events & Webhooks: React to payroll events
  • Type-safe: Full TypeScript support

Exports

Entry Point Description
@classytic/payroll Main API (Payroll class, types, errors)
@classytic/payroll/schemas Mongoose schemas for extending
@classytic/payroll/utils Date, money, validation utilities
@classytic/payroll/calculators Pure calculation functions (no DB)

Employee Management

// Hire
await payroll.hire({ organizationId, employment, compensation });

// Get employee
const employee = await payroll.getEmployee({ employeeId, organizationId });

// Get by flexible identity (userId, employeeId, or email)
const emp = await payroll.getEmployeeByIdentity({
  identity: 'EMP-001',  // or userId or email
  organizationId,
  mode: 'employeeId',   // 'userId' | 'employeeId' | 'email' | 'any'
});

// Update
await payroll.updateEmployment({ employeeId, updates: { position: 'Lead' } });

// Terminate
await payroll.terminate({ employeeId, terminationDate, reason: 'resignation' });

// Re-hire
await payroll.reHire({ employeeId, hireDate: new Date() });

Listing Employees

Employee listing/queries are done at app level using your models directly:

// Use your EmployeeModel with mongokit or mongoose directly
const employees = await EmployeeModel.find({
  organizationId,
  'employment.status': 'active'
});

// Or with mongokit repository
const repo = createRepository(EmployeeModel);
const result = await repo.getAll({
  filters: { organizationId, 'employment.status': 'active' },
  page: 1,
  limit: 100,
});

Compensation

// Update salary
await payroll.updateSalary({ employeeId, compensation: { baseSalary: 90000 } });

// Add allowance
await payroll.addAllowance({ employeeId, allowance: { type: 'housing', amount: 2000 } });

// Add deduction
await payroll.addDeduction({ employeeId, deduction: { type: 'loan', amount: 500 } });

Bulk Processing

await payroll.processBulkPayroll({
  organizationId,
  period: { month: 1, year: 2024 },
  onProgress: ({ current, total }) => console.log(`${current}/${total}`),
});

Leave Management

// Request leave
await payroll.requestLeave({
  employeeId,
  organizationId,
  leaveType: 'annual',
  startDate: new Date('2024-01-15'),
  endDate: new Date('2024-01-17'),
});

// Approve
await payroll.approveLeave({ leaveRequestId, approverId });

Void / Reverse / Restore

Payroll corrections with full state tracking:

// Void unpaid payroll
await payroll.voidPayroll({
  organizationId,
  payrollRecordId,
  reason: 'Test payroll',
});

// Reverse paid payroll (creates reversal transaction)
await payroll.reversePayroll({
  organizationId,
  payrollRecordId,
  reason: 'Duplicate payment',
});

// Restore voided payroll
await payroll.restorePayroll({
  organizationId,
  payrollRecordId,
  reason: 'Voided in error',
});

State Flow:

PENDING → PROCESSING → PAID → REVERSED
   ↓          ↓
   └──→ VOIDED ←── FAILED
         ↓
       PENDING (restore)

Events

payroll.on('employee:hired', (payload) => {
  console.log(`New hire: ${payload.employee.email}`);
});

payroll.on('payroll:processed', (payload) => {
  console.log(`Salary processed: ${payload.payrollRecord.id}`);
});

Webhooks

await payroll.registerWebhook({
  organizationId,
  url: 'https://api.example.com/webhooks',
  events: ['payroll:processed'],
  secret: 'your-secret',
});

Tenant Modes

For apps serving one organization:

const payroll = createPayrollInstance()
  .withModels({ EmployeeModel, PayrollRecordModel, TransactionModel })
  .forSingleTenant({ organizationId: YOUR_ORG_ID, autoInject: true })
  .build();

// No organizationId needed - auto-injected
await payroll.hire({
  employment: { email: 'dev@example.com', position: 'Engineer' },
  compensation: { baseSalary: 80000, currency: 'USD' },
});

await payroll.processSalary({
  employeeId,
  period: { month: 1, year: 2024 },
});

await payroll.getEmployee({ employeeId });
await payroll.updateEmployment({ employeeId, updates: { position: 'Lead' } });
await payroll.terminate({ employeeId, terminationDate, reason: 'resignation' });

Multi-Tenant

For SaaS apps serving multiple organizations:

const payroll = createPayrollInstance()
  .withModels({ EmployeeModel, PayrollRecordModel, TransactionModel })
  .build();

// organizationId required on all operations
await payroll.hire({ organizationId, employment, compensation });
await payroll.processSalary({ organizationId, employeeId, period });
await payroll.getEmployee({ organizationId, employeeId });

Pure Calculators

No database required - works in browser/serverless:

import {
  calculateSalaryBreakdown,
  calculateProRating,
  calculateAttendanceDeduction
} from '@classytic/payroll/calculators';

// Calculate salary breakdown
const breakdown = calculateSalaryBreakdown({
  baseSalary: 5000,
  allowances: [{ type: 'housing', amount: 500 }],
  deductions: [{ type: 'tax', percentage: 10 }],
});

// Pro-rate for mid-month joins
const proRated = calculateProRating({
  amount: 5000,
  startDate: new Date('2024-01-15'),
  endDate: new Date('2024-01-31'),
  totalDays: 31,
});

Shift Compliance

Late penalties, overtime bonuses, night differentials:

import {
  calculateShiftCompliance,
  DEFAULT_ATTENDANCE_POLICY
} from '@classytic/payroll';

const result = calculateShiftCompliance({
  policy: DEFAULT_ATTENDANCE_POLICY,
  baseSalary: 5000,
  shiftData: {
    lateArrivals: [{ minutes: 15 }],
    overtime: [{ hours: 2, type: 'weekday' }],
  },
});

console.log(result.penalties);  // Late penalties
console.log(result.bonuses);    // Overtime bonuses
console.log(result.netAdjustment);

Configuration

const payroll = createPayrollInstance()
  .withModels({ EmployeeModel, PayrollRecordModel, TransactionModel })
  .withConfig({
    currency: 'USD',
    payroll: {
      attendanceIntegration: true,
      autoCreateTransaction: true,
    },
    leave: {
      enabled: true,
      defaultBalances: { annual: 20, sick: 10 },
    },
  })
  .build();

Timeline Audit

Integrate with @classytic/mongoose-timeline-audit for WHO/WHAT/WHEN tracking:

import timelineAuditPlugin from '@classytic/mongoose-timeline-audit';
import { EMPLOYEE_TIMELINE_CONFIG, PAYROLL_EVENTS } from '@classytic/payroll';

employeeSchema.plugin(timelineAuditPlugin, EMPLOYEE_TIMELINE_CONFIG);

payroll.on('employee:hired', async ({ data }) => {
  const employee = await Employee.findById(data.employee.id);
  employee.addTimelineEvent(
    PAYROLL_EVENTS.EMPLOYEE.HIRED,
    `Hired as ${data.employee.position}`,
    request
  );
  await employee.save();
});

TypeScript

import type {
  EmployeeDocument,
  PayrollRecordDocument,
  LeaveRequestDocument,
  Compensation,
  PayrollBreakdown,
} from '@classytic/payroll';

Schemas & Indexes

The package exports schema creators and recommended indexes:

import {
  createEmployeeSchema,
  createPayrollRecordSchema,
  applyEmployeeIndexes,
  applyPayrollRecordIndexes,
} from '@classytic/payroll/schemas';

// Create schemas
const employeeSchema = createEmployeeSchema();
const payrollRecordSchema = createPayrollRecordSchema();

// Apply recommended indexes (optional)
applyEmployeeIndexes(employeeSchema);
applyPayrollRecordIndexes(payrollRecordSchema);

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.

If you need DB-level uniqueness:

// Add your own unique index if needed
payrollRecordSchema.index(
  { organizationId: 1, employeeId: 1, 'period.month': 1, 'period.year': 1 },
  { unique: true }
);

Mongokit Integration

The payroll package is built on @classytic/mongokit for powerful repository patterns and plugins.

Audit Trail Plugin

Automatically track who created/updated records with the built-in audit plugin:

import { Repository } from '@classytic/mongokit';
import { payrollAuditPlugin } from '@classytic/payroll';
import { EmployeeModel } from './models';

// Create repository with audit plugin
const employeeRepo = new Repository(EmployeeModel, [
  payrollAuditPlugin({
    userId: currentUser._id,
    userName: currentUser.name,
    organizationId: orgId,
  }),
]);

// All creates/updates now auto-capture audit fields
await employeeRepo.create({
  employment: { email: 'dev@example.com' },
  compensation: { baseSalary: 80000 },
  // createdBy, createdAt automatically added
});

await employeeRepo.update(employeeId, {
  $set: { 'employment.position': 'Senior' },
  // updatedBy, updatedAt automatically added
});

Available Audit Plugins

import {
  payrollAuditPlugin,    // Tracks creates & updates
  readAuditPlugin,       // Tracks read access (compliance)
  fullAuditPlugin,       // Combines both + comprehensive events
} from '@classytic/payroll';

// Full audit with compliance tracking
const repo = new Repository(PayrollRecordModel, [
  fullAuditPlugin({
    userId: currentUser._id,
    organizationId: orgId,
  }),
]);

Custom Mongokit Plugins

Create your own plugins for cross-cutting concerns:

import type { Repository } from '@classytic/mongokit';

// Example: Auto-encrypt sensitive fields
function encryptionPlugin(secretKey: string) {
  return (repo: Repository) => {
    repo.on('before:create', async (ctx) => {
      if (ctx.data.ssn) {
        ctx.data.ssn = encrypt(ctx.data.ssn, secretKey);
      }
    });

    repo.on('after:getById', async (ctx) => {
      if (ctx.result?.ssn) {
        ctx.result.ssn = decrypt(ctx.result.ssn, secretKey);
      }
    });
  };
}

// Apply to repository
const repo = new Repository(EmployeeModel, [
  encryptionPlugin(process.env.SECRET_KEY),
  payrollAuditPlugin({ userId, organizationId }),
]);

Transaction Management

Mongokit provides clean transaction handling:

import { Repository } from '@classytic/mongokit';

const payrollRepo = new Repository(PayrollRecordModel);

// Automatic transaction management
const result = await payrollRepo.withTransaction(async (session) => {
  // All operations use the same session
  const payroll = await payrollRepo.create(payrollData, { session });
  const transaction = await transactionRepo.create(txData, { session });

  // Automatic commit on success, rollback on error
  return { payroll, transaction };
});

Type-Safe Utilities

Use the new type guards for cleaner code:

import {
  getEmployeeEmail,
  getEmployeeName,
  isGuestEmployee,
  isDuplicateKeyError,
  parseDuplicateKeyError,
} from '@classytic/payroll';

// Type-safe employee identity access
const email = getEmployeeEmail(employee); // Works for guest & user-linked
const name = getEmployeeName(employee);   // Fallback to employeeId

if (isGuestEmployee(employee)) {
  console.log('Guest employee:', employee.employeeId);
}

// Type-safe error handling
try {
  await payroll.hire({ ... });
} catch (error) {
  if (isDuplicateKeyError(error)) {
    const field = parseDuplicateKeyError(error);
    console.error(`Duplicate ${field}`);
  }
}

Security

  • Multi-tenant isolation: All queries scoped by organizationId
  • Repository plugin: Auto-injects tenant filter on all operations
  • Secure lookups: findEmployeeSecure() enforces org boundaries
  • State machines: Prevent invalid status transitions

License

MIT