UNPKG

saldo

Version:

Portuguese salary calculator library

134 lines (133 loc) 5.56 kB
import { FrequencyChoices, } from "./schemas"; import { TAX_RANKS } from "../data/tax-ranks-data"; import { YOUTH_IRS } from "../data/youth-irs-data"; import { YEAR_BUSINESS_DAYS } from "../independent-worker/consts"; export function calculateGrossIncome(income, incomeFrequency, nrDaysOff) { const result = { year: 0, month: 0, day: 0, }; switch (incomeFrequency) { case FrequencyChoices.Year: result.year = income; result.month = income / 12; // Always use 12 months for calculations result.day = income / (YEAR_BUSINESS_DAYS - nrDaysOff); break; case FrequencyChoices.Month: result.year = income * 12; // Always use 12 months for calculations result.month = income; result.day = result.year / (YEAR_BUSINESS_DAYS - nrDaysOff); break; case FrequencyChoices.Day: result.year = income * (YEAR_BUSINESS_DAYS - nrDaysOff); result.month = result.year / 12; // Always use 12 months for calculations result.day = income; } return result; } export function calculateSsPay(grossIncome, ssTax, ssDiscount, maxSsIncome, firstYearForSs, nrDaysOff) { if (firstYearForSs) { return { year: 0, month: 0, day: 0, }; } // We first calculate 70% of the gross income, with the discount applied, // then we compare it to the maximum SS income, and we take the minimum const monthSS = ssTax * Math.min(maxSsIncome, grossIncome.month * 0.7 * (1 + ssDiscount)); const yearSSPay = Math.max(12 * monthSS, 20 * 12); return { year: yearSSPay, month: Math.max(monthSS, 20), day: yearSSPay / (YEAR_BUSINESS_DAYS - nrDaysOff), }; } export function calculateSpecificDeductions(ssPay, grossIncome) { return Math.max(4104, Math.min(ssPay.year, 0.1 * grossIncome.year)); } export function calculateMaxExpenses(grossIncome, maxExpensesTax) { return (maxExpensesTax / 100) * grossIncome.year; } export function calculateExpensesNeeded(maxExpenses, specificDeductions) { const expenses = maxExpenses - specificDeductions; return expenses > 0 ? expenses : 0; } export function calculateTaxableIncome(grossIncome, youthIrsDiscount, firstYear, secondYear, expensesNeeded, expenses) { const expensesMissing = expensesNeeded > expenses ? expensesNeeded - expenses : 0; return ((grossIncome.year - youthIrsDiscount) * (firstYear ? 0.375 : secondYear ? 0.5625 : 0.75) + expensesMissing); } export function calculateYouthIrsDiscount(benefitsOfYouthIrs, grossIncome, currentTaxRankYear, yearOfYouthIrs, currentIas) { if (!benefitsOfYouthIrs) { return 0; } const youthIrsRank = YOUTH_IRS[currentTaxRankYear][yearOfYouthIrs]; const maxDiscount = youthIrsRank.maxDiscountPercentage * grossIncome.year; const maxDiscountIas = youthIrsRank.maxDiscountIasMultiplier * currentIas; return Math.min(maxDiscount, maxDiscountIas); } export function findTaxRank(taxableIncome, currentTaxRankYear) { return TAX_RANKS[currentTaxRankYear].filter((taxRank, index) => { const isFirstRank = index === 0; const isLastRank = index === TAX_RANKS[currentTaxRankYear].length - 1; // Use <= for first bracket to include taxable income of 0 const isBiggerThanMin = isFirstRank ? taxRank.min <= taxableIncome : taxRank.min < taxableIncome; const isSmallerThanMax = taxRank.max === null || taxRank.max >= taxableIncome; if (isLastRank && isBiggerThanMin) { return true; } return isBiggerThanMin && isSmallerThanMax; })[0]; } export function calculateTaxIncomeAvg(taxRank, taxableIncome) { if (taxRank.id <= 1) { return taxableIncome; } // For the last bracket (where max is null), use min as the threshold return taxRank.max ?? taxRank.min; } export function calculateTaxIncomeNormal(taxRank, taxableIncome) { if (taxRank.id <= 1) { return 0; } return taxableIncome - calculateTaxIncomeAvg(taxRank, taxableIncome); } export function calculateIrsPay(taxableIncome, taxRank, rnh, rnhTax, nrDaysOff, taxRanks) { let yearIRS; if (rnh) { yearIRS = taxableIncome * rnhTax; } else { const taxIncomeAvg = calculateTaxIncomeAvg(taxRank, taxableIncome); const taxIncomeNormal = calculateTaxIncomeNormal(taxRank, taxableIncome); // For the last bracket (averageTax is null), use previous bracket's averageTax let avgTax = taxRank.averageTax; if (avgTax === null) { const previousBracket = taxRanks.find(r => r.id === taxRank.id - 1); avgTax = previousBracket?.averageTax ?? 0; } yearIRS = taxIncomeAvg * avgTax + taxIncomeNormal * taxRank.normalTax; } const monthIRS = Math.max(yearIRS / 12, 0); // Always use 12 months for calculations const yearIrsPay = Math.max(yearIRS, 0); return { year: yearIrsPay, month: monthIRS, day: yearIrsPay / (YEAR_BUSINESS_DAYS - nrDaysOff), }; } export function calculateNetIncome(grossIncome, irsPay, ssPay) { const monthIncome = grossIncome.month - irsPay.month - ssPay.month; const yearIncome = grossIncome.year - irsPay.year - ssPay.year; return { year: yearIncome, month: monthIncome, day: yearIncome / (YEAR_BUSINESS_DAYS - 0), // Using 0 for nrDaysOff in net income calculation }; }