saldo
Version:
Portuguese salary calculator library
171 lines (150 loc) • 6.47 kB
text/typescript
import { LocationT, RetentionPathsSchema, SituationCodesT } from "@/config/schemas";
import { taxTablesData } from "@/data/tax-tables-manifest"; // Import the pre-loaded JSON data
// Interface for the raw data structure of a tax bracket
interface TaxBracketData {
signal: "max" | "min";
limit: number;
max_marginal_rate: number;
deduction: number;
var1_deduction: number;
var2_deduction: number;
dependent_aditional_deduction: number;
effective_mensal_rate: number;
}
class TaxBracket {
signal: "max" | "min";
limit: number;
max_marginal_rate: number;
deduction: number;
var1_deduction: number;
var2_deduction: number;
dependent_aditional_deduction: number;
effective_mensal_rate: number;
constructor(data: TaxBracketData) {
this.signal = data.signal;
this.limit = data.limit;
this.max_marginal_rate = data.max_marginal_rate;
this.deduction = data.deduction;
this.var1_deduction = data.var1_deduction;
this.var2_deduction = data.var2_deduction;
this.dependent_aditional_deduction = data.dependent_aditional_deduction;
this.effective_mensal_rate = data.effective_mensal_rate;
}
calculate_deductible(salary: number): number {
// In Python, 0.0 is falsy. If var1_deduction or var2_deduction can be 0.0
// and that should prevent this path, this JS/TS equivalent is fine.
if (this.var1_deduction && this.var2_deduction) {
return this.deduction * this.var1_deduction * (this.var2_deduction - salary);
}
return this.deduction;
}
calculate_tax(
taxable_income: number,
twelfths_income: number,
number_of_dependents: number = 0,
extra_deduction: number = 0
): number {
const deduction = this.calculate_deductible(taxable_income);
let rate: number;
if (number_of_dependents >= 3) {
// https://diariodarepublica.pt/dr/detalhe/despacho/9971-a-2024-885806206#:~:text=h)%20Aos%20titulares,abater%20por%20dependente%3B
/*
h) Aos titulares de rendimentos de trabalho dependente com três ou mais dependentes
que se enquadrem nas tabelas previstas nas alíneas a) e b) do n.º 1, é aplicada uma
redução de um ponto percentual à taxa marginal máxima correspondente ao escalão em que
se integram, mantendo-se inalterada a parcela a abater e a parcela adicional a abater por dependente;
*/
rate = this.max_marginal_rate - 0.01;
} else {
rate = this.max_marginal_rate;
}
const base_tax =
taxable_income * rate -
deduction -
extra_deduction -
number_of_dependents * this.dependent_aditional_deduction;
// effective rate is the actual rate that is applied to the income after the deductions
// this is what we use to calculate the tax for the twelfths income
const effective_rate = taxable_income !== 0 ? base_tax / taxable_income : 0; // Avoid division by zero
const twelfths_tax = twelfths_income * effective_rate;
const tax = base_tax + twelfths_tax;
return Math.max(0, tax);
}
}
// Interface for the raw JSON structure when loading from file
// This interface should ideally be the same as TaxTableJsonData in the manifest
interface TaxRetentionTableJsonData {
situation: string;
description: string;
brackets: TaxBracketData[];
dependent_disabled_addition_deduction?: number;
}
class TaxRetentionTable {
region: string;
situation: string;
description: string;
tax_brackets: TaxBracket[]; // Array of TaxBracket class instances
dependent_disabled_addition_deduction?: number;
constructor(
region: string,
situation: string,
description: string,
tax_brackets_data: TaxBracketData[], // Accepts raw data for brackets
dependent_disabled_addition_deduction?: number
) {
this.region = region;
this.situation = situation;
this.description = description;
this.tax_brackets = tax_brackets_data.map(b => new TaxBracket(b));
this.dependent_disabled_addition_deduction = dependent_disabled_addition_deduction;
}
find_bracket(salary: number): TaxBracket {
for (const bracket of this.tax_brackets) {
if (bracket.signal === "max" && salary <= bracket.limit) {
return bracket;
} else if (bracket.signal === "min" && salary > bracket.limit) {
return bracket;
}
}
throw new Error(`No bracket found for salary ${salary}`);
}
// Renamed from load_from_file and modified to accept data directly
static from_data(region: string, data: TaxRetentionTableJsonData): TaxRetentionTable {
if (!data) {
throw new Error(`Tax table data not provided.`);
}
return new TaxRetentionTable(
region, // Region might need to be passed or inferred differently
data.situation,
data.description,
data.brackets, // Pass the raw bracket data
data.dependent_disabled_addition_deduction
);
}
static load(
date_start: Date,
date_end: Date,
location: LocationT,
situation_code: SituationCodesT
): TaxRetentionTable {
const year = date_start.getFullYear();
const retentionTablePathGenerator = new RetentionPathsSchema(
date_start,
date_end,
location,
situation_code,
year
);
const identifier = retentionTablePathGenerator.path; // This now returns the identifier string
const tableData = taxTablesData[identifier]; // Look up in the manifest
if (!tableData) {
throw new Error(`Tax table not found for identifier: ${identifier}. Ensure it's in tax-tables-manifest.ts`);
}
// Assuming 'location' can be used as the region directly.
// The original load_from_file hardcoded "continente".
// We use the 'location' parameter now, which seems more correct.
return TaxRetentionTable.from_data(location, tableData as TaxRetentionTableJsonData);
}
}
// Export the classes if they are to be used by other modules
export { TaxBracket, TaxRetentionTable };