UNPKG

cal-sal

Version:

A CLI tool for calculating monthly salary with tax deductions for Sri Lanka (2025 tax scheme)

220 lines (188 loc) 7.62 kB
#!/usr/bin/env node import * as p from '@clack/prompts'; import color from 'picocolors'; import fs from 'fs'; import path from 'path'; import PDFDocument from 'pdfkit'; import downloadsFolder from 'downloads-folder'; import { exec } from 'child_process'; // Tax Scheme Constants (2025) const TAX_FREE_THRESHOLD = 1800000; const TAX_BRACKETS = [ { upTo: 1000000, rate: 0.06 }, { upTo: 500000, rate: 0.18 }, { upTo: 500000, rate: 0.24 }, { upTo: 500000, rate: 0.30 }, { upTo: Infinity, rate: 0.36 }, ]; const EPF_RATE = 0.08; const STAMP_DUTY = 25; const SPORTS_CLUB = 350; // Utility Functions const formatValue = (value) => { return Number(value).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2, }); }; const calculateAnnualTax = (income, taxFreeThreshold, taxBrackets) => { const annualIncome = income * 12; if (annualIncome <= taxFreeThreshold) return 0; let tax = 0, remainingIncome = annualIncome - taxFreeThreshold; for (const { upTo, rate } of taxBrackets) { const taxableAmount = Math.min(remainingIncome, upTo); tax += taxableAmount * rate; remainingIncome -= taxableAmount; if (remainingIncome <= 0) break; } return tax / 12; // Monthly tax }; const calculateEPFContribution = (basicSalary) => basicSalary * EPF_RATE; const calculateNetSalary = (grossSalary, tax, epf) => { const totalDeductions = tax + epf + STAMP_DUTY + SPORTS_CLUB; return { netSalary: grossSalary - totalDeductions, totalDeductions }; }; const calculateExtraPayRate = (basicSalary) => { const totalHoursPerMonth = 240; const extraPayMultiplier = 1.5; const normalHourlyRate = basicSalary / totalHoursPerMonth; const extraPayRate = normalHourlyRate * extraPayMultiplier; return extraPayRate.toFixed(2); }; const exportToCSV = (filename, data) => { const filepath = path.join(downloadsFolder(), filename); const content = Object.entries(data) .map(([key, value]) => `"${key}","${value}"`) .join('\n'); fs.writeFileSync(filepath, content, 'utf8'); openFile(filepath); }; const exportToPDF = (filename, summaryText) => { const filepath = path.join(downloadsFolder(), filename); const doc = new PDFDocument(); const stream = fs.createWriteStream(filepath); doc.pipe(stream); doc.fontSize(12).text(summaryText); doc.end(); stream.on('finish', () => { openFile(filepath); }); }; const openFile = (filePath) => { const platform = process.platform; if (platform === 'darwin') { exec(`open "${filePath}"`); } else if (platform === 'win32') { exec(`start "" "${filePath}"`); } else if (platform === 'linux') { exec(`xdg-open "${filePath}"`); } else { console.warn('Automatic file opening is not supported on this OS.'); } }; // Main Function async function main() { console.clear(); p.intro(`${color.bgCyan(color.bold(color.black(' CAMMS Monthly Salary Calculator (2025 Tax Scheme) ')))}`); const { basicSalary, extraHours } = await p.group( { basicSalary: () => p.text({ message: 'Enter your basic salary (LKR/month):', placeholder: 'e.g., 100000', validate: (value) => { if (!value || isNaN(value) || Number(value) <= 0) return "Please enter a valid non-negative number."; }, }), extraHours: () => p.text({ message: 'Enter extra hours worked (OT):', placeholder: 'Enter 0 if none', validate: (value) => { if (!value || isNaN(value) || Number(value) < 0) return "Please enter a valid non-negative number."; }, initialValue: '0', }), }, { onCancel: () => process.exit(0) } ); const spinner = p.spinner(); spinner.start('Calculating salary details...'); // Calculations const basic = Number(basicSalary); const hours = Number(extraHours); let extraPay = 0; if (hours > 0) { const rate = Number(calculateExtraPayRate(basic)); extraPay = hours * rate; } const gross = basic + extraPay; const tax = calculateAnnualTax(gross, TAX_FREE_THRESHOLD, TAX_BRACKETS); const epf = calculateEPFContribution(basic); const { netSalary, totalDeductions } = calculateNetSalary(gross, tax, epf); spinner.stop(color.green('Calculation complete!')); // Summary const formattedOutput = ` ${color.bold(color.cyan('Salary Breakdown'))} ---------------------------------------- Basic Salary: ${color.green(formatValue(basic))} LKR Extra Hours Pay: ${color.green(formatValue(extraPay))} LKR ---------------------------------------- Gross Salary: ${color.green(formatValue(gross))} LKR ${color.bold(color.cyan('Deductions'))} ---------------------------------------- EPF Contribution: ${color.red(formatValue(epf))} LKR Tax (2025 Scheme): ${color.red(formatValue(tax))} LKR Stamp Duty: ${color.red(formatValue(STAMP_DUTY))} LKR Sports Club Fee: ${color.red(formatValue(SPORTS_CLUB))} LKR ---------------------------------------- Total Deductions: ${color.red(formatValue(totalDeductions))} LKR ${color.bold(color.cyan('Net Salary'))} ---------------------------------------- ${color.bold(color.green(formatValue(netSalary)))} LKR `; p.note(formattedOutput, 'Summary'); // Export Option const { exportChoice } = await p.group( { exportChoice: () => p.select({ message: 'Do you want to export the results?', options: [ { label: 'No Export', value: 'none' }, { label: 'CSV', value: 'csv' }, // { label: 'PDF', value: 'pdf' }, ], }), }, { onCancel: () => process.exit(0) } ); const exportData = { 'Basic Salary': `${formatValue(basic)} LKR`, 'Extra Hours Pay': `${formatValue(extraPay)} LKR`, 'Gross Salary': `${formatValue(gross)} LKR`, 'EPF Contribution': `${formatValue(epf)} LKR`, 'Tax': `${formatValue(tax)} LKR`, 'Stamp Duty': `${formatValue(STAMP_DUTY)} LKR`, 'Sports Club Fee': `${formatValue(SPORTS_CLUB)} LKR`, 'Total Deductions': `${formatValue(totalDeductions)} LKR`, 'Net Salary': `${formatValue(netSalary)} LKR`, }; const today = new Date(); const formatted = today.toISOString().split('T')[0]; // "2025-04-15" if (exportChoice === 'csv') { exportToCSV(`salary_summary_${formatted}.csv`, exportData); p.note(`CSV file saved as ~/Downloads/salary_summary_${formatted}.csv`, 'Export Complete'); } else if (exportChoice === 'pdf') { exportToPDF(`salary_summary_${formatted}.pdf`, formattedOutput); p.note(`PDF file saved as ~/Downloads/salary_summary_${formatted}.pdf`, 'Export Complete'); } p.outro(`${color.cyan('Thank you for using the Salary Calculator!')}`); } // Execute CLI main().catch(console.error);