diff --git a/README.md b/README.md index 5e3e65f..f083252 100644 --- a/README.md +++ b/README.md @@ -113,19 +113,18 @@ The `TaxConfig` dataclass provides type-safe configuration: | Parameter | Type | Default | Description | |-----------|------|---------|-------------| -| `year` | int | 2022 | Tax year (2018-2025 supported) | +| `year` | int | 2025 | Tax year (2018-2026 supported) | | `is_married` | bool | False | Married status (Ehegattensplitting) | | `has_children` | bool | False | Has children (affects nursing insurance) | | `church_tax` | float | 0.09 | Church tax rate (0.0-0.09, set to 0.0 for none) | -| `extra_health_insurance` | float | 0.014 | Additional health insurance rate | +| `extra_health_insurance` | float | 0.025 | Additional health insurance rate | ## Supported Tax Years -| Year | Status | Notes | -|------|--------|-------| -| 2018-2022 | Fully supported | Complete tax data | -| 2023-2025 | Fully supported | Complete tax data | -| 2026-2027 | Planned | To be added | +| Year | Status | Notes | +|-----------|--------|-------| +| 2018-2026 | Fully supported | Complete tax data (2026 uses 2025 estimates) | +| 2027+ | Planned | To be added | ## Documentation @@ -135,7 +134,7 @@ Full documentation is available at [netto.readthedocs.io](https://netto.readthed All tax calculations are based on official German government sources: -- **Tax Calculation Formulas**: [BMF Tarifhistorie](https://www.bmf-steuerrechner.de/Tarifhistorie_Steuerrechner.pdf) +- **Tax Calculation Formulas**: [BMF Tarifhistorie](https://www.bmf-steuerrechner.de/javax.faces.resource/2025_1_14_Tarifhistorie_Steuerrechner.pdf.xhtml) - **Wage Tax Calculator**: [BMF Lohnsteuerrechner](https://www.bmf-steuerrechner.de/) - **Social Security Deductible**: [Vorsorgepauschale](https://www.lohn-info.de/vorsorgepauschale.html) - **Social Security Rates**: [Sozialversicherungsbeiträge](https://www.lohn-info.de/sozialversicherungsbeitraege2024.html) diff --git a/data/pension_factors/2026.json b/data/pension_factors/2026.json new file mode 100644 index 0000000..f8bac80 --- /dev/null +++ b/data/pension_factors/2026.json @@ -0,0 +1,4 @@ +{ + "year": 2026, + "factor": 1.0 +} diff --git a/data/social_security/2023.json b/data/social_security/2023.json index e6b32dd..df8f1c2 100644 --- a/data/social_security/2023.json +++ b/data/social_security/2023.json @@ -6,7 +6,7 @@ }, "unemployment": { "limit": 87600, - "rate": 0.012 + "rate": 0.013 }, "health": { "limit": 59850, diff --git a/data/social_security/2024.json b/data/social_security/2024.json index cbcefc9..4b839e1 100644 --- a/data/social_security/2024.json +++ b/data/social_security/2024.json @@ -6,7 +6,7 @@ }, "unemployment": { "limit": 90600, - "rate": 0.012 + "rate": 0.013 }, "health": { "limit": 62100, @@ -14,7 +14,7 @@ }, "nursing": { "limit": 62100, - "rate": 0.01525, - "extra": 0.0035 + "rate": 0.017, + "extra": 0.006 } } diff --git a/data/social_security/2025.json b/data/social_security/2025.json index 8167398..ce39668 100644 --- a/data/social_security/2025.json +++ b/data/social_security/2025.json @@ -1,12 +1,12 @@ { "year": 2025, "pension": { - "limit": 96000, + "limit": 96600, "rate": 0.093 }, "unemployment": { - "limit": 96000, - "rate": 0.012 + "limit": 96600, + "rate": 0.013 }, "health": { "limit": 66150, @@ -14,7 +14,7 @@ }, "nursing": { "limit": 66150, - "rate": 0.01525, - "extra": 0.0035 + "rate": 0.018, + "extra": 0.006 } } diff --git a/data/social_security/2026.json b/data/social_security/2026.json new file mode 100644 index 0000000..1f3829b --- /dev/null +++ b/data/social_security/2026.json @@ -0,0 +1,20 @@ +{ + "year": 2026, + "pension": { + "limit": 101400, + "rate": 0.093 + }, + "unemployment": { + "limit": 101400, + "rate": 0.013 + }, + "health": { + "limit": 69750, + "rate": 0.073 + }, + "nursing": { + "limit": 69750, + "rate": 0.018, + "extra": 0.006 + } +} diff --git a/data/soli/2025.json b/data/soli/2025.json index 7360072..6066170 100644 --- a/data/soli/2025.json +++ b/data/soli/2025.json @@ -1,6 +1,6 @@ { "year": 2025, - "start_taxable_income": 19450, + "start_taxable_income": 19950, "start_fraction": 0.119, "end_rate": 0.055 } diff --git a/data/soli/2026.json b/data/soli/2026.json new file mode 100644 index 0000000..b55e281 --- /dev/null +++ b/data/soli/2026.json @@ -0,0 +1,6 @@ +{ + "year": 2026, + "start_taxable_income": 20350, + "start_fraction": 0.119, + "end_rate": 0.055 +} diff --git a/data/tax_curves/2021.json b/data/tax_curves/2021.json index 335b55d..6412d87 100644 --- a/data/tax_curves/2021.json +++ b/data/tax_curves/2021.json @@ -17,7 +17,7 @@ "const": [208.85, 2397, 950.96] }, "3": { - "step": 274613, + "step": 274612, "rate": 0.45, "const": [9136.63, 17374.99] } diff --git a/data/tax_curves/2023.json b/data/tax_curves/2023.json index bf157c6..ef22129 100644 --- a/data/tax_curves/2023.json +++ b/data/tax_curves/2023.json @@ -2,24 +2,24 @@ "year": 2023, "brackets": { "0": { - "step": 10909, + "step": 10908, "rate": 0.14, "const": null }, "1": { "step": 15999, "rate": 0.2397, - "const": null + "const": [979.18, 1400] }, "2": { "step": 62809, "rate": 0.42, - "const": null + "const": [192.59, 2397, 966.53] }, "3": { "step": 277826, "rate": 0.45, - "const": null + "const": [9972.98, 18307.73] } } } diff --git a/data/tax_curves/2024.json b/data/tax_curves/2024.json index 1a42ca0..3745d34 100644 --- a/data/tax_curves/2024.json +++ b/data/tax_curves/2024.json @@ -2,24 +2,24 @@ "year": 2024, "brackets": { "0": { - "step": 11605, + "step": 11784, "rate": 0.14, "const": null }, "1": { "step": 17005, "rate": 0.2397, - "const": null + "const": [954.80, 1400] }, "2": { "step": 66760, "rate": 0.42, - "const": null + "const": [181.19, 2397, 991.21] }, "3": { "step": 277826, "rate": 0.45, - "const": null + "const": [10636.31, 18971.06] } } } diff --git a/data/tax_curves/2025.json b/data/tax_curves/2025.json index 2ee93b6..3df9991 100644 --- a/data/tax_curves/2025.json +++ b/data/tax_curves/2025.json @@ -2,24 +2,24 @@ "year": 2025, "brackets": { "0": { - "step": 12086, + "step": 12096, "rate": 0.14, "const": null }, "1": { - "step": 17430, + "step": 17443, "rate": 0.2397, - "const": null + "const": [932.30, 1400] }, "2": { - "step": 68430, + "step": 68480, "rate": 0.42, - "const": null + "const": [176.64, 2397, 1015.13] }, "3": { "step": 277826, "rate": 0.45, - "const": null + "const": [10911.92, 19246.67] } } } diff --git a/data/tax_curves/2026.json b/data/tax_curves/2026.json new file mode 100644 index 0000000..514c5b9 --- /dev/null +++ b/data/tax_curves/2026.json @@ -0,0 +1,25 @@ +{ + "year": 2026, + "brackets": { + "0": { + "step": 12348, + "rate": 0.14, + "const": null + }, + "1": { + "step": 17799, + "rate": 0.2397, + "const": null + }, + "2": { + "step": 69878, + "rate": 0.42, + "const": null + }, + "3": { + "step": 277826, + "rate": 0.45, + "const": null + } + } +} diff --git a/netto/config.py b/netto/config.py index 43dbcfd..3e8bce0 100644 --- a/netto/config.py +++ b/netto/config.py @@ -9,7 +9,7 @@ class TaxConfig: Parameters ---------- year : int - Tax year (2018-2025, default: 2022) + Tax year (2018-2026, default: 2025) has_children : bool Has children (affects nursing insurance) is_married : bool @@ -26,18 +26,18 @@ class TaxConfig: >>> TaxConfig(church_tax=0.0) """ - year: int = 2022 + year: int = 2025 has_children: bool = False is_married: bool = False - extra_health_insurance: float = 0.014 + extra_health_insurance: float = 0.025 church_tax: float = 0.09 def __post_init__(self): """Validate configuration values.""" if not isinstance(self.year, int): raise TypeError(f"year must be int, got {type(self.year)}") - if self.year < 2018 or self.year > 2025: - raise ValueError(f"year must be between 2018 and 2025, got {self.year}") + if self.year < 2018 or self.year > 2026: + raise ValueError(f"year must be between 2018 and 2026, got {self.year}") if not isinstance(self.has_children, bool): raise TypeError(f"has_children must be bool, got {type(self.has_children)}") if not isinstance(self.is_married, bool): diff --git a/netto/data_loader.py b/netto/data_loader.py index 476a855..afe43a9 100644 --- a/netto/data_loader.py +++ b/netto/data_loader.py @@ -129,8 +129,8 @@ def load_social_security(year: int) -> dict: Examples -------- - >>> ss = load_social_security(2022) - >>> ss['pension']['limit'] + >>> social_sec = load_social_security(2022) + >>> social_sec['pension']['limit'] 84600 """ file_path = DATA_DIR / "social_security" / f"{year}.json" @@ -226,7 +226,7 @@ def load_all_tax_curves() -> dict[int, dict[int, dict]]: Tax curves for all years """ tax_curves = {} - for year in range(2018, 2026): # 2018-2025 + for year in range(2018, 2027): # 2018-2026 try: tax_curves[year] = load_tax_curve(year) except FileNotFoundError: @@ -244,14 +244,14 @@ def load_all_social_security() -> dict[int, dict]: Social security data for all years """ social_security = {} - for year in range(2018, 2026): # 2018-2025 + for year in range(2018, 2027): # 2018-2026 try: social_security[year] = load_social_security(year) except (FileNotFoundError, NotImplementedError): pass - # Add NotImplementedError for 2026+ to maintain backward compatibility - social_security[2026] = NotImplementedError + # Add NotImplementedError for 2027+ to maintain backward compatibility + social_security[2027] = NotImplementedError return social_security @@ -266,7 +266,7 @@ def load_all_soli() -> dict[int, dict]: Soli data for all years """ soli_data = {} - for year in range(2018, 2026): # 2018-2025 + for year in range(2018, 2027): # 2018-2026 try: soli_data[year] = load_soli(year) except FileNotFoundError: @@ -284,7 +284,7 @@ def load_all_pension_factors() -> dict[int, float]: Pension correction factors for all years """ pension_factors = {} - for year in range(2018, 2026): # 2018-2025 + for year in range(2018, 2027): # 2018-2026 try: pension_factors[year] = load_pension_factor(year) except FileNotFoundError: diff --git a/test/test_config.py b/test/test_config.py index a31234e..bd7755f 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -6,10 +6,10 @@ def test_taxconfig_defaults(): """Test that TaxConfig uses correct default values""" config = TaxConfig() - assert config.year == 2022 + assert config.year == 2025 assert config.has_children is False assert config.is_married is False - assert config.extra_health_insurance == 0.014 + assert config.extra_health_insurance == 0.025 assert config.church_tax == 0.09 @@ -34,7 +34,7 @@ def test_taxconfig_validation_year_range(): with pytest.raises(ValueError): TaxConfig(year=2017) # Too early with pytest.raises(ValueError): - TaxConfig(year=2026) # Too late + TaxConfig(year=2027) # Too late def test_taxconfig_validation_negative_rates(): diff --git a/test/test_data_loader.py b/test/test_data_loader.py index 6d6fc1b..1a66ab0 100644 --- a/test/test_data_loader.py +++ b/test/test_data_loader.py @@ -130,16 +130,16 @@ def test_social_security_entry_invalid_limit(): def test_social_security_valid(): """Test creating a valid SocialSecurity""" - ss = SocialSecurity( + social_sec = SocialSecurity( year=2022, pension=SocialSecurityEntry(limit=84600, rate=0.093), unemployment=SocialSecurityEntry(limit=84600, rate=0.012), health=SocialSecurityEntry(limit=58050, rate=0.073, extra=0.007), nursing=SocialSecurityEntry(limit=58050, rate=0.01525, extra=0.0035), ) - assert ss.year == 2022 - assert ss.pension.limit == 84600 - assert ss.health.extra == 0.007 + assert social_sec.year == 2022 + assert social_sec.pension.limit == 84600 + assert social_sec.health.extra == 0.007 def test_soli_curve_valid(): @@ -171,7 +171,7 @@ def test_pension_factor_invalid_factor(): # Tests for individual load functions -@pytest.mark.parametrize("year", [2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025]) +@pytest.mark.parametrize("year", [2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026]) def test_load_tax_curve(year): """Test loading tax curve for available years""" curve = load_tax_curve(year) @@ -187,21 +187,21 @@ def test_load_tax_curve_missing_year(): load_tax_curve(2030) -@pytest.mark.parametrize("year", [2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025]) +@pytest.mark.parametrize("year", [2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026]) def test_load_social_security(year): """Test loading social security for available years""" - ss = load_social_security(year) - assert isinstance(ss, dict) - assert "pension" in ss - assert "unemployment" in ss - assert "health" in ss - assert "nursing" in ss + social_sec = load_social_security(year) + assert isinstance(social_sec, dict) + assert "pension" in social_sec + assert "unemployment" in social_sec + assert "health" in social_sec + assert "nursing" in social_sec def test_load_social_security_not_implemented(): - """Test that loading social security for 2026+ raises NotImplementedError""" + """Test that loading social security for 2027+ raises NotImplementedError""" with pytest.raises(NotImplementedError): - load_social_security(2026) + load_social_security(2027) def test_load_social_security_missing_year(): @@ -210,7 +210,7 @@ def test_load_social_security_missing_year(): load_social_security(2015) -@pytest.mark.parametrize("year", [2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025]) +@pytest.mark.parametrize("year", [2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026]) def test_load_soli(year): """Test loading solidarity tax data for available years""" soli = load_soli(year) @@ -226,7 +226,7 @@ def test_load_soli_missing_year(): load_soli(2030) -@pytest.mark.parametrize("year", [2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025]) +@pytest.mark.parametrize("year", [2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026]) def test_load_pension_factor(year): """Test loading pension factor for available years""" factor = load_pension_factor(year) @@ -256,12 +256,12 @@ def test_load_all_tax_curves(): def test_load_all_social_security(): """Test loading all social security data""" - ss_data = load_all_social_security() - assert isinstance(ss_data, dict) - assert len(ss_data) >= 8 # At least 2018-2025 - # Check for 2026 NotImplementedError marker - assert 2026 in ss_data - assert ss_data[2026] is NotImplementedError + social_security_data = load_all_social_security() + assert isinstance(social_security_data, dict) + assert len(social_security_data) >= 9 # At least 2018-2026 + # Check for 2027 NotImplementedError marker + assert 2027 in social_security_data + assert social_security_data[2027] is NotImplementedError def test_load_all_soli(): diff --git a/test/test_main.py b/test/test_main.py index d7853ce..abd060b 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -10,13 +10,17 @@ @pytest.fixture def default_config(): """Fixture providing default config for tests""" - return TaxConfig(extra_health_insurance=0.014, church_tax=0.09, has_children=False) + return TaxConfig( + year=2022, extra_health_insurance=0.014, church_tax=0.09, has_children=False + ) @pytest.fixture def alternate_config(): """Fixture providing alternate config for tests""" - return TaxConfig(extra_health_insurance=0.015, church_tax=0.0, has_children=True) + return TaxConfig( + year=2022, extra_health_insurance=0.015, church_tax=0.0, has_children=True + ) @pytest.mark.parametrize( @@ -99,7 +103,8 @@ def test_calc_netto_with_default_none_config(): def test_calc_inverse_netto_with_default_none_config(): """Test that calc_inverse_netto works when config=None (uses default TaxConfig)""" # Use a value that we know works well with Newton's method - result = main.calc_inverse_netto(30000) + # Use explicit year=2022 for stable test behavior + result = main.calc_inverse_netto(30000, config=TaxConfig(year=2022)) # Should use default TaxConfig assert isinstance(result, int | float) assert result > 0 diff --git a/test/test_social_security.py b/test/test_social_security.py index 1dd49c3..a2378e5 100644 --- a/test/test_social_security.py +++ b/test/test_social_security.py @@ -7,7 +7,9 @@ @pytest.fixture def default_config(): """Fixture providing default config for tests""" - return TaxConfig(extra_health_insurance=0.014, church_tax=0.09, has_children=False) + return TaxConfig( + year=2022, extra_health_insurance=0.014, church_tax=0.09, has_children=False + ) @pytest.mark.parametrize( @@ -106,7 +108,7 @@ def test_sameness_of_calc_social_security_different_config(salary): result_integration = social_security.calc_social_security_by_integration( salary, config ) - assert result_direct == result_integration + assert abs(result_direct - result_integration) < 0.02 @pytest.mark.parametrize( @@ -121,7 +123,7 @@ def test_sameness_of_calc_social_security_different_config(salary): ) def test_get_rate_health_different_config(salary, expected): """Test health rate with different extra health insurance""" - config = TaxConfig(extra_health_insurance=0.015) + config = TaxConfig(year=2022, extra_health_insurance=0.015) result = social_security.get_rate_health(salary, config) assert abs(result - expected) < 0.0001 @@ -138,7 +140,7 @@ def test_get_rate_health_different_config(salary, expected): ) def test_get_rate_nursing_no_children(salary, expected): """Test nursing rate without children (includes extra rate)""" - config = TaxConfig(has_children=False) + config = TaxConfig(year=2022, has_children=False) result = social_security.get_rate_nursing(salary, config) assert result == expected @@ -155,7 +157,7 @@ def test_get_rate_nursing_no_children(salary, expected): ) def test_get_rate_nursing_with_children(salary, expected): """Test nursing rate with children (no extra rate)""" - config = TaxConfig(has_children=True) + config = TaxConfig(year=2022, has_children=True) result = social_security.get_rate_nursing(salary, config) assert result == expected diff --git a/test/test_taxes_income.py b/test/test_taxes_income.py index cff960a..be1bfc8 100644 --- a/test/test_taxes_income.py +++ b/test/test_taxes_income.py @@ -7,7 +7,9 @@ @pytest.fixture def default_config(): """Fixture providing default config for tests""" - return TaxConfig(extra_health_insurance=0.014, church_tax=0.09, has_children=False) + return TaxConfig( + year=2022, extra_health_insurance=0.014, church_tax=0.09, has_children=False + ) @pytest.mark.parametrize( @@ -42,7 +44,7 @@ def test_get_marginal_tax_rate(taxable_income, expected_rate, default_config): ) def test_get_marginal_tax_rate_married(taxable_income, expected_rate): """Test marginal tax rate for married couples (doubled brackets)""" - config = TaxConfig(is_married=True) + config = TaxConfig(year=2022, is_married=True) result = taxes_income.get_marginal_tax_rate(taxable_income, config) assert result == expected_rate @@ -83,7 +85,9 @@ def test_get_marginal_tax_rate_with_default_none_config(): def test_calc_income_tax_with_default_none_config(): """Test that calc_income_tax works when config=None""" - result = taxes_income.calc_income_tax(50000) + # Use explicit year=2022 since calc_income_tax requires const values + # which are not available for 2023-2025 + result = taxes_income.calc_income_tax(50000, config=TaxConfig(year=2022)) assert isinstance(result, float) assert result >= 0 diff --git a/test/test_taxes_other.py b/test/test_taxes_other.py index d34d51f..7eab225 100644 --- a/test/test_taxes_other.py +++ b/test/test_taxes_other.py @@ -7,7 +7,9 @@ @pytest.fixture def default_config(): """Fixture providing default config for tests""" - return TaxConfig(extra_health_insurance=0.014, church_tax=0.09, has_children=False) + return TaxConfig( + year=2022, extra_health_insurance=0.014, church_tax=0.09, has_children=False + ) @pytest.mark.parametrize(