New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details
Socket
Book a DemoSign in
Socket

@classytic/payroll

Package Overview
Dependencies
Maintainers
1
Versions
30
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@classytic/payroll

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

latest
Source
npmnpm
Version
2.8.0
Version published
Maintainers
1
Created
Source

@classytic/payroll

HRM & Payroll for MongoDB. Multi-tenant, event-driven, type-safe.

Install

npm install @classytic/payroll mongoose @classytic/mongokit

Quick Start

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

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

// Hire
await payroll.hire({
  organizationId,
  employment: { email: 'dev@example.com', position: 'Engineer', hireDate: new Date() },
  compensation: { baseAmount: 80000, currency: 'USD', frequency: 'monthly' },
});

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

Package Exports

Entry PointDescription
@classytic/payrollMain API: Payroll class, types, schemas, errors
@classytic/payroll/calculatorsPure calculation functions (no DB required)
@classytic/payroll/utilsDate, money, validation utilities
@classytic/payroll/schemasMongoose schema factories

Employee Operations

// Hire
await payroll.hire({
  organizationId,
  employment: { email, employeeId, position, department, hireDate },
  compensation: { baseAmount, currency, frequency },
});

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

// Update employment
await payroll.updateEmployment({
  employeeId,
  organizationId,
  updates: { position: 'Senior Engineer', department: 'engineering' },
});

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

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

Compensation

// Update salary
await payroll.updateSalary({
  employeeId,
  organizationId,
  compensation: { baseAmount: 90000 },
  effectiveFrom: new Date(),
});

// Add allowance
await payroll.addAllowance({
  employeeId,
  organizationId,
  allowance: {
    type: 'housing',    // 'housing' | 'transport' | 'meal' | 'mobile' | 'medical' | 'bonus' | 'other'
    amount: 2000,
    taxable: true,
  },
});

// Add deduction
await payroll.addDeduction({
  employeeId,
  organizationId,
  deduction: {
    type: 'provident_fund',  // 'tax' | 'loan' | 'advance' | 'provident_fund' | 'insurance'
    amount: 500,
    auto: true,
  },
});

// Update bank details
await payroll.updateBankDetails({
  employeeId,
  organizationId,
  bankDetails: { accountNumber, bankName, routingNumber },
});

Payment Frequencies

Supports multiple payment frequencies with automatic tax annualization:

FrequencybaseAmountPeriods/YearExample ($104k/year)
monthlyMonthly salary12$8,666.67/month
bi_weeklyBi-weekly wage26$4,000/bi-week
weeklyWeekly wage52$2,000/week
dailyDaily rate365$285/day
hourlyHourly rate2080$50/hour

Tax is calculated consistently: same annual income = same annual tax, regardless of frequency.

Payroll Processing

// Single employee
const result = await payroll.processSalary({
  organizationId,
  employeeId,
  month: 1,
  year: 2024,
  paymentDate: new Date(),
  paymentMethod: 'bank',
  payrollRunType: 'regular',  // 'regular' | 'supplemental' | 'retroactive' | 'off-cycle'
});
// Returns: { employee, payrollRecord, transaction }

// Bulk processing
const bulk = await payroll.processBulkPayroll({
  organizationId,         // Optional in single-tenant mode or with context.organizationId
  month: 1,
  year: 2024,
  employeeIds: [],        // Optional: specific employees (default: all active + on_leave)
  batchSize: 50,
  concurrency: 5,
  onProgress: (p) => console.log(`${p.percentage}%`),
});
// Returns: { successCount, failCount, totalAmount, successful[], failed[] }

Duplicate Protection

The package provides database-level duplicate protection via a unique compound index:

// Unique index on: (organizationId, employeeId, period.month, period.year, payrollRunType)
// With partial filter: { isVoided: { $eq: false } }

// This allows:
// - One active record per employee per period per run type
// - Multiple run types in same period (regular + supplemental)
// - Re-processing after voiding (requires restorePayroll() first)
// - Re-processing after reversing

