saldo
Version:
Portuguese salary calculator library
193 lines (170 loc) • 6.47 kB
text/typescript
import { describe, it, expect, beforeEach, vi } from "vitest";
import { simulateDependentWorker } from "@/dependent-worker/simulator";
import { Twelfths } from "@/dependent-worker/schemas";
import { LunchAllowance } from "@/dependent-worker/lunch-allowance";
import * as ConfigSchemas from "@/config/schemas";
import { TaxRetentionTable as ActualTaxRetentionTable } from "@/tables/tax-retention"; // This import might be used if we were still partially mocking, but now we want the real one.
const defaultLunchAllowance = new LunchAllowance(10.2, "cupon", 22);
// Mock the calculation functions to control their output for tests
vi.mock("@/dependent-worker/calculations", () => ({
getPartnerExtraDeduction: vi.fn(
(
married: boolean,
numberOfHolders: number | null | undefined,
partnerDisabled: boolean
) => {
if (married && numberOfHolders === 1 && partnerDisabled) return 50; // Mocked value
return 0;
}
),
getTwelfthsIncome: vi.fn((income: number, twelfths: Twelfths) => {
return (income * Number(twelfths)) / 12; // Using Number(twelfths) as per stub
}),
getDisabledDependentExtraDeduction: vi.fn(
(taxRetentionTable: ActualTaxRetentionTable, numDisabled: number) => {
const baseDeduction =
taxRetentionTable?.dependent_disabled_addition_deduction || 20; // Mocked base
return baseDeduction * numDisabled;
}
),
}));
// Minimal mock for TaxRetentionTable and TaxBracket to avoid actual file loading in unit tests
vi.mock("@/tables/tax-retention", () => {
class MockTaxBracket {
constructor(public data: any) {}
calculate_tax(
taxable_income: number,
twelfths_income: number,
number_of_dependents: number = 0,
extra_deduction: number = 0
): number {
const basicTax =
(taxable_income + twelfths_income) * 0.1 -
extra_deduction -
number_of_dependents * 10;
return Math.max(0, basicTax);
}
}
class MockTaxRetentionTable {
dependent_disabled_addition_deduction?: number;
constructor(
public situationCode: string,
public dependentDisabledDeductionVal?: number
) {
this.dependent_disabled_addition_deduction =
dependentDisabledDeductionVal;
}
static load(
date_start: Date,
date_end: Date,
location: ConfigSchemas.LocationT,
situation_code: string
) {
return new MockTaxRetentionTable(situation_code, 30);
}
find_bracket(taxableIncome: number) {
return new MockTaxBracket({
limit: taxableIncome + 1000,
max_marginal_rate: 0.2,
});
}
}
return { TaxRetentionTable: MockTaxRetentionTable };
});
describe("simulateDependentWorker", () => {
const baseIncome = 1000;
it("should calculate for a basic scenario with defaults", () => {
const result = simulateDependentWorker({ income: baseIncome });
expect(result).toBeDefined();
expect(result.taxableIncome).toBe(
baseIncome + defaultLunchAllowance.taxableMonthlyValue
);
expect(result.socialSecurityTax).toEqual(0.11);
expect(result.lunchAllowance).toEqual(
new LunchAllowance(10.2, "cupon", 22)
);
});
it("should calculate for a married individual, 1 holder, 2 dependents", () => {
const incomeVal = 2000;
const result = simulateDependentWorker({
income: incomeVal,
married: true,
numberOfHolders: 1,
numberOfDependents: 2,
numberOfDependentsDisabled: 0,
});
expect(result).toBeDefined();
expect(result.taxableIncome).toBe(
incomeVal + defaultLunchAllowance.taxableMonthlyValue
);
});
it("should calculate with worker disability and custom lunch allowance", () => {
const incomeVal = 1500;
const result = simulateDependentWorker({
income: incomeVal,
disabled: true,
lunchAllowanceDailyValue: 8,
lunchAllowanceMode: "salary",
lunchAllowanceDaysCount: 22,
});
expect(result).toBeDefined();
expect(result.lunchAllowance.dailyValue).toBe(8);
expect(result.taxableIncome).toBe(incomeVal + 44);
});
it("should calculate for 'acores' location and no twelfths", () => {
const incomeVal = 2500;
const result = simulateDependentWorker({
income: incomeVal,
location: "acores",
twelfths: Twelfths.NONE,
});
expect(result).toBeDefined();
const expectedTwelfthsIncome = (incomeVal * Twelfths.NONE) / 12;
const expectedTaxable =
incomeVal + defaultLunchAllowance.taxableMonthlyValue;
const expectedRetentionIncome = expectedTaxable + expectedTwelfthsIncome;
expect(result.grossIncome).toBe(
expectedRetentionIncome + defaultLunchAllowance.taxFreeMonthlyValue
);
});
it("should throw error for invalid numberOfHolders when married", () => {
expect(() => {
simulateDependentWorker({
income: 1000,
married: true,
numberOfHolders: null, // Invalid for married
});
}).toThrow("'numberOfHolders' is required for married workers");
});
it("should throw error if situation cannot be determined", () => {
// Spy on SituationUtils.getSituation and make it return undefined for this test
const getSituationSpy = vi.spyOn(
ConfigSchemas.SituationUtils,
"getSituation"
);
getSituationSpy.mockReturnValueOnce(undefined);
expect(() => {
simulateDependentWorker({
income: 1000,
married: true,
numberOfHolders: 1,
disabled: false,
// The specific value of numberOfDependents doesn't strictly matter here due to the mock,
// but using a common value like 1 or 0 is fine.
numberOfDependents: 1,
});
}).toThrow(/Could not determine situation for the given parameters/);
// Restore the original implementation after the test
getSituationSpy.mockRestore();
});
// Example test for checking yearly values (highly dependent on mocked calculations)
it("should calculate yearly gross and net salaries correctly based on (mocked) monthly values", () => {
const income = 1200;
const twelfths = Twelfths.ONE_MONTH; // example
const result = simulateDependentWorker({ income, twelfths });
const expectedYearlyGrossSalary = 19268.4;
const expectedYearlyNetSalary = 16413.6;
expect(result.yearlyGrossSalary).toBeCloseTo(expectedYearlyGrossSalary);
expect(result.yearlyNetSalary).toBeCloseTo(expectedYearlyNetSalary);
});
});