diff --git a/.github/workflows/build-and-tests.yaml b/.github/workflows/build-and-tests.yaml new file mode 100644 index 0000000..dc47c59 --- /dev/null +++ b/.github/workflows/build-and-tests.yaml @@ -0,0 +1,19 @@ +name: Build & Test +run-name: "" +on: + push: + pull_request: + +env: + CARGO_TERM_COLOR: always + +jobs: + Build-and-Test: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v4 + - name: Build the workspace + run: cargo build --verbose + - name: Test the workspace + run: cargo test --verbose diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e8a5904 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,242 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" + +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "clap" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + +[[package]] +name = "net-income-germany" +version = "0.1.0" + +[[package]] +name = "net-income-germany-cmd" +version = "0.1.0" +dependencies = [ + "clap", + "net-income-germany", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..779e860 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[workspace] +resolver = "2" +members = [ + "net_income_germany", + "net_income_germany_cmd", +] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e8e183a --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# Net Income (Germany) +[![Build & Test](https://github.com/awinterstein/net-income-germany/actions/workflows/build-and-tests.yaml/badge.svg)](https://github.com/awinterstein/net-income-germany/actions/workflows/build-and-tests.yaml) + +See documentation for the net income module in the sudirectory [net_income_germany](net_income_germany/). diff --git a/net_income_germany/Cargo.lock b/net_income_germany/Cargo.lock new file mode 100644 index 0000000..5c6a21f --- /dev/null +++ b/net_income_germany/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "net-income-germany" +version = "0.1.0" diff --git a/net_income_germany/Cargo.toml b/net_income_germany/Cargo.toml new file mode 100644 index 0000000..dd86950 --- /dev/null +++ b/net_income_germany/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "net-income-germany" +description = "Net income calculation for a given gross income based on the German social security and income tax rules." +publish = ["cerritus"] +license = "MPL-2.0" +version = "0.1.0" +edition = "2021" +authors = ["Adrian Winterstein "] +repository = "https://deposito.cerritus.eu/freelancing/net-income-germany" diff --git a/net_income_germany/README.md b/net_income_germany/README.md new file mode 100644 index 0000000..0fa754e --- /dev/null +++ b/net_income_germany/README.md @@ -0,0 +1,27 @@ +# net-income-germany + +Calculates the net income from a given yearly gross income. + +## Example +```rust +// set the necessary input data values +let tax_data = net_income_germany::TaxData { + gross_income: 80000, // the gross income of one year + expenses: 5300, // the tax-deductible expenses of one year + fixed_retirement: Some(800), // an optional fixed monthly retirement rate (otherwise percentage applies) + self_employed: false, // whether social security taxes should be calculated for a self-employed person + married: false, // whether tax splitting due to marriage should apply +}; + +// create the default configuration for a specific year +let config = net_income_germany::config::create(2025)?; + +// do the tax calculation based on the input data values +let tax_result = net_income_germany::calculate(&config, &tax_data)?; + +// access the results (in this example just the resulting net income) +println!("Net income: {}", tax_result.net_income); + +``` + +License: MPL-2.0 diff --git a/net_income_germany/src/config.rs b/net_income_germany/src/config.rs new file mode 100644 index 0000000..03c83f4 --- /dev/null +++ b/net_income_germany/src/config.rs @@ -0,0 +1,190 @@ +//! Tax and social security configurations (e.g, the rates to apply on the income) per year. +//! +//! There are configurations available based on the German laws for the years 2024 and 2025. + +// values for the social security (health and retirement) can be found on the website of the health ministry: +// https://www.bundesgesundheitsministerium.de/beitraege + +/// Configuration for the state-operated health insurance used as part of the social security calculations. +#[derive(Debug)] +pub struct HealthInsuranceConfig { + pub premium_general: f32, + pub premium_general_reduced: f32, + pub premium_additional: f32, + pub premium_nursing: f32, + pub premium_nursing_additional: f32, + pub min_income: f32, // minimum income that is used for the health insurance calculation (only self-employed) + pub max_income: f32, // maximum income that is used for the health insurance calculation +} + +/// Configuration for the state-operated retirement insurance used as part of the social security calculations. +#[derive(Debug)] +pub struct RetirementInsuranceConfig { + pub premium: f32, + pub max_income: f32, +} + +#[derive(Debug)] +pub struct UnemploymentInsuranceConfig { + pub premium: f32, + pub max_income: f32, +} + +#[derive(Debug, Clone)] +pub struct TaxRange { + pub lower_limit: u32, + pub upper_limit: u32, + pub rate_min: f32, + pub rate_max: f32, +} + +#[derive(Debug)] +pub struct SolidaryAdditionConfig { + pub exemption_level: u32, + pub rate: f32, + pub max_percentage: f32, +} + +#[derive(Debug)] +pub struct IncomeTaxConfig { + pub tax_ranges: Vec, + pub solidary_addition_config: SolidaryAdditionConfig, +} + +/// Main configuration struct that contains all the needed tax and social security configurations. +#[derive(Debug)] +pub struct Config { + pub health_insurance: HealthInsuranceConfig, + pub retirement_insurance: RetirementInsuranceConfig, + pub unemployment_insurance: UnemploymentInsuranceConfig, + pub income_tax: IncomeTaxConfig, +} + +impl Default for Config { + fn default() -> Self { + return create(2025).unwrap(); + } +} + +/// Creates the configuration for the given year. +pub fn create(year: u32) -> Result { + match year { + 2025 => Ok(Config { + retirement_insurance: RetirementInsuranceConfig { + premium: 0.186, + max_income: 8050.0, + }, + health_insurance: HealthInsuranceConfig { + premium_general: 0.146, + premium_general_reduced: 0.14, + premium_additional: 0.0245, + premium_nursing: 0.036, + premium_nursing_additional: 0.006, + min_income: 1248.32, + max_income: 5512.5, + }, + unemployment_insurance: UnemploymentInsuranceConfig { + premium: 0.026, + max_income: 8050.0, + }, + income_tax: IncomeTaxConfig { + tax_ranges: vec![ + TaxRange { + lower_limit: 0, + upper_limit: 12096, + rate_min: 0.00, + rate_max: 0.00, + }, + TaxRange { + lower_limit: 12097, + upper_limit: 17444, + rate_min: 0.14, + rate_max: 0.2397, + }, + TaxRange { + lower_limit: 17445, + upper_limit: 68481, + rate_min: 0.2397, + rate_max: 0.42, + }, + TaxRange { + lower_limit: 68481, + upper_limit: 277826, + rate_min: 0.42, + rate_max: 0.42, + }, + TaxRange { + lower_limit: 277826, + upper_limit: u32::MAX, + rate_min: 0.45, + rate_max: 0.45, + }, + ], + solidary_addition_config: SolidaryAdditionConfig { + exemption_level: 19950, + rate: 0.055, + max_percentage: 0.119, + }, + }, + }), + 2024 => Ok(Config { + retirement_insurance: RetirementInsuranceConfig { + premium: 0.186, + max_income: 7550.0, + }, + health_insurance: HealthInsuranceConfig { + premium_general: 0.146, + premium_general_reduced: 0.14, + premium_additional: 0.012, + premium_nursing: 0.034, + premium_nursing_additional: 0.006, + min_income: 1178.33, + max_income: 5175.0, + }, + unemployment_insurance: UnemploymentInsuranceConfig { + premium: 0.026, + max_income: 7550.0, + }, + income_tax: IncomeTaxConfig { + tax_ranges: vec![ + TaxRange { + lower_limit: 0, + upper_limit: 11784, + rate_min: 0.00, + rate_max: 0.00, + }, + TaxRange { + lower_limit: 11784, + upper_limit: 17005, + rate_min: 0.14, + rate_max: 0.2397, + }, + TaxRange { + lower_limit: 17005, + upper_limit: 66760, + rate_min: 0.2397, + rate_max: 0.42, + }, + TaxRange { + lower_limit: 66760, + upper_limit: 277825, + rate_min: 0.42, + rate_max: 0.42, + }, + TaxRange { + lower_limit: 277825, + upper_limit: u32::MAX, + rate_min: 0.45, + rate_max: 0.45, + }, + ], + solidary_addition_config: SolidaryAdditionConfig { + exemption_level: 18130, + rate: 0.055, + max_percentage: 0.119, + }, + }, + }), + _ => Err("No configuration available for given year."), + } +} diff --git a/net_income_germany/src/income_tax.rs b/net_income_germany/src/income_tax.rs new file mode 100644 index 0000000..960af33 --- /dev/null +++ b/net_income_germany/src/income_tax.rs @@ -0,0 +1,141 @@ +use crate::config::{IncomeTaxConfig, SolidaryAdditionConfig, TaxRange}; + +impl TaxRange { + /// Calculate the range from the upper and lower limit. + pub fn range(&self) -> u32 { + self.upper_limit - self.lower_limit + } +} + +pub fn calculate(config: &IncomeTaxConfig, taxable_income: u32, together: bool) -> u32 { + let tax = calculate_income_tax(&config, taxable_income, together); + let tax_solidarity = + calculate_solidarity_addition(tax, together, &config.solidary_addition_config); + + return tax + tax_solidarity; +} + +fn deduct_tax_for_one_range(income: u32, tax_range: &TaxRange) -> f32 { + // income so small, that this tax range does not apply + if income <= tax_range.lower_limit { + return 0.0; + } + + // remove the lower limit from the income (as everything below is taxed in lower ranges) + // and make sure that not more than the current tax range of the income is considered + let taxed_income = (income - tax_range.lower_limit).min(tax_range.range()); + + let income_range = tax_range.range() as f32; + let taxed_income = taxed_income as f32; + + let rate_diff = tax_range.rate_max - tax_range.rate_min; + let effective_rate_diff = taxed_income / income_range * rate_diff; + + let effective_rate = tax_range.rate_min + effective_rate_diff / 2.0; + + return taxed_income * effective_rate; +} + +fn calculate_income_tax(config: &IncomeTaxConfig, income: u32, together: bool) -> u32 { + let mut tax_sum = 0.0; + + // for married couples the taxes are calculated based on half of the combined income + let income = if together { income / 2 } else { income }; + + for tax_range in &config.tax_ranges { + let tax = deduct_tax_for_one_range(income, tax_range); + + tax_sum = tax_sum + tax; + } + + if together { + // the tax value needs to be doubled again after calculating with half for married couples + return tax_sum as u32 * 2; + } else { + return tax_sum as u32; + } +} + +fn calculate_solidarity_addition( + tax: u32, + together: bool, + solidarity_addition_config: &SolidaryAdditionConfig, +) -> u32 { + let tax_exemption_level = if together { + solidarity_addition_config.exemption_level * 2 + } else { + solidarity_addition_config.exemption_level + }; + + if tax < tax_exemption_level { + return 0; + } + + let max_solidarity_addition = + (tax - tax_exemption_level) as f32 * solidarity_addition_config.max_percentage; + let solidarity_addition = tax as f32 * solidarity_addition_config.rate; + + return solidarity_addition.min(max_solidarity_addition) as u32; +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::create as create_config; + + struct Data { + i: u32, + o: u32, + } + + #[test] + fn test_tax_calculation_2024() { + // the test data is based on the income tax calculator of the + // German ministry of finances (https://www.bmf-steuerrechner.de) + let test_data = vec![ + Data { i: 11791, o: 0 }, + Data { i: 11792, o: 1 }, + Data { i: 17008, o: 991 }, + Data { i: 18000, o: 1231 }, + Data { i: 46231, o: 9544 }, + Data { i: 66760, o: 17402 }, + Data { + i: 277825, + o: 111882, + }, + ]; + + test_tax_calculation(&test_data, 2024, false); + } + + #[test] + fn test_tax_calculation_married_2024() { + // the test data is based on the income tax calculator of the + // German ministry of finances (https://www.bmf-steuerrechner.de) + let test_data = vec![ + Data { i: 23583, o: 0 }, + Data { i: 23584, o: 2 }, + Data { i: 50000, o: 6046 }, + Data { i: 66760, o: 10804 }, + Data { + i: 277825, + o: 100659, + }, + Data { + i: 555650, + o: 223765, + }, + ]; + + test_tax_calculation(&test_data, 2024, true); + } + + fn test_tax_calculation(test_data: &Vec, year: u32, together: bool) { + let config = create_config(year).unwrap(); + + for data in test_data { + let result = calculate(&config.income_tax, data.i, together); + assert_eq!(result, data.o); + } + } +} diff --git a/net_income_germany/src/lib.rs b/net_income_germany/src/lib.rs new file mode 100644 index 0000000..8607663 --- /dev/null +++ b/net_income_germany/src/lib.rs @@ -0,0 +1,100 @@ +//! Calculates the net income from a given yearly gross income. +//! +//! # Example +//! ``` +//! # fn main() -> Result<(), &'static str> { +//! // set the necessary input data values +//! let tax_data = net_income_germany::TaxData { +//! gross_income: 80000, // the gross income of one year +//! expenses: 5300, // the tax-deductible expenses of one year +//! fixed_retirement: Some(800), // an optional fixed monthly retirement rate (otherwise percentage applies) +//! self_employed: false, // whether social security taxes should be calculated for a self-employed person +//! married: false, // whether tax splitting due to marriage should apply +//! }; +//! +//! // create the default configuration for a specific year +//! let config = net_income_germany::config::create(2025)?; +//! +//! // do the tax calculation based on the input data values +//! let tax_result = net_income_germany::calculate(&config, &tax_data)?; +//! +//! // access the results (in this example just the resulting net income) +//! println!("Net income: {}", tax_result.net_income); +//! +//! # Ok(()) +//! # } +//! ``` + +pub mod config; +mod income_tax; +mod social_security; + +/// Input data struct for the tax calculation. +pub struct TaxData { + /// The gross income of one year. + pub gross_income: u32, + + /// The expenses of one year that will be deducted from the gross income, before calculating the income taxes. + pub expenses: u32, + + /// Optional value of a fixed monthly retirement insurance rate. If this is set, then this rate is used for every + /// month. Otherwise, the retirement insurance rate is calculated by a percentage of the income. + pub fixed_retirement: Option, + + /// Whether the calculations should be done for a self-employed person. + pub self_employed: bool, + + /// Whether the income should be split for two people according to tax law. + pub married: bool, +} + +/// Result struct of the tax calculation. +pub struct TaxResult { + /// The net income after deducting social security taxes and income taxes. + pub net_income: i32, + + /// The social security taxes that were deducted from the gross income. + pub social_security_taxes: u32, + + /// The income taxes that were deducted from the gross income. + pub income_taxes: u32, +} + +impl TaxResult { + /// Returns how much of the gross income was spent on social security and income taxes. + pub fn get_tax_ratio(&self) -> f32 { + let taxes = (self.social_security_taxes + self.income_taxes) as f32; + return taxes / (self.net_income as f32 + taxes); + } +} + +/// Calculates social security taxes and income taxes based on the given income. +/// +/// Returns the remaining net income and the calculated social security taxes and income taxes. +pub fn calculate(config: &config::Config, tax_data: &TaxData) -> Result { + // calculate the social security taxes + let social_security = social_security::calculate( + &config.health_insurance, + &config.retirement_insurance, + &config.unemployment_insurance, + &tax_data, + )?; + + // reduce income by social security taxes and calculate income taxes on this + let taxable_income = + tax_data.gross_income as i32 - social_security as i32 - tax_data.expenses as i32; + let taxes = income_tax::calculate( + &config.income_tax, + taxable_income.max(0) as u32, + tax_data.married, + ); + + // store the results in the result struct + let tax_result = TaxResult { + net_income: taxable_income - taxes as i32, + social_security_taxes: social_security as u32, + income_taxes: taxes, + }; + + return Ok(tax_result); +} diff --git a/net_income_germany/src/social_security.rs b/net_income_germany/src/social_security.rs new file mode 100644 index 0000000..1039df0 --- /dev/null +++ b/net_income_germany/src/social_security.rs @@ -0,0 +1,163 @@ +use crate::config::{ + HealthInsuranceConfig, RetirementInsuranceConfig, UnemploymentInsuranceConfig, +}; +use crate::TaxData; + +/// Calculate the social security payment from the given health and retirement insurance configuration and the tax data (yearly income). +pub fn calculate( + health_insurance_config: &HealthInsuranceConfig, + retirement_insurance_config: &RetirementInsuranceConfig, + unemployment_insurance_config: &UnemploymentInsuranceConfig, + tax_data: &TaxData, +) -> Result { + // for self-employed persons there is a minimum income that needs to be + // used for the health insurance calculations in case that the actual + // income is lower + let income_for_health_insurance = match tax_data.self_employed { + true => { + let min_income_year = health_insurance_config.min_income * 12.0; + tax_data.gross_income.max(min_income_year as u32) + } + false => tax_data.gross_income, + }; + + // calculate health insurance based on the given gross income (limited by the maximum configured income value) + let health_insurance = calculate_social_insurance( + income_for_health_insurance, + calculate_health_insurance_premium(health_insurance_config, tax_data), + health_insurance_config.max_income, + ); + + // calculate retirement insurance either from a given fixed value or as percentage from income + let retirement_insurance = match tax_data.fixed_retirement { + Some(fixed_retirement) => (fixed_retirement * 12) as f32, + None => calculate_social_insurance( + tax_data.gross_income, + calculate_retirement_insurance_premium(retirement_insurance_config, tax_data), + retirement_insurance_config.max_income, + ), + }; + + let unemployment_insurance = match tax_data.self_employed { + true => 0.0, + false => calculate_social_insurance( + tax_data.gross_income, + unemployment_insurance_config.premium / 2.0, + unemployment_insurance_config.max_income, + ), + }; + + println!( + "{}, {}, {}", + health_insurance, retirement_insurance, unemployment_insurance + ); + + return Ok((health_insurance + retirement_insurance + unemployment_insurance) as u32); +} + +/// Calculate the social security payment (for one insurance) based on the given yearly income and premium percentage. +/// +/// The premium is limited by the maximum monthly income value to be considered for the calculation. +fn calculate_social_insurance( + yearly_income: u32, // the yearly income on which the social security payment is calculated + premium_percentage: f32, // how much of the income needs to be payed for the insurance + max_monthly_value: f32, // the maximum monthly income that is considered for the premium (monthly upper income limit) +) -> f32 { + let effective_income = (yearly_income as f32).min(max_monthly_value * 12.0); + return effective_income * premium_percentage; +} + +fn calculate_health_insurance_premium( + health_insurance_config: &HealthInsuranceConfig, + tax_data: &TaxData, +) -> f32 { + if tax_data.self_employed { + return health_insurance_config.premium_general_reduced + + health_insurance_config.premium_additional + + health_insurance_config.premium_nursing + + health_insurance_config.premium_nursing_additional; + } else { + return (health_insurance_config.premium_general + + health_insurance_config.premium_additional + + health_insurance_config.premium_nursing) + / 2.0 + + health_insurance_config.premium_nursing_additional; + } +} + +fn calculate_retirement_insurance_premium( + retirement_insurance_config: &RetirementInsuranceConfig, + tax_data: &TaxData, +) -> f32 { + if tax_data.self_employed { + return retirement_insurance_config.premium; + } else { + // the employer is paying half of the premium for an employee + return retirement_insurance_config.premium / 2.0; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::create as create_config; + + struct Data { + i: u32, + o: u32, + } + + #[test] + fn test_social_security_calculation_2024() { + let test_data = vec![ + Data { i: 12000, o: 2496 }, + Data { i: 25132, o: 5227 }, + Data { i: 62100, o: 12916 }, + Data { i: 90600, o: 15937 }, + Data { i: 99999, o: 15937 }, + ]; + + test_social_security(&test_data, 2024, false, None); + } + + #[test] + fn test_social_security_calculation_self_employed_2025() { + let test_data = vec![ + Data { i: 12000, o: 3093 }, + Data { i: 25128, o: 5188 }, + Data { i: 62100, o: 12823 }, + Data { i: 66150, o: 13659 }, + Data { i: 99999, o: 13659 }, + ]; + + test_social_security(&test_data, 2025, true, Some(0)); + } + + fn test_social_security( + test_data: &Vec, + year: u32, + self_employed: bool, + fixed_retirement: Option, + ) { + let config = create_config(year).unwrap(); + + for data in test_data { + let tax_data = TaxData { + gross_income: data.i, + expenses: 0, + fixed_retirement: fixed_retirement, + self_employed: self_employed, + married: false, + }; + + let result = calculate( + &config.health_insurance, + &config.retirement_insurance, + &config.unemployment_insurance, + &tax_data, + ) + .unwrap(); + assert_eq!(result, data.o); + } + } +} diff --git a/net_income_germany_cmd/Cargo.lock b/net_income_germany_cmd/Cargo.lock new file mode 100644 index 0000000..d3f3cb3 --- /dev/null +++ b/net_income_germany_cmd/Cargo.lock @@ -0,0 +1,237 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" + +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "clap" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + +[[package]] +name = "net-income-germany-cmd" +version = "0.1.0" +dependencies = [ + "clap", +] + +[[package]] +name = "proc-macro2" +version = "1.0.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" diff --git a/net_income_germany_cmd/Cargo.toml b/net_income_germany_cmd/Cargo.toml new file mode 100644 index 0000000..7580bf6 --- /dev/null +++ b/net_income_germany_cmd/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "net-income-germany-cmd" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4.5.4", features = ["deprecated", "derive"] } +net-income-germany = { path = "../net_income_germany" } diff --git a/net_income_germany_cmd/src/main.rs b/net_income_germany_cmd/src/main.rs new file mode 100644 index 0000000..5080154 --- /dev/null +++ b/net_income_germany_cmd/src/main.rs @@ -0,0 +1,74 @@ +//! Calculates and displays taxes and social insurances for a given income. +//! +//! The minimal command line call needs just the gross income of the year: +//! ``` +//! $ net-income-germany-cmd --income 80000 +//! ``` + +use clap::Parser; +use std::process; + +/// Command line arguments of the application. +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// Annual income before taxes, social security and tax-deductible expenses + #[arg(short, long)] + income: u32, + + /// Tax-deductible expenses + #[arg(short, long, default_value_t = 0)] + expenses: u32, + + /// Fixed retirement rate (percentage will be calculated if not set) + #[arg(short, long)] + fixed_retirement: Option, + + /// Calculate social security and income taxes for a self-employed person + #[arg(short, long)] + self_employed: bool, + + /// Calculate with tax splitting for a married couple + #[arg(short, long)] + married: bool, + + /// For which year the taxes should be calculated + #[arg(short, long, default_value_t = 2025)] + year: u32, +} + +/// Parses command line arguments, calls the net-income-germany crate then for +/// calculation of the taxes and social security premiums and prints the result +/// to the standard output. +fn main() { + let args = Args::parse(); + + let tax_data = net_income_germany::TaxData { + gross_income: args.income, + expenses: args.expenses, + fixed_retirement: args.fixed_retirement, + self_employed: args.self_employed, + married: args.married, + }; + + // create the tax configuration for the given year + let config: net_income_germany::config::Config = net_income_germany::config::create(args.year) + .unwrap_or_else(|err| { + eprintln!("Failed to calculate the taxes: {err}"); + process::exit(1); + }); + + // calculate the taxes with the configuration and the given tax data + let tax_result = net_income_germany::calculate(&config, &tax_data).unwrap_or_else(|err| { + eprintln!("Failed to calculate the taxes: {err}"); + process::exit(1); + }); + + println!( + "Net income: {}, social security taxes: {}, income taxes: {}, net ratio: {}", + tax_result.net_income, + tax_result.social_security_taxes, + tax_result.income_taxes, + 1.0 - tax_result.get_tax_ratio() + ) +}