Important: Voided records require restorePayroll() before re-processing. Voided is a terminal state that preserves audit trail.

Two-Phase Export

Safe export that only marks records after downstream confirms receipt:

// Phase 1: Prepare (records NOT marked)
const { records, exportId } = await payroll.prepareExport({
  organizationId,
  startDate: new Date('2024-01-01'),
  endDate: new Date('2024-01-31'),
});

// Send to external system...

// Phase 2a: Confirm success (marks records)
await payroll.confirmExport({ organizationId, exportId });

// Phase 2b: Cancel if failed (records stay unmarked)
await payroll.cancelExport({ organizationId, exportId, reason: 'API error' });

Void / Reverse / Restore

// Void unpaid payroll (pending, processing, failed)
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 (blocked if replacement exists)
await payroll.restorePayroll({
  organizationId,
  payrollRecordId,
  reason: 'Voided in error',
});

Status Flow:

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

Leave Management

// Request leave
await payroll.requestLeave({
  employeeId,
  organizationId,
  leaveType: 'annual',  // 'annual' | 'sick' | 'unpaid' | 'maternity' | 'paternity'
  startDate: new Date('2024-03-01'),
  endDate: new Date('2024-03-05'),
  reason: 'Vacation',
});

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

// Reject
await payroll.rejectLeave({
  leaveRequestId,
  organizationId,
  rejectedBy: managerId,
  rejectionReason: 'Insufficient leave balance',
});

// Get balance
const balance = await payroll.getLeaveBalance({ employeeId, organizationId });
// { annual: { total: 20, used: 5, remaining: 15 }, sick: {...}, ... }

Pure Calculators (No DB Required)

Import from @classytic/payroll/calculators for client-side or serverless:

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

Salary Breakdown

const breakdown = calculateSalaryBreakdown({
  employee: {
    hireDate: new Date('2024-01-01'),
    terminationDate: null,
    compensation: {
      baseAmount: 100000,
      frequency: 'monthly',
      currency: 'USD',
      allowances: [
        { type: 'housing', amount: 20000, taxable: true },
        { type: 'transport', amount: 5000, taxable: true },
      ],
      deductions: [
        { type: 'provident_fund', amount: 5000, auto: true },
      ],
    },
  },
  period: {
    month: 3,
    year: 2024,
    startDate: new Date('2024-03-01'),
    endDate: new Date('2024-03-31'),
  },
  attendance: {
    expectedDays: 22,
    actualDays: 20,
  },
  config: {
    allowProRating: true,
    autoDeductions: true,
    defaultCurrency: 'USD',
    attendanceIntegration: true,
  },
  taxBrackets: [
    { min: 0, max: 600000, rate: 0 },
    { min: 600000, max: 1200000, rate: 0.1 },
    { min: 1200000, max: Infinity, rate: 0.2 },
  ],
});

// Returns PayrollBreakdown
{
  baseAmount: number,
  allowances: Array<{ type, amount, taxable }>,
  deductions: Array<{ type, amount, description }>,
  grossSalary: number,
  netSalary: number,
  taxableAmount: number,
  taxAmount: number,
  workingDays: number,
  actualDays: number,
  proRatedAmount: number,
  attendanceDeduction: number,
}

Pro-Rating

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

const result = calculateProRating({
  hireDate: new Date('2024-03-15'),
  terminationDate: null,
  periodStart: new Date('2024-03-01'),
  periodEnd: new Date('2024-03-31'),
  workingDays: [1, 2, 3, 4, 5],
  holidays: [],
});

// Returns ProRatingResult
{
  isProRated: true,
  ratio: 0.545,
  periodWorkingDays: 22,
  effectiveWorkingDays: 12,
  reason: 'new_hire',
}

Events

payroll.on('employee:hired', (payload) => { /* { employee, organizationId } */ });
payroll.on('employee:terminated', (payload) => { /* { employee, reason } */ });
payroll.on('salary:processed', (payload) => { /* { payrollRecord, transaction } */ });
payroll.on('payroll:completed', (payload) => { /* { summary, period } */ });
payroll.on('payroll:exported', (payload) => { /* { exportId, recordCount } */ });

