Skip to content

Commit

Permalink
chore(update Freibetraege for 2024) (#1533)
Browse files Browse the repository at this point in the history
* simplify freibetrag interface(s)

* update freibetraege for 2024, use year map for ease of updating, replace magic strings and update tests

* update unit test to ensure fallback works, regardless of whatever the actual, current year is

* add const for simplified child allowance
  • Loading branch information
Spencer6497 authored Dec 10, 2024
1 parent c4a39bd commit ccff44f
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 82 deletions.
135 changes: 87 additions & 48 deletions app/domains/beratungshilfe/vorabcheck/__test__/freibetrag.test.ts
Original file line number Diff line number Diff line change
@@ -1,157 +1,192 @@
import {
freibetrag,
BASE_ALLOWANCE,
calculateFreibetrag,
freibetraegePerYear,
getFreibetraege,
getVerfuegbaresEinkommenFreibetrag,
latestFreibetraegeYear,
SIMPLIFIED_CHILD_ALLOWANCE,
} from "~/domains/beratungshilfe/vorabcheck/freibetrag";
import { today } from "~/util/date";

describe("freibetrag", () => {
it("should return 572 when single not working", () => {
const { incomeAllowance, partnerAllowance, childrenBelow6Allowance } =
getFreibetraege(today().getFullYear());

vi.spyOn(console, "warn");

describe("getFreibetraege", () => {
it(`returns Freibetraege for ${latestFreibetraegeYear}`, () => {
expect(getFreibetraege(latestFreibetraegeYear)).toEqual(
freibetraegePerYear[latestFreibetraegeYear],
);
});

it("returns Freibetraege for the last valid year if current year is not found, and shows the user a warning", () => {
const nonExistentYear = latestFreibetraegeYear + 1;
const freibetraege = getFreibetraege(nonExistentYear);
// eslint-disable-next-line no-console
expect(console.warn).toHaveBeenCalledWith(
`No Freibeträge for year ${nonExistentYear}, using last valid Freibeträge from ${latestFreibetraegeYear}`,
);
expect(freibetraege).toEqual(freibetraegePerYear[latestFreibetraegeYear]);
});
});

describe("calculateFreibetrag", () => {
// Need values in cents
const baseAllowanceCents = BASE_ALLOWANCE * 100;
const incomeAllowanceCents = incomeAllowance * 100;
const partnerAllowanceCents = partnerAllowance * 100;
const childrenBelow6AllowanceCents = childrenBelow6Allowance * 100;

it(`should return ${baseAllowanceCents} when single not working`, () => {
expect(
freibetrag({
calculateFreibetrag({
working: false,
partnership: false,
}),
).toEqual(57200);
).toEqual(baseAllowanceCents);
});

it("should return 572 + 251 when single working", () => {
it(`should return ${baseAllowanceCents} + ${incomeAllowanceCents} when single working`, () => {
expect(
freibetrag({
calculateFreibetrag({
working: true,
partnership: false,
}),
).toEqual(57200 + 25100);
).toEqual(baseAllowanceCents + incomeAllowanceCents);
});

it("should return 572 + 552 when partner not working", () => {
it(`should return ${baseAllowanceCents} + ${partnerAllowanceCents} when partner not working`, () => {
expect(
freibetrag({
calculateFreibetrag({
working: false,
partnership: true,
}),
).toEqual(57200 + 55200);
).toEqual(baseAllowanceCents + partnerAllowanceCents);
});

it("should return 572 + 552 - partner income when partner working smaller than 552", () => {
it(`should return ${baseAllowanceCents} + ${partnerAllowanceCents} - partner income when partner working smaller than ${partnerAllowanceCents}`, () => {
expect(
freibetrag({
calculateFreibetrag({
working: false,
partnership: true,
partnerIncome: 50000,
}),
).toEqual(57200 + 55200 - 50000);
).toEqual(baseAllowanceCents + partnerAllowanceCents - 50000);
});

it("should return 572 when partner working bigger than 552", () => {
it(`should return ${baseAllowanceCents} when partner working bigger than ${partnerAllowanceCents}`, () => {
expect(
freibetrag({
calculateFreibetrag({
working: false,
partnership: true,
partnerIncome: 80000,
}),
).toEqual(57200);
).toEqual(baseAllowanceCents);
});

it("should return 572 + 350 when 1 child 0-6 not working", () => {
it(`should return ${baseAllowanceCents} + ${childrenBelow6AllowanceCents} when 1 child 0-6 not working`, () => {
expect(
freibetrag({
calculateFreibetrag({
childrenBelow6: 1,
}),
).toEqual(57200 + 35000);
).toEqual(baseAllowanceCents + childrenBelow6AllowanceCents);
});

it("should return 572 + 2*350 when 2 children 0-6 not working", () => {
it(`should return ${baseAllowanceCents} + 2*${childrenBelow6AllowanceCents} when 2 children 0-6 not working`, () => {
expect(
freibetrag({
calculateFreibetrag({
childrenBelow6: 2,
}),
).toEqual(57200 + 2 * 35000);
).toEqual(baseAllowanceCents + 2 * childrenBelow6AllowanceCents);
});

it("should return 572 + 350 - child income when 1 child 0-6 working less than 350", () => {
it(`should return ${baseAllowanceCents} + ${childrenBelow6AllowanceCents} - child income when 1 child 0-6 working less than ${childrenBelow6AllowanceCents}`, () => {
expect(
freibetrag({
calculateFreibetrag({
childrenBelow6: 1,
childrenIncome: 10000,
}),
).toEqual(57200 + 35000 - 10000);
).toEqual(baseAllowanceCents + childrenBelow6AllowanceCents - 10000);
});

it("should return 572 when 1 child 0-6 working more than 350", () => {
it(`should return ${baseAllowanceCents} when 1 child 0-6 working more than ${childrenBelow6AllowanceCents}`, () => {
expect(
freibetrag({
calculateFreibetrag({
childrenBelow6: 1,
childrenIncome: 80000,
}),
).toEqual(57200);
).toEqual(baseAllowanceCents);
});

it("should return 572 + 2*350 - children income when 2 children 0-6 working less than 350", () => {
it(`should return ${baseAllowanceCents} + 2*${childrenBelow6AllowanceCents} - children income when 2 children 0-6 working less than ${childrenBelow6AllowanceCents}`, () => {
expect(
freibetrag({
calculateFreibetrag({
childrenBelow6: 2,
childrenIncome: 10000,
}),
).toEqual(57200 + 2 * 35000 - 10000);
).toEqual(baseAllowanceCents + 2 * childrenBelow6AllowanceCents - 10000);
});

it("should return 572 + 2*350 - children income when 2 children 0-6 working more than 350 less than 2*350", () => {
it(`should return ${baseAllowanceCents} + 2*${childrenBelow6AllowanceCents} - children income when 2 children 0-6 working more than ${childrenBelow6AllowanceCents} less than 2*${childrenBelow6AllowanceCents}`, () => {
expect(
freibetrag({
calculateFreibetrag({
childrenBelow6: 2,
childrenIncome: 50000,
}),
).toEqual(57200 + 2 * 35000 - 50000);
).toEqual(baseAllowanceCents + 2 * childrenBelow6AllowanceCents - 50000);
});

it("should handle NaN children as zero children", () => {
it("should handle undefined children as zero children", () => {
expect(
freibetrag({
childrenBelow6: NaN,
calculateFreibetrag({
childrenBelow6: undefined,
}),
).toEqual(57200);
).toEqual(baseAllowanceCents);
});
});

const cases = [
{
condition: "empty context",
context: {},
result: 572,
result: BASE_ALLOWANCE,
},
{
condition: "partner",
context: { partnerschaft: "yes" },
result: 572 + 552,
result: BASE_ALLOWANCE + partnerAllowance,
},
{
condition: "partner and no kid but kinderAnzahlKurz given",
context: { partnerschaft: "yes", kinderAnzahlKurz: "1" },
result: 572 + 552,
result: BASE_ALLOWANCE + partnerAllowance,
},
{
condition: "partner and one kid",
context: { partnerschaft: "yes", kinderKurz: "yes", kinderAnzahlKurz: "1" },
result: 572 + 552 + 400,
result: BASE_ALLOWANCE + partnerAllowance + SIMPLIFIED_CHILD_ALLOWANCE,
},
{
condition: "no partner and one kid",
context: { partnerschaft: "no", kinderKurz: "yes", kinderAnzahlKurz: "1" },
result: 572 + 400,
result: BASE_ALLOWANCE + SIMPLIFIED_CHILD_ALLOWANCE,
},
{
condition: "no partner and three kids",
context: { partnerschaft: "no", kinderKurz: "yes", kinderAnzahlKurz: "3" },
result: 572 + 3 * 400,
result: BASE_ALLOWANCE + 3 * SIMPLIFIED_CHILD_ALLOWANCE,
},
{
condition: "erwerbstaetig",
context: { erwerbstaetigkeit: "yes" },
result: 572 + 251,
result: BASE_ALLOWANCE + incomeAllowance,
},
{
condition: "partner and erwerbstaetig",
context: { partnerschaft: "yes", erwerbstaetigkeit: "yes" },
result: 572 + 552 + 251,
result: BASE_ALLOWANCE + partnerAllowance + incomeAllowance,
},
{
condition: "partner and erwerbstaetig and two kids",
Expand All @@ -161,7 +196,11 @@ const cases = [
kinderKurz: "yes",
kinderAnzahlKurz: "2",
},
result: 572 + 552 + 251 + 2 * 400,
result:
BASE_ALLOWANCE +
partnerAllowance +
incomeAllowance +
2 * SIMPLIFIED_CHILD_ALLOWANCE,
},
] as const;

Expand Down
111 changes: 85 additions & 26 deletions app/domains/beratungshilfe/vorabcheck/freibetrag.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,64 @@
import mapValues from "lodash/mapValues";
import { today } from "~/util/date";
import { type AllContexts } from "../../common";

export function freibetrag({
type Freibetraege = {
incomeAllowance: number;
partnerAllowance: number;
dependentAdultAllowance: number;
children15To18Allowance: number;
children7To14Allowance: number;
childrenBelow6Allowance: number;
};

export const BASE_ALLOWANCE = 572;
export const SIMPLIFIED_CHILD_ALLOWANCE = 400;
export const freibetraegePerYear: Record<number, Freibetraege> = {
2023: {
incomeAllowance: 251,
partnerAllowance: 552,
dependentAdultAllowance: 442,
children15To18Allowance: 462,
children7To14Allowance: 383,
childrenBelow6Allowance: 350,
},
2024: {
incomeAllowance: 282,
partnerAllowance: 619,
dependentAdultAllowance: 496,
children15To18Allowance: 518,
children7To14Allowance: 429,
childrenBelow6Allowance: 393,
},
};
export const latestFreibetraegeYear = Math.max(
...Object.keys(freibetraegePerYear).map((year) => Number(year)),
);

export function getFreibetraege(year: number) {
const freibetraege = freibetraegePerYear[year];
if (!freibetraege) {
// eslint-disable-next-line no-console
console.warn(
`No Freibeträge for year ${year}, using last valid Freibeträge from ${latestFreibetraegeYear}`,
);
return freibetraegePerYear[latestFreibetraegeYear];
}
return freibetraege;
}

type CalculateFreibetragProps = {
working: boolean;
partnership: boolean;
partnerIncome: number;
childrenBelow6: number;
children7To14: number;
children15To18: number;
childrenAbove18: number;
childrenIncome: number;
};

export function calculateFreibetrag({
working,
partnership,
partnerIncome,
Expand All @@ -9,48 +67,49 @@ export function freibetrag({
children15To18,
childrenAbove18,
childrenIncome,
}: {
working?: boolean;
partnership?: boolean;
partnerIncome?: number;
childrenBelow6?: number;
children7To14?: number;
children15To18?: number;
childrenAbove18?: number;
childrenIncome?: number;
}): number {
let betrag = 57200;
}: Partial<CalculateFreibetragProps>): number {
let betrag = BASE_ALLOWANCE * 100;
const {
incomeAllowance,
partnerAllowance,
dependentAdultAllowance,
children15To18Allowance,
children7To14Allowance,
childrenBelow6Allowance,
} = mapValues(getFreibetraege(today().getFullYear()), (v) => v * 100);

if (working) {
betrag += 25100;
betrag += incomeAllowance;
}

if (partnership) {
betrag += Math.max(55200 - (partnerIncome ?? 0), 0);
betrag += Math.max(partnerAllowance - (partnerIncome ?? 0), 0);
}

const childrenFreibetrag =
(Number.isNaN(childrenBelow6) ? 0 : (childrenBelow6 ?? 0)) * 35000 +
(Number.isNaN(children7To14) ? 0 : (children7To14 ?? 0)) * 38300 +
(Number.isNaN(children15To18) ? 0 : (children15To18 ?? 0)) * 46200 +
(Number.isNaN(childrenAbove18) ? 0 : (childrenAbove18 ?? 0)) * 44200;
(childrenBelow6 ? childrenBelow6 * childrenBelow6Allowance : 0) +
(children7To14 ? children7To14 * children7To14Allowance : 0) +
(children15To18 ? children15To18 * children15To18Allowance : 0) +
(childrenAbove18 ? childrenAbove18 * dependentAdultAllowance : 0);

betrag += Math.max(childrenFreibetrag - (childrenIncome ?? 0), 0);

return betrag;
}

function freibetragShort(
working?: boolean,
partnership?: boolean,
childrenCount?: number,
working: boolean,
partnership: boolean,
childrenCount: number,
): number {
const betrag = 552 + 20;
const { partnerAllowance, incomeAllowance } = getFreibetraege(
today().getFullYear(),
);
return (
betrag +
(working ? 251 : 0) +
(partnership ? 552 : 0) +
(childrenCount ? childrenCount * 400 : 0)
BASE_ALLOWANCE +
(working ? incomeAllowance : 0) +
(partnership ? partnerAllowance : 0) +
(childrenCount ? childrenCount * SIMPLIFIED_CHILD_ALLOWANCE : 0)
);
}

Expand Down
Loading

0 comments on commit ccff44f

Please sign in to comment.