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
JavaScript
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);