Webhooks

// Register webhook
payroll.registerWebhook({
  url: 'https://api.example.com/webhooks',
  events: ['salary:processed', 'employee:hired'],
  secret: 'your-secret',
});

// Verify signature in handler
const signature = req.headers['x-payroll-signature'];
const timestamp = req.headers['x-payroll-timestamp'];
const signedPayload = `${timestamp}.${JSON.stringify(req.body)}`;
const expected = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex');

Configuration

Multi-Tenant (Default)

const payroll = createPayrollInstance()
  .withModels({ EmployeeModel, PayrollRecordModel, TransactionModel })
  .withConfig({
    payroll: {
      defaultCurrency: 'USD',
      attendanceIntegration: true,
      allowProRating: true,
      autoDeductions: true,
    },
  })
  .build();

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

Single-Tenant

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

// organizationId auto-injected
await payroll.hire({ employment, compensation });

Key Types

import type {
  // Documents
  EmployeeDocument,
  PayrollRecordDocument,
  LeaveRequestDocument,

  // Core types
  Compensation,
  Allowance,
  Deduction,
  PayrollBreakdown,
  TaxBracket,
  BankDetails,

  // Params
  HireEmployeeParams,
  ProcessSalaryParams,
  ProcessBulkPayrollParams,
  ExportPayrollParams,

  // Results
  ProcessSalaryResult,
  BulkPayrollResult,

  // Enums
  EmployeeStatus,      // 'active' | 'on_leave' | 'suspended' | 'terminated'
  PayrollStatus,       // 'pending' | 'processing' | 'paid' | 'failed' | 'voided' | 'reversed'
  PayrollRunType,      // 'regular' | 'supplemental' | 'retroactive' | 'off-cycle'
  LeaveType,           // 'annual' | 'sick' | 'unpaid' | 'maternity' | 'paternity'
  AllowanceType,       // 'housing' | 'transport' | 'meal' | 'mobile' | 'medical' | 'bonus' | 'other'
  DeductionType,       // 'tax' | 'loan' | 'advance' | 'provident_fund' | 'insurance'
  PaymentFrequency,    // 'monthly' | 'bi_weekly' | 'weekly' | 'daily' | 'hourly'
  PaymentMethod,       // 'bank' | 'cash' | 'check'
} from '@classytic/payroll';

Schemas

import {
  createEmployeeSchema,
  createPayrollRecordSchema,
  employeeIndexes,
  payrollRecordIndexes,
} from '@classytic/payroll/schemas';

// Create with custom fields
const employeeSchema = createEmployeeSchema({
  skills: [String],
  certifications: [{ name: String, date: Date }],
});

// Apply indexes
employeeIndexes.forEach(idx => employeeSchema.index(idx.fields, idx.options));

Utilities

import {
  // Date
  addDays, addMonths, diffInDays, startOfMonth, endOfMonth,
  getPayPeriod, getWorkingDaysInMonth,

  // Money (banker's rounding)
  roundMoney, percentageOf, prorateAmount,

  // Query builders
  toObjectId, isValidObjectId,
} from '@classytic/payroll/utils';

Error Handling

import {
  PayrollError,
  DuplicatePayrollError,
  EmployeeNotFoundError,
  NotEligibleError,
  ValidationError,
} from '@classytic/payroll';

try {
  await payroll.processSalary({ organizationId, employeeId, month, year });
} catch (error) {
  if (error instanceof DuplicatePayrollError) {
    // Already processed for this period + run type
  } else if (error instanceof EmployeeNotFoundError) {
    // Employee doesn't exist
  } else if (error instanceof NotEligibleError) {
    // Employee not eligible (terminated, etc.)
  }
}

License

MIT

Keywords

hrm

FAQs

Package last updated on 03 Feb 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts