diff --git a/src/de.rs b/src/de.rs index 2904a1a..a5eb716 100644 --- a/src/de.rs +++ b/src/de.rs @@ -6,6 +6,9 @@ use regex::Regex; pub struct DE {} impl etradeTaxReturnHelper::Residency for DE { + fn get_tax_deduction_thresholds(&self) -> Box f32> { + todo!("Thresholds for Germany of deductabile tax not implemented!"); + } fn get_exchange_rates( &self, dates: &mut std::collections::HashMap< @@ -59,6 +62,7 @@ impl etradeTaxReturnHelper::Residency for DE { &self, gross_div: f32, tax_div: f32, + demonstratable_tax_div: f32, gross_sold: f32, cost_sold: f32, ) -> (Vec, Option) { @@ -85,6 +89,7 @@ mod tests { let gross_div = 100.0f32; let tax_div = 15.0f32; + let demonstratable_tax_div = 15.0f32; let gross_sold = 1000.0f32; let cost_sold = 10.0f32; @@ -95,7 +100,13 @@ mod tests { "===> (SOLD STOCK) TAX DEDUCTIBLE COST: 10.00 EUR".to_string(), ]; - let (results, _) = rd.present_result(gross_div, tax_div, gross_sold, cost_sold); + let (results, _) = rd.present_result( + gross_div, + tax_div, + demonstratable_tax_div, + gross_sold, + cost_sold, + ); results .iter() diff --git a/src/gui.rs b/src/gui.rs index b94ce41..0ce0bdb 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -122,6 +122,7 @@ fn create_execute_documents( let etradeTaxReturnHelper::TaxCalculationResult { gross_income: gross_div, tax: tax_div, + demonstratable_tax, gross_sold, cost_sold, interests: interests_transactions, @@ -139,7 +140,7 @@ fn create_execute_documents( panic!("Error: unable to perform taxation"); } }; - let (presentation,warning) = rd.present_result(gross_div, tax_div, gross_sold, cost_sold); + let (presentation,warning) = rd.present_result(gross_div, tax_div, demonstratable_tax, gross_sold, cost_sold); buffer.set_text(&presentation.join("\n")); if let Some(warn_msg) = warning { nbuffer.set_text(&warn_msg); diff --git a/src/lib.rs b/src/lib.rs index b3e9385..06e956a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -143,10 +143,12 @@ impl SoldTransaction { } pub trait Residency { + fn get_tax_deduction_thresholds(&self) -> Box f32>; fn present_result( &self, gross_div: f32, tax_div: f32, + demonstratable_tax_div: f32, gross_sold: f32, cost_sold: f32, ) -> (Vec, Option); @@ -265,6 +267,7 @@ pub trait Residency { pub struct TaxCalculationResult { pub gross_income: f32, pub tax: f32, + pub demonstratable_tax: f32, pub gross_sold: f32, pub cost_sold: f32, pub interests: Vec, @@ -295,8 +298,10 @@ fn create_client() -> reqwest::blocking::Client { let client = client.build().expect_and_log("Could not create client"); client } - -fn compute_div_taxation(transactions: &Vec) -> (f32, f32) { +fn compute_div_taxation( + transactions: &[Transaction], + threshold: Option<&dyn Fn(&str) -> f32>, +) -> (f32, f32, f32) { // Gross income from dividends in target currency (PLN, EUR etc.) let gross_us_pl: f32 = transactions .iter() @@ -307,7 +312,26 @@ fn compute_div_taxation(transactions: &Vec) -> (f32, f32) { .iter() .map(|x| x.exchange_rate * x.tax_paid.value() as f32) .sum(); - (gross_us_pl, tax_us_pl) + let demonstratable_tax_pl = if let Some(threshold_func) = threshold { + // Tax paid in US in PLN that can fit into allowed threshold + transactions + .iter() + .map(|x| { + if let Some(country_code) = &x.country { + let max_tax_to_be_deducted = threshold_func(country_code.as_ref() as &str); + // Get lower out of two values e.g. actually paid tax vs + // maximum of tax that can be indicated based on double tax avoidance agreement + (x.exchange_rate * x.tax_paid.value() as f32) + .min(max_tax_to_be_deducted * x.gross.value() as f32 * x.exchange_rate) + } else { + x.exchange_rate * x.tax_paid.value() as f32 + } + }) + .sum() + } else { + tax_us_pl + }; + (gross_us_pl, tax_us_pl, demonstratable_tax_pl) } fn compute_sold_taxation(transactions: &Vec) -> (f32, f32) { @@ -553,14 +577,21 @@ pub fn run_taxation( ReportMode::None => (), } - let (gross_interests, _) = compute_div_taxation(&interests); - let (gross_div, tax_div) = compute_div_taxation(&transactions); + let tax_deduction_thresholds = rd.get_tax_deduction_thresholds(); + + let (gross_interests, _, _) = compute_div_taxation(&interests, None); + let (gross_div, tax_div, demonstratable_tax) = + compute_div_taxation(&transactions, Some(&tax_deduction_thresholds)); let (gross_sold, cost_sold) = compute_sold_taxation(&sold_transactions); - let (gross_revolut, tax_revolut) = compute_div_taxation(&revolut_dividends_transactions); + let (gross_revolut, tax_revolut, demonstratable_tax_revolut) = compute_div_taxation( + &revolut_dividends_transactions, + Some(&tax_deduction_thresholds), + ); let (gross_revolut_sold, cost_revolut_sold) = compute_sold_taxation(&revolut_sold_transactions); Ok(TaxCalculationResult { gross_income: gross_interests + gross_div + gross_revolut, tax: tax_div + tax_revolut, + demonstratable_tax: demonstratable_tax + demonstratable_tax_revolut, gross_sold: gross_sold + gross_revolut_sold, cost_sold: cost_sold + cost_revolut_sold, interests, @@ -647,7 +678,13 @@ mod tests { company: Some("INTEL CORP".to_owned()), country: Some("US".to_string()), }]; - assert_eq!(compute_div_taxation(&transactions), (400.0, 100.0)); + + let threshold = Box::new(|_: &str| -> f32 { 0.15 }); + + assert_eq!( + compute_div_taxation(&transactions, Some(&threshold)), + (400.0, 100.0, 0.15 * 400.0) + ); Ok(()) } @@ -666,17 +703,24 @@ mod tests { }, Transaction { transaction_date: "N/A".to_string(), - gross: crate::Currency::USD(126.0), - tax_paid: crate::Currency::USD(10.0), + gross: crate::Currency::USD(100.0), + tax_paid: crate::Currency::USD(26.0), exchange_rate_date: "N/A".to_string(), exchange_rate: 3.5, - company: Some("INTEL CORP".to_owned()), - country: Some("US".to_string()), + company: Some("BMO".to_owned()), + country: Some("CA".to_string()), }, ]; + + let threshold = Box::new(|_: &str| -> f32 { 0.15 }); + assert_eq!( - compute_div_taxation(&transactions), - (400.0 + 126.0 * 3.5, 100.0 + 10.0 * 3.5) + compute_div_taxation(&transactions, Some(&threshold)), + ( + 400.0 + 100.0 * 3.5, + 100.0 + 26.0 * 3.5, + 0.15 * 400.0 + 0.15 * 100.0 * 3.5 + ) ); Ok(()) } @@ -703,8 +747,8 @@ mod tests { }, ]; assert_eq!( - compute_div_taxation(&transactions), - (0.44 * 1.0 + 0.45 * 1.0, 0.0) + compute_div_taxation(&transactions, None), + (0.44 * 1.0 + 0.45 * 1.0, 0.0, 0.0) ); Ok(()) } @@ -732,8 +776,8 @@ mod tests { }, ]; assert_eq!( - compute_div_taxation(&transactions), - (0.44 * 2.0 + 0.45 * 3.0, 0.0) + compute_div_taxation(&transactions, None), + (0.44 * 2.0 + 0.45 * 3.0, 0.0, 0.0) ); Ok(()) } diff --git a/src/main.rs b/src/main.rs index 6eac0ca..a9b288d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,7 @@ use etradeTaxReturnHelper::run_taxation; use etradeTaxReturnHelper::TaxCalculationResult; use logging::ResultExt; +// Make per-country mode to show sells and dividends separatly // TODO: check if Tax from Terna company taken by IT goverment was taken into account // TODO: Extend structure of TaxCalculationResult with country // TODO: Make parsing of PDF start from first page not second so then reproduction of problem @@ -126,6 +127,7 @@ fn main() { let TaxCalculationResult { gross_income: gross_div, tax: tax_div, + demonstratable_tax: demonstratable_tax, gross_sold, cost_sold, .. @@ -134,7 +136,13 @@ fn main() { Err(msg) => panic!("\nError: Unable to compute taxes. \n\nDetails: {msg}"), }; - let (presentation, warning) = rd.present_result(gross_div, tax_div, gross_sold, cost_sold); + let (presentation, warning) = rd.present_result( + gross_div, + tax_div, + demonstratable_tax, + gross_sold, + cost_sold, + ); presentation.iter().for_each(|x| println!("{x}")); if let Some(warn_msg) = warning { diff --git a/src/pl.rs b/src/pl.rs index 3fe19d9..1efb9ac 100644 --- a/src/pl.rs +++ b/src/pl.rs @@ -238,34 +238,54 @@ impl etradeTaxReturnHelper::Residency for PL { })?; Ok(()) } + fn get_tax_deduction_thresholds(&self) -> Box f32> { + Box::new(|code: &str| -> f32 { + match code { + "JE" => 0.19, + _ => 0.15, + } + }) + } fn present_result( &self, gross_div: f32, tax_div: f32, + demonstratable_tax_div: f32, gross_sold: f32, cost_sold: f32, ) -> (Vec, Option) { let mut presentation: Vec = vec![]; let tax_pl = 0.19 * gross_div; presentation.push(format!( - "(DYWIDENDY) PRZYCHOD Z ZAGRANICY: {:.2} PLN", + "(DYWIDENDY) PRZYCHÓD Z ZAGRANICY: {:.2} PLN", gross_div )); presentation.push(format!( - "===> (DYWIDENDY) ZRYCZALTOWANY PODATEK: {:.2} PLN", + "===> (DYWIDENDY) ZRYCZAŁTOWANY PODATEK: {:.2} PLN", tax_pl )); + // If tax paid abroad exceeds what can be demonstrated to + // tax collectors e.g. certificate of residency was not presented + // then print information about paid tax and also what can be demonstrated(put into form) + if tax_div != demonstratable_tax_div { + presentation.push(format!( + "===> (DYWIDENDY) PODATEK MOŻLIWY DO WYKAZANIA, ZAPŁACONY ZAGRANICĄ: {:.2} PLN + (PEŁNY PODATEK ZAPŁACONY ZAGRANICĄ: {:.2} PLN)", + demonstratable_tax_div, tax_div, + )); + } else { + presentation.push(format!( + "===> (DYWIDENDY) PODATEK ZAPŁACONY ZAGRANICĄ: {:.2} PLN", + tax_div + )); + } presentation.push(format!( - "===> (DYWIDENDY) PODATEK ZAPLACONY ZAGRANICA: {:.2} PLN", - tax_div - )); - presentation.push(format!( - "===> (SPRZEDAZ AKCJI) PRZYCHOD Z ZAGRANICY: {:.2} PLN", + "===> (SPRZEDAŻ AKCJI) PRZYCHÓD Z ZAGRANICY: {:.2} PLN", gross_sold )); presentation.push(format!( - "===> (SPRZEDAZ AKCJI) KOSZT UZYSKANIA PRZYCHODU: {:.2} PLN", + "===> (SPRZEDAŻ AKCJI) KOSZT UZYSKANIA PRZYCHODU: {:.2} PLN", cost_sold )); if tax_div > tax_pl { @@ -285,18 +305,25 @@ mod tests { let gross_div = 100.0f32; let tax_div = 15.0f32; + let demonstrable_tax_div = 15.0f32; let gross_sold = 1000.0f32; let cost_sold = 10.0f32; let ref_results: Vec = vec![ - "(DYWIDENDY) PRZYCHOD Z ZAGRANICY: 100.00 PLN".to_string(), - "===> (DYWIDENDY) ZRYCZALTOWANY PODATEK: 19.00 PLN".to_string(), - "===> (DYWIDENDY) PODATEK ZAPLACONY ZAGRANICA: 15.00 PLN".to_string(), - "===> (SPRZEDAZ AKCJI) PRZYCHOD Z ZAGRANICY: 1000.00 PLN".to_string(), - "===> (SPRZEDAZ AKCJI) KOSZT UZYSKANIA PRZYCHODU: 10.00 PLN".to_string(), + "(DYWIDENDY) PRZYCHÓD Z ZAGRANICY: 100.00 PLN".to_string(), + "===> (DYWIDENDY) ZRYCZAŁTOWANY PODATEK: 19.00 PLN".to_string(), + "===> (DYWIDENDY) PODATEK ZAPŁACONY ZAGRANICĄ: 15.00 PLN".to_string(), + "===> (SPRZEDAŻ AKCJI) PRZYCHÓD Z ZAGRANICY: 1000.00 PLN".to_string(), + "===> (SPRZEDAŻ AKCJI) KOSZT UZYSKANIA PRZYCHODU: 10.00 PLN".to_string(), ]; - let (results, _) = rd.present_result(gross_div, tax_div, gross_sold, cost_sold); + let (results, _) = rd.present_result( + gross_div, + tax_div, + demonstrable_tax_div, + gross_sold, + cost_sold, + ); results .iter() @@ -356,18 +383,27 @@ mod tests { let gross_div = 100.0f32; let tax_div = 30.0f32; + let demonstrable_tax_div = 15.0f32; let gross_sold = 1000.0f32; let cost_sold = 10.0f32; let ref_results: Vec = vec![ - "(DYWIDENDY) PRZYCHOD Z ZAGRANICY: 100.00 PLN".to_string(), - "===> (DYWIDENDY) ZRYCZALTOWANY PODATEK: 19.00 PLN".to_string(), - "===> (DYWIDENDY) PODATEK ZAPLACONY ZAGRANICA: 30.00 PLN".to_string(), - "===> (SPRZEDAZ AKCJI) PRZYCHOD Z ZAGRANICY: 1000.00 PLN".to_string(), - "===> (SPRZEDAZ AKCJI) KOSZT UZYSKANIA PRZYCHODU: 10.00 PLN".to_string(), + "(DYWIDENDY) PRZYCHÓD Z ZAGRANICY: 100.00 PLN".to_string(), + "===> (DYWIDENDY) ZRYCZAŁTOWANY PODATEK: 19.00 PLN".to_string(), + "===> (DYWIDENDY) PODATEK MOŻLIWY DO WYKAZANIA, ZAPŁACONY ZAGRANICĄ: 15.00 PLN + (PEŁNY PODATEK ZAPŁACONY ZAGRANICĄ: 30.00 PLN)" + .to_string(), + "===> (SPRZEDAŻ AKCJI) PRZYCHÓD Z ZAGRANICY: 1000.00 PLN".to_string(), + "===> (SPRZEDAŻ AKCJI) KOSZT UZYSKANIA PRZYCHODU: 10.00 PLN".to_string(), ]; - let (results, warning) = rd.present_result(gross_div, tax_div, gross_sold, cost_sold); + let (results, warning) = rd.present_result( + gross_div, + tax_div, + demonstrable_tax_div, + gross_sold, + cost_sold, + ); results .iter() diff --git a/src/us.rs b/src/us.rs index d305a01..0ee889e 100644 --- a/src/us.rs +++ b/src/us.rs @@ -3,6 +3,10 @@ pub struct US {} impl etradeTaxReturnHelper::Residency for US { + fn get_tax_deduction_thresholds(&self) -> Box f32> { + todo!("Thresholds for US of deductabile tax not implemented!"); + } + fn get_exchange_rates( &self, dates: &mut std::collections::HashMap< @@ -20,6 +24,7 @@ impl etradeTaxReturnHelper::Residency for US { &self, gross_div: f32, tax_div: f32, + demonstratable_tax_div: f32, gross_sold: f32, cost_sold: f32, ) -> (Vec, Option) { @@ -44,6 +49,7 @@ mod tests { let gross_div = 100.0f32; let tax_div = 15.0f32; + let demonstrable_tax_div = 15.0f32; let gross_sold = 1000.0f32; let cost_sold = 10.0f32; @@ -54,7 +60,13 @@ mod tests { "===> (SOLD STOCK) TAX DEDUCTIBLE COST: $10.00".to_string(), ]; - let (results, _) = rd.present_result(gross_div, tax_div, gross_sold, cost_sold); + let (results, _) = rd.present_result( + gross_div, + tax_div, + demonstrable_tax_div, + gross_sold, + cost_sold, + ); results .iter()