diff --git a/src/gui.rs b/src/gui.rs index c9001b5..6fc9bef 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -117,11 +117,11 @@ pub mod gui { tbuffer.set_text(""); nbuffer.set_text("Running..."); let rd: Box = Box::new(PL {}); - let (gross_div, tax_div, gross_sold, cost_sold, div_transactions, revolut_transactions, sold_transactions) = + let (gross_div, tax_div, gross_sold, cost_sold, interests_transactions, div_transactions, revolut_transactions, sold_transactions) = match run_taxation(&rd, file_names) { - Ok((gd, td, gs, cs, dts, rts, sts)) => { + Ok((gd, td, gs, cs, its, dts, rts, sts)) => { nbuffer.set_text("Finished.\n\n (Double check if generated tax data (Summary) makes sense and then copy it to your tax form)"); - (gd, td, gs, cs, dts, rts, sts) + (gd, td, gs, cs, its, dts, rts, sts) } Err(err) => { nbuffer.set_text(&err); @@ -134,6 +134,9 @@ pub mod gui { nbuffer.set_text(&warn_msg); } let mut transactions_strings: Vec = vec![]; + interests_transactions + .iter() + .for_each(|x| transactions_strings.push(x.format_to_print("INTERESTS").expect_and_log("Error: Formatting INTERESTS transaction failed"))); div_transactions .iter() .for_each(|x| transactions_strings.push(x.format_to_print("DIV").expect_and_log("Error: Formatting DIV transaction failed"))); diff --git a/src/lib.rs b/src/lib.rs index 0b993a0..d91f214 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,9 +10,9 @@ type ReqwestClient = reqwest::blocking::Client; pub use logging::ResultExt; use transactions::{ - create_detailed_div_transactions, create_detailed_revolut_transactions, - create_detailed_sold_transactions, reconstruct_sold_transactions, - verify_dividends_transactions, + create_detailed_div_transactions, create_detailed_interests_transactions, + create_detailed_revolut_transactions, create_detailed_sold_transactions, + reconstruct_sold_transactions, verify_dividends_transactions, verify_interests_transactions, }; #[derive(Debug, PartialEq, PartialOrd, Copy, Clone)] @@ -260,10 +260,12 @@ pub fn run_taxation( f32, Vec, Vec, + Vec, Vec, ), String, > { + let mut parsed_interests_transactions: Vec<(String, f32)> = vec![]; let mut parsed_div_transactions: Vec<(String, f32, f32)> = vec![]; let mut parsed_sold_transactions: Vec<(String, String, f32, f32, f32)> = vec![]; let mut parsed_gain_and_losses: Vec<(String, String, f32, f32, f32)> = vec![]; @@ -274,7 +276,8 @@ pub fn run_taxation( // If name contains .pdf then parse as pdf // if name contains .xlsx then parse as spreadsheet if x.contains(".pdf") { - let (mut div_t, mut sold_t, _) = pdfparser::parse_statement(x)?; + let (mut int_t, mut div_t, mut sold_t, _) = pdfparser::parse_statement(x)?; + parsed_interests_transactions.append(&mut int_t); parsed_div_transactions.append(&mut div_t); parsed_sold_transactions.append(&mut sold_t); } else if x.contains(".xlsx") { @@ -287,6 +290,8 @@ pub fn run_taxation( Ok::<(), String>(()) })?; // 2. Verify Transactions + verify_interests_transactions(&parsed_interests_transactions)?; + log::info!("Interests transactions are consistent"); verify_dividends_transactions(&parsed_div_transactions)?; log::info!("Dividends transactions are consistent"); @@ -300,6 +305,14 @@ pub fn run_taxation( // Hash map : Key(event date) -> (preceeding date, exchange_rate) let mut dates: std::collections::HashMap> = std::collections::HashMap::new(); + parsed_interests_transactions + .iter() + .for_each(|(trade_date, _)| { + let ex = Exchange::USD(trade_date.clone()); + if dates.contains_key(&ex) == false { + dates.insert(ex, None); + } + }); parsed_div_transactions .iter() .for_each(|(trade_date, _, _)| { @@ -340,19 +353,22 @@ pub fn run_taxation( rd.get_exchange_rates(&mut dates).map_err(|x| "Error: unable to get exchange rates. Please check your internet connection or proxy settings\n\nDetails:".to_string()+x.as_str())?; // Make a detailed_div_transactions + let interests = create_detailed_interests_transactions(parsed_interests_transactions, &dates)?; let transactions = create_detailed_div_transactions(parsed_div_transactions, &dates)?; let sold_transactions = create_detailed_sold_transactions(detailed_sold_transactions, &dates)?; let revolut_transactions = create_detailed_revolut_transactions(parsed_revolut_transactions, &dates)?; + let (gross_interests, _) = compute_div_taxation(&interests); let (gross_div, tax_div) = compute_div_taxation(&transactions); let (gross_sold, cost_sold) = compute_sold_taxation(&sold_transactions); let (gross_revolut, cost_revolut) = compute_div_taxation(&revolut_transactions); Ok(( gross_div, tax_div, - gross_sold + gross_revolut, // We put sold and savings income into the same column + gross_interests + gross_sold + gross_revolut, // We put sold and savings income into the same column cost_sold + cost_revolut, + interests, transactions, revolut_transactions, sold_transactions, diff --git a/src/main.rs b/src/main.rs index 440ec44..71689ba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ mod gui; use etradeTaxReturnHelper::run_taxation; use logging::ResultExt; +// TODO: make UT working and test GUI // TODO: Make a parsing of incomplete date // TODO: Dividends of revolut should combined with dividends not sold // TODO: When I sold on Dec there was EST cost (0.04). Make sure it is included in your results @@ -78,7 +79,7 @@ fn main() { let pdfnames: Vec = pdfnames.map(|x| x.to_string()).collect(); let (gross_div, tax_div, gross_sold, cost_sold) = match run_taxation(&rd, pdfnames) { - Ok((gross_div, tax_div, gross_sold, cost_sold, _, _, _)) => { + Ok((gross_div, tax_div, gross_sold, cost_sold, _, _, _, _)) => { (gross_div, tax_div, gross_sold, cost_sold) } Err(msg) => panic!("\nError: Unable to compute taxes. \n\nDetails: {msg}"), @@ -286,7 +287,7 @@ mod tests { let pdfnames: Vec = pdfnames.map(|x| x.to_string()).collect(); match etradeTaxReturnHelper::run_taxation(&rd, pdfnames) { - Ok((gross_div, tax_div, gross_sold, cost_sold, _, _, _)) => { + Ok((gross_div, tax_div, gross_sold, cost_sold, _, _, _, _)) => { assert_eq!( (gross_div, tax_div, gross_sold, cost_sold), (14062.57, 2109.3772, 395.45355, 91.156715) @@ -317,7 +318,7 @@ mod tests { let pdfnames: Vec = pdfnames.map(|x| x.to_string()).collect(); match etradeTaxReturnHelper::run_taxation(&rd, pdfnames) { - Ok((gross_div, tax_div, gross_sold, cost_sold, _, _, _)) => { + Ok((gross_div, tax_div, gross_sold, cost_sold, _, _, _, _)) => { assert_eq!( (gross_div, tax_div, gross_sold, cost_sold), (2930.206, 439.54138, 395.45355, 91.156715) @@ -330,7 +331,7 @@ mod tests { #[test] #[ignore] - fn test_sold_dividends_taxation_2023() -> Result<(), clap::Error> { + fn test_sold_dividends_interests_taxation() -> Result<(), clap::Error> { // Get all brokerage with dividends only let myapp = App::new("E-trade tax helper").setting(AppSettings::ArgRequiredElseHelp); let rd: Box = Box::new(pl::PL {}); @@ -353,10 +354,10 @@ mod tests { let pdfnames: Vec = pdfnames.map(|x| x.to_string()).collect(); match etradeTaxReturnHelper::run_taxation(&rd, pdfnames) { - Ok((gross_div, tax_div, gross_sold, cost_sold, _, _, _)) => { + Ok((gross_div, tax_div, gross_sold, cost_sold, _, _, _, _)) => { assert_eq!( (gross_div, tax_div, gross_sold, cost_sold), - (8369.726, 1253.2899, 14983.293, 7701.9253) + (8355.114, 1253.2899, 14997.904, 7701.9253) ); Ok(()) } @@ -380,7 +381,7 @@ mod tests { let pdfnames: Vec = pdfnames.map(|x| x.to_string()).collect(); match etradeTaxReturnHelper::run_taxation(&rd, pdfnames) { - Ok((gross_div, tax_div, gross_sold, cost_sold, _, _, _)) => { + Ok((gross_div, tax_div, gross_sold, cost_sold, _, _, _, _)) => { assert_eq!( (gross_div, tax_div, gross_sold, cost_sold), (3272.3125, 490.82773, 0.0, 0.0), diff --git a/src/pdfparser.rs b/src/pdfparser.rs index c3cca29..6d37557 100644 --- a/src/pdfparser.rs +++ b/src/pdfparser.rs @@ -11,6 +11,7 @@ enum StatementType { #[derive(Clone, Debug, PartialEq)] enum TransactionType { + Interests, Dividends, Sold, Tax, @@ -150,7 +151,7 @@ fn create_tax_parsing_sequence(sequence: &mut std::collections::VecDeque>, ) { sequence.push_back(Box::new(StringEntry { @@ -369,6 +370,7 @@ fn recognize_statement(page: PageRc) -> Result { } fn process_transaction( + interests_transactions: &mut Vec<(String, f32)>, div_transactions: &mut Vec<(String, f32, f32)>, sold_transactions: &mut Vec<(String, String, f32, f32, f32)>, actual_string: &pdf::primitive::PdfString, @@ -422,6 +424,21 @@ fn process_transaction( subject_to_tax.2 = tax_us; log::info!("Completed parsing Tax transaction"); } + TransactionType::Interests => { + let gross_us = transaction + .next() + .unwrap() + .getf32() + .ok_or("Processing of Interests transaction went wrong")?; + + interests_transactions.push(( + transaction_dates + .pop() + .ok_or("Error: missing transaction dates when parsing")?, + gross_us, + )); + log::info!("Completed parsing Dividend transaction"); + } TransactionType::Dividends => { let gross_us = transaction .next() @@ -470,6 +487,7 @@ fn parse_brokerage_statement<'a, I>( pages_iter: I, ) -> Result< ( + Vec<(String, f32)>, Vec<(String, f32, f32)>, Vec<(String, String, f32, f32, f32)>, Vec<(String, String, i32, f32, f32, f32, f32, f32)>, @@ -571,6 +589,9 @@ where TransactionType::Tax => { return Err("TransactionType::Tax should not appear during brokerage statement processing!".to_string()); } + TransactionType::Interests => { + return Err("TransactionType::Interest rate should not appear during brokerage statement processing!".to_string()); + } TransactionType::Dividends => { let tax_us = transaction.next().unwrap().getf32().expect_and_log("Processing of Dividend transaction went wrong"); let gross_us = transaction.next().unwrap().getf32().expect_and_log("Processing of Dividend transaction went wrong"); @@ -644,7 +665,7 @@ where } } } - Ok((div_transactions, sold_transactions, trades)) + Ok((vec![], div_transactions, sold_transactions, trades)) } fn check_if_transaction( @@ -661,9 +682,9 @@ fn check_if_transaction( year.ok_or("Missing year that should be parsed before transactions".to_owned())?; if candidate_string == "DIVIDEND" { - create_dividend_fund_parsing_sequence(sequence); - state = ParserState::ProcessingTransaction(TransactionType::Dividends); - log::info!("Starting to parse Dividend Fund transaction"); + create_interests_fund_parsing_sequence(sequence); + state = ParserState::ProcessingTransaction(TransactionType::Interests); + log::info!("Starting to parse Interests Fund transaction"); } else if candidate_string == "QUALIFIED DIVIDEND" { create_qualified_dividend_parsing_sequence(sequence); state = ParserState::ProcessingTransaction(TransactionType::Dividends); @@ -708,6 +729,7 @@ fn parse_account_statement<'a, I>( pages_iter: I, ) -> Result< ( + Vec<(String, f32)>, Vec<(String, f32, f32)>, Vec<(String, String, f32, f32, f32)>, Vec<(String, String, i32, f32, f32, f32, f32, f32)>, @@ -717,6 +739,7 @@ fn parse_account_statement<'a, I>( where I: Iterator>, { + let mut interests_transactions: Vec<(String, f32)> = vec![]; let mut div_transactions: Vec<(String, f32, f32)> = vec![]; let mut sold_transactions: Vec<(String, String, f32, f32, f32)> = vec![]; let trades: Vec<(String, String, i32, f32, f32, f32, f32, f32)> = vec![]; @@ -778,6 +801,7 @@ where } ParserState::ProcessingTransaction(transaction_type) => { state = process_transaction( + &mut interests_transactions, &mut div_transactions, &mut sold_transactions, &actual_string, @@ -799,10 +823,16 @@ where } } - Ok((div_transactions, sold_transactions, trades)) + Ok(( + interests_transactions, + div_transactions, + sold_transactions, + trades, + )) } /// This function parses given PDF document /// and returns result of parsing which is a tuple of +/// interest rate transactions /// found Dividends paid transactions (div_transactions), /// Sold stock transactions (sold_transactions) /// information on transactions in case of parsing trade document (trades) @@ -814,6 +844,7 @@ pub fn parse_statement( pdftoparse: &str, ) -> Result< ( + Vec<(String, f32)>, Vec<(String, f32, f32)>, Vec<(String, String, f32, f32, f32)>, Vec<(String, String, i32, f32, f32, f32, f32, f32)>, @@ -835,7 +866,8 @@ pub fn parse_statement( let document_type = recognize_statement(first_page)?; - let (div_transactions, sold_transactions, trades) = match document_type { + let (interests_transactions, div_transactions, sold_transactions, trades) = match document_type + { StatementType::BrokerageStatement => { log::info!("Processing brokerage statement PDF"); parse_brokerage_statement(pdffile_iter)? @@ -846,7 +878,12 @@ pub fn parse_statement( } }; - Ok((div_transactions, sold_transactions, trades)) + Ok(( + interests_transactions, + div_transactions, + sold_transactions, + trades, + )) } #[cfg(test)] @@ -975,7 +1012,7 @@ mod tests { Some("23".to_owned()) ), Ok(ParserState::ProcessingTransaction( - TransactionType::Dividends + TransactionType::Interests )) ); @@ -1025,10 +1062,8 @@ mod tests { assert_eq!( parse_statement("data/MS_ClientStatements_6557_202312.pdf"), (Ok(( - vec![ - ("12/1/23".to_owned(), 1.22, 0.00), - ("12/1/23".to_owned(), 386.50, 57.98), - ], + vec![("12/1/23".to_owned(), 1.22)], + vec![("12/1/23".to_owned(), 386.50, 57.98),], vec![( "12/21/23".to_owned(), "12/26/23".to_owned(), @@ -1048,6 +1083,7 @@ mod tests { assert_eq!( parse_statement("data/example-divs.pdf"), (Ok(( + vec![], vec![("03/01/22".to_owned(), 698.25, 104.74)], vec![], vec![] @@ -1056,6 +1092,7 @@ mod tests { assert_eq!( parse_statement("data/example-sold-wire.pdf"), Ok(( + vec![], vec![], vec![( "05/02/22".to_owned(), diff --git a/src/transactions.rs b/src/transactions.rs index 5a5d031..b93625a 100644 --- a/src/transactions.rs +++ b/src/transactions.rs @@ -4,6 +4,35 @@ use chrono::Datelike; pub use crate::logging::ResultExt; use crate::{SoldTransaction, Transaction}; +/// Check if all interests rate transactions come from the same year +pub fn verify_interests_transactions( + interests_transactions: &Vec<(String, f32)>, +) -> Result<(), String> { + let mut trans = interests_transactions.iter(); + let (transaction_date, _) = match trans.next() { + Some((x, a)) => (x, a), + None => { + log::info!("No interests transactions"); + return Ok(()); + } + }; + + let transaction_year = chrono::NaiveDate::parse_from_str(&transaction_date, "%m/%d/%y") + .unwrap() + .year(); + let mut verification: Result<(), String> = Ok(()); + trans.for_each(|(tr_date, _)| { + let tr_year = chrono::NaiveDate::parse_from_str(&tr_date, "%m/%d/%y") + .unwrap() + .year(); + if tr_year != transaction_year { + let msg: &str = "Error: Brokerage statements are related to different years!"; + verification = Err(msg.to_owned()); + } + }); + verification +} + /// Check if all dividends transaction come from the same year pub fn verify_dividends_transactions( div_transactions: &Vec<(String, f32, f32)>, @@ -115,6 +144,37 @@ pub fn create_detailed_revolut_transactions( Ok(detailed_transactions) } +pub fn create_detailed_interests_transactions( + transactions: Vec<(String, f32)>, + dates: &std::collections::HashMap>, +) -> Result, &str> { + let mut detailed_transactions: Vec = Vec::new(); + transactions + .iter() + .try_for_each(|(transaction_date, gross_us)| { + let (exchange_rate_date, exchange_rate) = dates + [&crate::Exchange::USD(transaction_date.clone())] + .clone() + .unwrap(); + + let transaction = Transaction { + transaction_date: transaction_date.clone(), + gross: crate::Currency::USD(*gross_us as f64), + tax_paid: crate::Currency::USD(0.0 as f64), + exchange_rate_date, + exchange_rate, + }; + + let msg = transaction.format_to_print("INTERESTS")?; + + println!("{}", msg); + log::info!("{}", msg); + detailed_transactions.push(transaction); + Ok::<(), &str>(()) + })?; + Ok(detailed_transactions) +} + pub fn create_detailed_div_transactions( transactions: Vec<(String, f32, f32)>, dates: &std::collections::HashMap>, @@ -199,6 +259,15 @@ mod tests { use super::*; + #[test] + fn test_interests_verification_ok() -> Result<(), String> { + let transactions: Vec<(String, f32)> = vec![ + ("06/01/21".to_string(), 100.0), + ("03/01/21".to_string(), 126.0), + ]; + verify_interests_transactions(&transactions) + } + #[test] fn test_dividends_verification_ok() -> Result<(), String> { let transactions: Vec<(String, f32, f32)> = vec![ @@ -294,6 +363,49 @@ mod tests { Ok(()) } + #[test] + fn test_create_detailed_interests_transactions() -> Result<(), String> { + let parsed_transactions: Vec<(String, f32)> = vec![ + ("04/11/21".to_string(), 100.0), + ("03/01/21".to_string(), 126.0), + ]; + + let mut dates: std::collections::HashMap> = + std::collections::HashMap::new(); + + dates.insert( + crate::Exchange::USD("03/01/21".to_owned()), + Some(("02/28/21".to_owned(), 2.0)), + ); + dates.insert( + crate::Exchange::USD("04/11/21".to_owned()), + Some(("04/10/21".to_owned(), 3.0)), + ); + + let transactions = create_detailed_interests_transactions(parsed_transactions, &dates); + + assert_eq!( + transactions, + Ok(vec![ + Transaction { + transaction_date: "04/11/21".to_string(), + gross: crate::Currency::USD(100.0), + tax_paid: crate::Currency::USD(0.0), + exchange_rate_date: "04/10/21".to_string(), + exchange_rate: 3.0, + }, + Transaction { + transaction_date: "03/01/21".to_string(), + gross: crate::Currency::USD(126.0), + tax_paid: crate::Currency::USD(0.0), + exchange_rate_date: "02/28/21".to_string(), + exchange_rate: 2.0, + }, + ]) + ); + Ok(()) + } + #[test] fn test_create_detailed_div_transactions() -> Result<(), String> { let parsed_transactions: Vec<(String, f32, f32)> = vec![