diff --git a/src/csvparser.rs b/src/csvparser.rs index 3049e92..b5aa804 100644 --- a/src/csvparser.rs +++ b/src/csvparser.rs @@ -24,6 +24,7 @@ struct InvestmentTransactions { pub sold_dates: Vec, pub costs: Vec, pub gross: Vec, + pub symbols: Vec>, } #[derive(Default)] struct TransactionAccumulator { @@ -32,12 +33,19 @@ struct TransactionAccumulator { pub dates: Vec, pub incomes: Vec, pub taxes: Vec, + pub symbols: Vec>, } #[derive(Debug, PartialEq)] pub struct RevolutTransactions { - pub dividend_transactions: Vec<(String, crate::Currency, crate::Currency)>, - pub sold_transactions: Vec<(String, String, crate::Currency, crate::Currency)>, + pub dividend_transactions: Vec<(String, crate::Currency, crate::Currency, Option)>, + pub sold_transactions: Vec<( + String, + String, + crate::Currency, + crate::Currency, + Option, + )>, pub crypto_transactions: Vec<(String, String, crate::Currency, crate::Currency)>, } @@ -115,10 +123,17 @@ fn extract_cash(cashline: &str) -> Result { fn extract_dividends_transactions(df: &DataFrame) -> Result { let df_transactions = if df.get_column_names().contains(&"Currency") { - df.select(["Date", "Gross amount", "Withholding tax", "Currency"]) + df.select([ + "Date", + "Symbol", + "Gross amount", + "Withholding tax", + "Currency", + ]) } else { df.select([ "Date", + "Symbol", "Gross amount base currency", "Net amount base currency", ]) @@ -133,6 +148,7 @@ fn extract_sold_transactions(df: &DataFrame) -> Result df.select([ "Date acquired", "Date sold", + "Symbol", "Cost basis", "Gross proceeds", "Currency", @@ -141,6 +157,7 @@ fn extract_sold_transactions(df: &DataFrame) -> Result df.select([ "Date acquired", "Date sold", + "Symbol", "Cost basis base currency", "Gross proceeds base currency", "Fees base currency", @@ -155,7 +172,7 @@ fn extract_investment_gains_and_costs_transactions( df: &DataFrame, ) -> Result { let df_transactions = df - .select(["Date", "Type", "Total Amount"]) + .select(["Date", "Ticker", "Type", "Total Amount"]) .map_err(|_| "Error: Unable to select description")?; let intrest_rate_mask = df_transactions @@ -223,6 +240,27 @@ fn extract_intrest_rate_transactions(df: &DataFrame) -> Result Result>, &'static str> { + let symbol = df + .column(col_name) + .map_err(|_| "Error: Unable to select Symbol")?; + let mut symbols: Vec> = vec![]; + let possible_symbols = symbol + .utf8() + .map_err(|_| "Error: Unable to convert to utf8")?; + + possible_symbols.into_iter().try_for_each(|maybe_symbol| { + if let Some(s) = maybe_symbol { + symbols.push(Some(s.to_string())); + } else { + symbols.push(None); + } + Ok::<(), &str>(()) + })?; + + Ok(symbols) +} + fn parse_investment_transaction_dates( df: &DataFrame, col_name: &str, @@ -352,6 +390,7 @@ fn process_tax_consolidated_data( ta.dates .extend(parse_investment_transaction_dates(&filtred_df, "Date")?); let lincomes = parse_incomes(&filtred_df, "Money in")?; + ta.symbols.extend(std::iter::repeat_n(None, lincomes.len())); let ltaxes: Vec = lincomes.iter().map(|i| i.derive(0.0)).collect(); ta.taxes.extend(ltaxes); ta.incomes.extend(lincomes); @@ -376,6 +415,9 @@ fn process_tax_consolidated_data( } ta.stock.sold_dates.extend(lsold_dates); ta.stock.acquired_dates.extend(lacquired_dates); + ta.stock + .symbols + .extend(parse_symbols(&filtred_df, "Symbol")?); let lcosts = parse_incomes(&filtred_df, "Cost basis base currency")?; ta.stock .gross @@ -403,6 +445,8 @@ fn process_tax_consolidated_data( ta.dates .extend(parse_investment_transaction_dates(&filtred_df, "Date")?); + ta.symbols.extend(parse_symbols(&filtred_df, "Symbol")?); + // parse income let lincomes = parse_incomes(&filtred_df, "Gross amount base currency")?; // parse taxes @@ -447,13 +491,20 @@ fn process_tax_consolidated_data( /// Parse revolut CSV documents (savings account, trading, crypto) /// returns: ( -/// dividend transactions in a form: date, gross income, tax taken +/// dividend transactions in a form: date, gross income, tax taken, company name (if available) /// sold transactions in a form date acquired, date sold, cost basis, gross income /// crypto transactions in a form date acquired, date sold, cost basis, gross income /// ) pub fn parse_revolut_transactions(csvtoparse: &str) -> Result { - let mut dividend_transactions: Vec<(String, crate::Currency, crate::Currency)> = vec![]; - let mut sold_transactions: Vec<(String, String, crate::Currency, crate::Currency)> = vec![]; + let mut dividend_transactions: Vec<(String, crate::Currency, crate::Currency, Option)> = + vec![]; + let mut sold_transactions: Vec<( + String, + String, + crate::Currency, + crate::Currency, + Option, + )> = vec![]; let mut crypto_transactions: Vec<(String, String, crate::Currency, crate::Currency)> = vec![]; let mut ta = TransactionAccumulator::default(); @@ -496,6 +547,8 @@ pub fn parse_revolut_transactions(csvtoparse: &str) -> Result Result Result Result Result Result Result<(), &'static str> { + let dates = vec!["25 Aug 2023", "1 Sep 2023"]; + let symbols = vec!["AAPL", "MSFT"]; + let expected_symbols = symbols + .iter() + .map(|s| Some(s.to_string())) + .collect::>>(); + + let input_date_series = Series::new("Date", dates); + let input_symbols = Series::new("Symbol", symbols); + + let df = DataFrame::new(vec![input_date_series, input_symbols]) + .map_err(|_| "Error creating DataFrame")?; + + assert_eq!(parse_symbols(&df, "Symbol"), Ok(expected_symbols)); + Ok(()) + } + #[test] fn test_parse_transaction_dates_us() -> Result<(), String> { let description = vec!["odsetki", "odsetki"]; @@ -979,7 +1103,7 @@ mod tests { parsed .dividend_transactions .iter() - .for_each(|(_, amount, _)| match amount { + .for_each(|(_, amount, _, _)| match amount { crate::Currency::EUR(v) => sum_eur += v, crate::Currency::PLN(v) => sum_pln += v, _ => (), @@ -1016,16 +1140,19 @@ mod tests { "01/03/24".to_owned(), crate::Currency::EUR(0.01), crate::Currency::EUR(0.00), + None, ), ( "01/04/24".to_owned(), crate::Currency::EUR(0.02), crate::Currency::EUR(0.00), + None, ), ( "12/31/24".to_owned(), crate::Currency::EUR(0.01), crate::Currency::EUR(0.00), + None, ), ], sold_transactions: vec![], @@ -1047,44 +1174,52 @@ mod tests { "01/01/24".to_owned(), crate::Currency::EUR(0.26), crate::Currency::EUR(0.00), + None, ), ( "04/12/24".to_owned(), crate::Currency::EUR(0.24), crate::Currency::EUR(0.00), + None, ), // PLN interests ( "01/04/24".to_owned(), crate::Currency::PLN(0.86), crate::Currency::PLN(0.00), + None, ), ( "05/31/24".to_owned(), crate::Currency::PLN(1.26), crate::Currency::PLN(0.00), + None, ), // Euro dividends ( "08/26/24".to_owned(), crate::Currency::PLN(302.43), crate::Currency::PLN(302.43 - 222.65), + Some("DE000A289XJ2".to_string()), ), // USD dividends ( "03/04/24".to_owned(), crate::Currency::PLN(617.00), crate::Currency::PLN(617.00 - 524.43), + Some("TFC".to_string()), ), ( "03/21/24".to_owned(), crate::Currency::PLN(259.17), crate::Currency::PLN(0.0), + Some("AMCR".to_string()), ), ( "12/17/24".to_owned(), crate::Currency::PLN(903.35), crate::Currency::PLN(903.35 - 767.83), + Some("EPR".to_string()), ), ], sold_transactions: vec![ @@ -1093,36 +1228,42 @@ mod tests { "10/28/24".to_owned(), crate::Currency::PLN(13037.94 + 65.94), crate::Currency::PLN(13348.22), + Some("EU000A3K4DJ5".to_string()), ), ( "09/09/24".to_owned(), "11/21/24".to_owned(), crate::Currency::PLN(16097.86 + 81.41), crate::Currency::PLN(16477.91), + Some("XS1218821756".to_string()), ), ( "11/20/23".to_owned(), "08/12/24".to_owned(), crate::Currency::PLN(19863.25 + 0.66), crate::Currency::PLN(22865.17), + Some("XOM".to_string()), ), ( "06/11/24".to_owned(), "10/14/24".to_owned(), crate::Currency::PLN(525.08 + 0.0), crate::Currency::PLN(624.00), + Some("TFC".to_string()), ), ( "10/23/23".to_owned(), "10/14/24".to_owned(), crate::Currency::PLN(835.88 + 0.03), crate::Currency::PLN(1046.20), + Some("AMCR".to_string()), ), ( "08/22/24".to_owned(), "10/17/24".to_owned(), crate::Currency::PLN(25135.50 + 128.17), crate::Currency::PLN(26130.41), + Some("US13607LNF66".to_string()), ), ], crypto_transactions: vec![], @@ -1142,21 +1283,25 @@ mod tests { "06/04/24".to_owned(), crate::Currency::PLN(2.80), crate::Currency::PLN(0.68), + Some("QDVY".to_string()), ), ( "06/20/24".to_owned(), crate::Currency::PLN(0.34), crate::Currency::PLN(0.08), + Some("EXI2".to_string()), ), ( "06/28/24".to_owned(), crate::Currency::PLN(3.79), crate::Currency::PLN(0.94), + Some("IS3K".to_string()), ), ( "07/01/24".to_owned(), crate::Currency::PLN(1.07), crate::Currency::PLN(0.25), + Some("IBCD".to_string()), ), ], sold_transactions: vec![], @@ -1179,46 +1324,55 @@ mod tests { "06/04/24".to_owned(), crate::Currency::PLN(2.80), crate::Currency::PLN(0.68), + Some("QDVY".to_string()), ), ( "06/20/24".to_owned(), crate::Currency::PLN(0.34), crate::Currency::PLN(0.08), + Some("EXI2".to_string()), ), ( "06/28/24".to_owned(), crate::Currency::PLN(3.79), crate::Currency::PLN(0.94), + Some("IS3K".to_string()), ), ( "07/01/24".to_owned(), crate::Currency::PLN(1.07), crate::Currency::PLN(0.25), + Some("IBCD".to_string()), ), ( "09/27/24".to_owned(), crate::Currency::PLN(1.02), crate::Currency::PLN(0.25), + Some("IBCD".to_string()), ), ( "09/27/24".to_owned(), crate::Currency::PLN(1.71), crate::Currency::PLN(0.42), + Some("IUSU".to_string()), ), ( "11/29/24".to_owned(), crate::Currency::PLN(2.92), crate::Currency::PLN(0.73), + Some("QDVY".to_string()), ), ( "12/17/24".to_owned(), crate::Currency::PLN(0.04), crate::Currency::PLN(0.0), + Some("EXI2".to_string()), ), ( "12/31/24".to_owned(), crate::Currency::PLN(1.07), crate::Currency::PLN(0.25), + Some("IBCD".to_string()), ), ], sold_transactions: vec![], @@ -1241,81 +1395,97 @@ mod tests { "03/04/24".to_owned(), crate::Currency::PLN(617.00), crate::Currency::PLN(92.57), + Some("TFC".to_string()), ), ( "03/21/24".to_owned(), crate::Currency::PLN(259.17), crate::Currency::PLN(0.0), + Some("AMCR".to_string()), ), ( "03/25/24".to_owned(), crate::Currency::PLN(212.39), crate::Currency::PLN(31.87), + Some("PXD".to_string()), ), ( "05/16/24".to_owned(), crate::Currency::PLN(700.17), crate::Currency::PLN(105.04), + Some("EPR".to_string()), ), ( "05/31/24".to_owned(), crate::Currency::PLN(875.82), crate::Currency::PLN(131.38), + Some("UPS".to_string()), ), ( "06/03/24".to_owned(), crate::Currency::PLN(488.26), crate::Currency::PLN(73.25), + Some("ABR".to_string()), ), ( "06/04/24".to_owned(), crate::Currency::PLN(613.2), crate::Currency::PLN(92.00), + Some("TFC".to_string()), ), ( "06/11/24".to_owned(), crate::Currency::PLN(186.16), crate::Currency::PLN(27.92), + Some("XOM".to_string()), ), ( "06/13/24".to_owned(), crate::Currency::PLN(264.74), crate::Currency::PLN(0.00), + Some("AMCR".to_string()), ), ( "06/18/24".to_owned(), crate::Currency::PLN(858.33), crate::Currency::PLN(128.74), + Some("EPR".to_string()), ), ( "07/12/24".to_owned(), crate::Currency::PLN(421.5), crate::Currency::PLN(63.23), + Some("BBY".to_string()), ), ( "07/16/24".to_owned(), crate::Currency::PLN(834.55), crate::Currency::PLN(125.18), + Some("EPR".to_string()), ), ( "08/16/24".to_owned(), crate::Currency::PLN(834.79), crate::Currency::PLN(125.23), + Some("EPR".to_string()), ), ( "08/26/24".to_owned(), crate::Currency::PLN(302.43), crate::Currency::PLN(79.77), + Some("DE000A289XJ2".to_string()), ), ( "08/29/24".to_owned(), crate::Currency::PLN(801.25), crate::Currency::PLN(0.0), + Some("BMO".to_string()), ), ( "08/30/24".to_owned(), crate::Currency::PLN(872.56), crate::Currency::PLN(130.90), + Some("CAG".to_string()), ), ], sold_transactions: vec![( @@ -1323,6 +1493,7 @@ mod tests { "08/12/24".to_owned(), crate::Currency::USD(5000.0), crate::Currency::USD(5804.62), + Some("XOM".to_string()), )], crypto_transactions: vec![], }); @@ -1345,96 +1516,115 @@ mod tests { "12/12/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), + None, ), ( "12/13/23".to_owned(), crate::Currency::PLN(0.20), crate::Currency::PLN(0.00), + None, ), ( "12/15/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), + None, ), ( "12/16/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), + None, ), ( "12/17/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), + None, ), ( "12/18/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), + None, ), ( "12/19/23".to_owned(), crate::Currency::PLN(0.41), crate::Currency::PLN(0.00), + None, ), ( "12/20/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), + None, ), ( "12/21/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), + None, ), ( "12/22/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), + None, ), ( "12/23/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), + None, ), ( "12/24/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), + None, ), ( "12/25/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), + None, ), ( "12/26/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), + None, ), ( "12/27/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), + None, ), ( "12/28/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), + None, ), ( "12/29/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), + None, ), ( "12/30/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), + None, ), ( "12/31/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), + None, ), ], sold_transactions: vec![], @@ -1456,16 +1646,19 @@ mod tests { "11/02/23".to_owned(), crate::Currency::USD(-0.02), crate::Currency::USD(0.00), + None, ), ( "12/01/23".to_owned(), crate::Currency::USD(-0.51), crate::Currency::USD(0.00), + None, ), ( "12/14/23".to_owned(), crate::Currency::USD(2.94), crate::Currency::USD(0.00), + Some("AMCR".to_string()), ), ], sold_transactions: vec![], diff --git a/src/gui.rs b/src/gui.rs index 0ce7a10..1329e21 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -129,7 +129,7 @@ fn create_execute_documents( revolut_dividends_transactions: revolut_transactions, sold_transactions, revolut_sold_transactions, - } = match run_taxation(&rd, file_names) { + } = match run_taxation(&rd, file_names,false) { Ok(res) => { nbuffer.set_text("Finished.\n\n (Double check if generated tax data (Summary) makes sense and then copy it to your tax form)"); res diff --git a/src/lib.rs b/src/lib.rs index 874e6a6..b6b6324 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,7 @@ pub use logging::ResultExt; use transactions::{ create_detailed_div_transactions, create_detailed_interests_transactions, create_detailed_revolut_sold_transactions, create_detailed_revolut_transactions, - create_detailed_sold_transactions, reconstruct_sold_transactions, + create_detailed_sold_transactions, create_per_company_report, reconstruct_sold_transactions, verify_dividends_transactions, verify_interests_transactions, verify_transactions, }; @@ -65,6 +65,7 @@ pub struct Transaction { pub tax_paid: Currency, pub exchange_rate_date: String, pub exchange_rate: f32, + pub company: Option, } impl Transaction { @@ -114,6 +115,9 @@ pub struct SoldTransaction { pub exchange_rate_settlement: f32, pub exchange_rate_acquisition_date: String, pub exchange_rate_acquisition: f32, + pub company: Option, + // TODO + //pub country : Option, } impl SoldTransaction { @@ -368,15 +372,27 @@ pub fn validate_file_names(files: &Vec) -> Result<(), String> { pub fn run_taxation( rd: &Box, names: Vec, + per_company: bool, ) -> Result { validate_file_names(&names)?; let mut parsed_interests_transactions: Vec<(String, f32, 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_div_transactions: Vec<(String, f32, f32, Option)> = vec![]; + let mut parsed_sold_transactions: Vec<(String, String, f32, f32, f32, Option)> = vec![]; let mut parsed_gain_and_losses: Vec<(String, String, f32, f32, f32)> = vec![]; - let mut parsed_revolut_dividends_transactions: Vec<(String, Currency, Currency)> = vec![]; - let mut parsed_revolut_sold_transactions: Vec<(String, String, Currency, Currency)> = vec![]; + let mut parsed_revolut_dividends_transactions: Vec<( + String, + Currency, + Currency, + Option, + )> = vec![]; + let mut parsed_revolut_sold_transactions: Vec<( + String, + String, + Currency, + Currency, + Option, + )> = vec![]; // 1. Parse PDF,XLSX and CSV documents to get list of transactions names.iter().try_for_each(|x| { @@ -432,14 +448,14 @@ pub fn run_taxation( }); parsed_div_transactions .iter() - .for_each(|(trade_date, _, _)| { + .for_each(|(trade_date, _, _, _)| { let ex = Exchange::USD(trade_date.clone()); if dates.contains_key(&ex) == false { dates.insert(ex, None); } }); detailed_sold_transactions.iter().for_each( - |(trade_date, settlement_date, acquisition_date, _, _)| { + |(trade_date, settlement_date, acquisition_date, _, _, _)| { let ex = Exchange::USD(trade_date.clone()); if dates.contains_key(&ex) == false { dates.insert(ex, None); @@ -456,15 +472,14 @@ pub fn run_taxation( ); parsed_revolut_dividends_transactions .iter() - .for_each(|(trade_date, gross, _)| { + .for_each(|(trade_date, gross, _, _)| { let ex = gross.derive_exchange(trade_date.clone()); if dates.contains_key(&ex) == false { dates.insert(ex, None); } }); - parsed_revolut_sold_transactions - .iter() - .for_each(|(acquired_date, sold_date, cost, gross)| { + parsed_revolut_sold_transactions.iter().for_each( + |(acquired_date, sold_date, cost, gross, _)| { let ex = cost.derive_exchange(acquired_date.clone()); if dates.contains_key(&ex) == false { dates.insert(ex, None); @@ -473,7 +488,8 @@ pub fn run_taxation( if dates.contains_key(&ex) == false { dates.insert(ex, None); } - }); + }, + ); 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())?; @@ -486,6 +502,18 @@ pub fn run_taxation( let revolut_sold_transactions = create_detailed_revolut_sold_transactions(parsed_revolut_sold_transactions, &dates)?; + if per_company { + let per_company_report = create_per_company_report( + &interests, + &transactions, + &sold_transactions, + &revolut_dividends_transactions, + &revolut_sold_transactions, + )?; + + println!("{}", per_company_report); + } + 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); @@ -577,6 +605,7 @@ mod tests { tax_paid: crate::Currency::USD(25.0), exchange_rate_date: "N/A".to_string(), exchange_rate: 4.0, + company: Some("INTEL CORP".to_owned()), }]; assert_eq!(compute_div_taxation(&transactions), (400.0, 100.0)); Ok(()) @@ -592,6 +621,7 @@ mod tests { tax_paid: crate::Currency::USD(25.0), exchange_rate_date: "N/A".to_string(), exchange_rate: 4.0, + company: Some("INTEL CORP".to_owned()), }, Transaction { transaction_date: "N/A".to_string(), @@ -599,6 +629,7 @@ mod tests { tax_paid: crate::Currency::USD(10.0), exchange_rate_date: "N/A".to_string(), exchange_rate: 3.5, + company: Some("INTEL CORP".to_owned()), }, ]; assert_eq!( @@ -616,6 +647,7 @@ mod tests { tax_paid: crate::Currency::PLN(0.0), exchange_rate_date: "N/A".to_string(), exchange_rate: 1.0, + company: None, }, Transaction { transaction_date: "04/11/21".to_string(), @@ -623,6 +655,7 @@ mod tests { tax_paid: crate::Currency::PLN(0.0), exchange_rate_date: "N/A".to_string(), exchange_rate: 1.0, + company: None, }, ]; assert_eq!( @@ -641,6 +674,7 @@ mod tests { tax_paid: crate::Currency::EUR(0.0), exchange_rate_date: "02/28/21".to_string(), exchange_rate: 2.0, + company: None, }, Transaction { transaction_date: "04/11/21".to_string(), @@ -648,6 +682,7 @@ mod tests { tax_paid: crate::Currency::EUR(0.0), exchange_rate_date: "04/10/21".to_string(), exchange_rate: 3.0, + company: None, }, ]; assert_eq!( @@ -670,6 +705,7 @@ mod tests { exchange_rate_settlement: 5.0, exchange_rate_acquisition_date: "N/A".to_string(), exchange_rate_acquisition: 6.0, + company: Some("TFC".to_owned()), }]; assert_eq!( compute_sold_taxation(&transactions), @@ -692,6 +728,7 @@ mod tests { exchange_rate_settlement: 5.0, exchange_rate_acquisition_date: "N/A".to_string(), exchange_rate_acquisition: 6.0, + company: Some("PXD".to_owned()), }, SoldTransaction { trade_date: "N/A".to_string(), @@ -703,6 +740,7 @@ mod tests { exchange_rate_settlement: 2.0, exchange_rate_acquisition_date: "N/A".to_string(), exchange_rate_acquisition: 3.0, + company: Some("TFC".to_owned()), }, ]; assert_eq!( diff --git a/src/main.rs b/src/main.rs index 7dbb99f..dd65a83 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,8 @@ use etradeTaxReturnHelper::run_taxation; use etradeTaxReturnHelper::TaxCalculationResult; use logging::ResultExt; +// 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 // require one page not two // TODO: remove support for account statement of investment account of revolut @@ -40,15 +42,29 @@ fn create_cmd_line_pattern(myapp: Command) -> Command { ) .arg( Arg::new("financial documents") - .help("Brokerage statement PDFs and Gain & Losses xlsx documents\n\nBrokerege statements can be downloaded from:\n\thttps://edoc.etrade.com/e/t/onlinedocs/docsearch?doc_type=stmt\n\nGain&Losses documents can be downloaded from:\n\thttps://us.etrade.com/etx/sp/stockplan#/myAccount/gainsLosses\n") + .help("Account statement PDFs and Gain & Losses xlsx documents\n\nAccount statements can be downloaded from:\n\thttps://edoc.etrade.com/e/t/onlinedocs/docsearch?doc_type=stmt\n\nGain&Losses documents can be downloaded from:\n\thttps://us.etrade.com/etx/sp/stockplan#/myAccount/gainsLosses\n") .num_args(1..) .required(true), ) + .arg( + Arg::new("per-company") + .long("per-company") + .help("Enable per-company mode") + .action(clap::ArgAction::SetTrue) + ) +} + +fn configure_dataframes_format() { + // Make sure to show all raws + if std::env::var("POLARS_FMT_MAX_ROWS").is_err() { + std::env::set_var("POLARS_FMT_MAX_ROWS", "-1") + } } fn main() { const VERSION: &str = env!("CARGO_PKG_VERSION"); logging::init_logging_infrastructure(); + configure_dataframes_format(); log::info!("Started etradeTaxHelper"); // If there is no arguments then start GUI @@ -91,7 +107,7 @@ fn main() { gross_sold, cost_sold, .. - } = match run_taxation(&rd, pdfnames) { + } = match run_taxation(&rd, pdfnames, matches.get_flag("per-company")) { Ok(res) => res, Err(msg) => panic!("\nError: Unable to compute taxes. \n\nDetails: {msg}"), }; @@ -212,6 +228,38 @@ mod tests { }; Ok(()) } + #[test] + fn test_cmdline_per_company() -> Result<(), clap::Error> { + // Init Transactions + let myapp = Command::new("E-trade tax helper"); + let matches = + create_cmd_line_pattern(myapp).get_matches_from(vec!["mytest", "data/example.pdf"]); + let per_company = matches.get_flag("per-company"); + match per_company { + false => (), + true => { + return Err(clap::error::Error::::new( + clap::error::ErrorKind::InvalidValue, + )) + } + }; + let myapp = Command::new("E-trade tax helper"); + let matches = create_cmd_line_pattern(myapp).get_matches_from(vec![ + "mytest", + "--per-company", + "data/example.pdf", + ]); + let per_company = matches.get_flag("per-company"); + match per_company { + true => (), + false => { + return Err(clap::error::Error::::new( + clap::error::ErrorKind::InvalidValue, + )) + } + }; + Ok(()) + } #[test] fn test_cmdline_pl() -> Result<(), clap::Error> { @@ -282,134 +330,12 @@ mod tests { .expect_and_log("error getting financial documents names"); let pdfnames: Vec = pdfnames.map(|x| x.to_string()).collect(); - match etradeTaxReturnHelper::run_taxation(&rd, pdfnames) { + match etradeTaxReturnHelper::run_taxation(&rd, pdfnames, false) { Ok(_) => panic!("Expected an error from run_taxation, but got Ok"), Err(_) => Ok(()), // Expected error, test passes } } - #[test] - #[ignore] - fn test_dividends_taxation() -> Result<(), clap::Error> { - // Get all brokerage with dividends only - let myapp = Command::new("etradeTaxHelper").arg_required_else_help(true); - let rd: Box = Box::new(pl::PL {}); - // Check printed values or returned values? - let matches = create_cmd_line_pattern(myapp).get_matches_from(vec![ - "mytest", - "data/Brokerage Statement - XXXX0848 - 202202.pdf", - "data/Brokerage Statement - XXXX0848 - 202203.pdf", - "data/Brokerage Statement - XXXX0848 - 202204.pdf", - "data/Brokerage Statement - XXXX0848 - 202205.pdf", - "data/Brokerage Statement - XXXX0848 - 202206.pdf", - "data/Brokerage Statement - XXXX0848 - 202209.pdf", - "data/Brokerage Statement - XXXX0848 - 202211.pdf", - "data/Brokerage Statement - XXXX0848 - 202212.pdf", - "data/G&L_Collapsed.xlsx", - ]); - - let pdfnames = matches - .get_many::("financial documents") - .expect_and_log("error getting brokarage statements pdfs names"); - let pdfnames: Vec = pdfnames.map(|x| x.to_string()).collect(); - - match etradeTaxReturnHelper::run_taxation(&rd, pdfnames) { - Ok(TaxCalculationResult { - gross_income: gross_div, - tax: tax_div, - gross_sold, - cost_sold, - .. - }) => { - assert_eq!( - (gross_div, tax_div, gross_sold, cost_sold), - (14062.57, 2109.3772, 395.45355, 91.156715) - ); - Ok(()) - } - Err(x) => panic!("Error in taxation process: {x}"), - } - } - - #[test] - #[ignore] - fn test_sold_dividends_taxation() -> Result<(), clap::Error> { - // Get all brokerage with dividends only - let myapp = Command::new("etradeTaxHelper").arg_required_else_help(true); - let rd: Box = Box::new(pl::PL {}); - let matches = create_cmd_line_pattern(myapp).get_matches_from(vec![ - "mytest", - "data/Brokerage Statement - XXXX0848 - 202202.pdf", - "data/Brokerage Statement - XXXX0848 - 202203.pdf", - "data/Brokerage Statement - XXXX0848 - 202204.pdf", - "data/Brokerage Statement - XXXX0848 - 202205.pdf", - "data/G&L_Collapsed.xlsx", - ]); - let pdfnames = matches - .get_many::("financial documents") - .expect_and_log("error getting brokarage statements pdfs names"); - let pdfnames: Vec = pdfnames.map(|x| x.to_string()).collect(); - - match etradeTaxReturnHelper::run_taxation(&rd, pdfnames) { - Ok(TaxCalculationResult { - gross_income: gross_div, - tax: tax_div, - gross_sold, - cost_sold, - .. - }) => { - assert_eq!( - (gross_div, tax_div, gross_sold, cost_sold), - (2930.206, 439.54138, 395.45355, 91.156715) - ); - Ok(()) - } - Err(x) => panic!("Error in taxation process: {x}"), - } - } - - #[test] - #[ignore] - fn test_sold_dividends_interests_taxation() -> Result<(), clap::Error> { - // Get all brokerage with dividends only - let myapp = Command::new("etradeTaxHelper").arg_required_else_help(true); - let rd: Box = Box::new(pl::PL {}); - - let matches = create_cmd_line_pattern(myapp).get_matches_from(vec![ - "mytest", - "etrade_data_2023/Brokerage Statement - XXXXX6557 - 202302.pdf", - "etrade_data_2023/Brokerage Statement - XXXXX6557 - 202303.pdf", - "etrade_data_2023/Brokerage Statement - XXXXX6557 - 202306.pdf", - "etrade_data_2023/Brokerage Statement - XXXXX6557 - 202308.pdf", - "etrade_data_2023/Brokerage Statement - XXXXX6557 - 202309.pdf", - "etrade_data_2023/MS_ClientStatements_6557_202309.pdf", - "etrade_data_2023/MS_ClientStatements_6557_202311.pdf", - "etrade_data_2023/MS_ClientStatements_6557_202312.pdf", - "etrade_data_2023/G&L_Collapsed-2023.xlsx", - ]); - let pdfnames = matches - .get_many::("financial documents") - .expect_and_log("error getting brokarage statements pdfs names"); - let pdfnames: Vec = pdfnames.map(|x| x.to_string()).collect(); - - match etradeTaxReturnHelper::run_taxation(&rd, pdfnames) { - Ok(TaxCalculationResult { - gross_income: gross_div, - tax: tax_div, - gross_sold, - cost_sold, - .. - }) => { - assert_eq!( - (gross_div, tax_div, gross_sold, cost_sold), - (8369.726, 1253.2899, 14983.293, 7701.9253), - ); - Ok(()) - } - Err(x) => panic!("Error in taxation process: {x}"), - } - } - #[test] fn test_revolut_dividends_pln() -> Result<(), clap::Error> { // Get all brokerage with dividends only @@ -425,7 +351,7 @@ mod tests { .expect_and_log("error getting brokarage statements pdfs names"); let pdfnames: Vec = pdfnames.map(|x| x.to_string()).collect(); - match etradeTaxReturnHelper::run_taxation(&rd, pdfnames) { + match etradeTaxReturnHelper::run_taxation(&rd, pdfnames, false) { Ok(TaxCalculationResult { gross_income: gross_div, tax: tax_div, @@ -458,7 +384,7 @@ mod tests { .expect_and_log("error getting brokarage statements pdfs names"); let pdfnames: Vec = pdfnames.map(|x| x.to_string()).collect(); - match etradeTaxReturnHelper::run_taxation(&rd, pdfnames) { + match etradeTaxReturnHelper::run_taxation(&rd, pdfnames, false) { Ok(TaxCalculationResult { gross_income: gross_div, tax: tax_div, @@ -491,7 +417,7 @@ mod tests { .expect_and_log("error getting brokarage statements pdfs names"); let pdfnames: Vec = pdfnames.map(|x| x.to_string()).collect(); - match etradeTaxReturnHelper::run_taxation(&rd, pdfnames) { + match etradeTaxReturnHelper::run_taxation(&rd, pdfnames, false) { Ok(TaxCalculationResult { gross_income: gross_div, tax: tax_div, @@ -511,20 +437,21 @@ mod tests { #[test] #[ignore] - fn test_sold_dividends_only_taxation() -> Result<(), clap::Error> { + fn test_sold_dividends_interests_taxation() -> Result<(), clap::Error> { // Get all brokerage with dividends only let myapp = Command::new("etradeTaxHelper").arg_required_else_help(true); let rd: Box = Box::new(pl::PL {}); let matches = create_cmd_line_pattern(myapp).get_matches_from(vec![ "mytest", - "data/Brokerage Statement - XXXX0848 - 202206.pdf", + "etrade_data_2025/ClientStatements_010226.pdf", + "etrade_data_2025/G&L_Collapsed.xlsx", ]); let pdfnames = matches .get_many::("financial documents") .expect_and_log("error getting brokarage statements pdfs names"); let pdfnames: Vec = pdfnames.map(|x| x.to_string()).collect(); - match etradeTaxReturnHelper::run_taxation(&rd, pdfnames) { + match etradeTaxReturnHelper::run_taxation(&rd, pdfnames, false) { Ok(TaxCalculationResult { gross_income: gross_div, tax: tax_div, @@ -534,7 +461,7 @@ mod tests { }) => { assert_eq!( (gross_div, tax_div, gross_sold, cost_sold), - (3272.3125, 490.82773, 0.0, 0.0), + (219.34755, 0.0, 89845.65, 44369.938), ); Ok(()) } @@ -555,7 +482,7 @@ mod tests { .expect_and_log("error getting brokarage statements pdfs names"); let pdfnames: Vec = pdfnames.map(|x| x.to_string()).collect(); - match etradeTaxReturnHelper::run_taxation(&rd, pdfnames) { + match etradeTaxReturnHelper::run_taxation(&rd, pdfnames, false) { Ok(TaxCalculationResult { gross_income: gross_div, tax: tax_div, diff --git a/src/pdfparser.rs b/src/pdfparser.rs index 79c95b5..027a986 100644 --- a/src/pdfparser.rs +++ b/src/pdfparser.rs @@ -227,6 +227,10 @@ fn create_qualified_dividend_parsing_sequence( } fn create_sold_parsing_sequence(sequence: &mut std::collections::VecDeque>) { + sequence.push_back(Box::new(StringEntry { + val: String::new(), + patterns: vec!["INTC".to_owned(), "DLB".to_owned()], + })); // INTC, DLB sequence.push_back(Box::new(F32Entry { val: 0.0 })); // Quantity sequence.push_back(Box::new(F32Entry { val: 0.0 })); // Price sequence.push_back(Box::new(F32Entry { val: 0.0 })); // Amount Sold @@ -330,7 +334,12 @@ fn create_trade_parsing_sequence(sequence: &mut std::collections::VecDeque>, transaction_dates: &mut Vec, -) -> Option<(String, String, f32, f32, f32)> { +) -> Option<(String, String, f32, f32, f32, Option)> { + let symbol = transaction + .next() + .unwrap() + .getstring() + .expect_and_log("Processing of Sold transaction went wrong"); let quantity = transaction .next() .unwrap() @@ -371,7 +380,14 @@ fn yield_sold_transaction( } }; - Some((trade_date, settlement_date, quantity, price, amount_sold)) + Some(( + trade_date, + settlement_date, + quantity, + price, + amount_sold, + Some(symbol), + )) } /// Recognize whether PDF document is of Brokerage Statement type (old e-trade type of PDF @@ -451,8 +467,8 @@ fn recognize_statement(page: PageRc) -> Result { fn process_transaction( interests_transactions: &mut Vec<(String, f32, f32)>, - div_transactions: &mut Vec<(String, f32, f32)>, - sold_transactions: &mut Vec<(String, String, f32, f32, f32)>, + div_transactions: &mut Vec<(String, f32, f32, Option)>, + sold_transactions: &mut Vec<(String, String, f32, f32, f32, Option)>, actual_string: &pdf::primitive::PdfString, transaction_dates: &mut Vec, processed_sequence: &mut Vec>, @@ -470,8 +486,20 @@ fn process_transaction( // attach to sequence the same string parser if pattern is not met match obj.getstring() { Some(token) => { - if obj.is_pattern() == false && token != "$" { - sequence.push_front(obj); + let support_companies = vec![ + "TREASURY LIQUIDITY FUND".to_owned(), + "INTEL CORP".to_owned(), + "ADVANCED MICRO DEVICES".to_owned(), + "INTEREST ADJUSTMENT".to_owned(), + ]; + if obj.is_pattern() == true { + if support_companies.contains(&token) == true { + processed_sequence.push(obj); + } + } else { + if token != "$" { + sequence.push_front(obj); + } } } @@ -486,6 +514,11 @@ fn process_transaction( let mut transaction = processed_sequence.iter(); match transaction_type { TransactionType::Tax => { + let symbol = transaction + .next() + .unwrap() + .getstring() + .expect_and_log("Processing of Tax transaction went wrong"); // Ok we assume here that taxation of transaction appears later in document // than actual transaction that is a subject to taxation let tax_us = transaction @@ -497,16 +530,36 @@ fn process_transaction( // Here we just go through registered transactions and pick the one where // income is higher than tax and apply tax value and where tax was not yet // applied - let subject_to_tax = div_transactions + let mut interests_as_div: Vec<( + &mut String, + &mut f32, + &mut f32, + Option, + )> = interests_transactions .iter_mut() - .chain(interests_transactions.iter_mut()) - .find(|x| x.1 > tax_us && x.2 == 0.0f32) + .map(|x| (&mut x.0, &mut x.1, &mut x.2, None)) + .collect(); + let mut div_as_ref: Vec<(&mut String, &mut f32, &mut f32, Option)> = + div_transactions + .iter_mut() + .map(|x| (&mut x.0, &mut x.1, &mut x.2, x.3.clone())) + .collect(); + + let subject_to_tax = div_as_ref + .iter_mut() + .chain(interests_as_div.iter_mut()) + .find(|x| *x.1 > tax_us && *x.2 == 0.0f32) .ok_or("Error: Unable to find transaction that was taxed")?; log::info!("Tax: {tax_us} was applied to {subject_to_tax:?}"); - subject_to_tax.2 = tax_us; + *subject_to_tax.2 = tax_us; log::info!("Completed parsing Tax transaction"); } TransactionType::Interests => { + let _symbol = transaction + .next() + .unwrap() + .getstring() + .expect_and_log("Processing of Interests transaction went wrong"); let gross_us = transaction .next() .unwrap() @@ -523,6 +576,11 @@ fn process_transaction( log::info!("Completed parsing Interests transaction"); } TransactionType::Dividends => { + let symbol = transaction + .next() + .unwrap() + .getstring() + .expect_and_log("Processing of Dividend transaction went wrong"); let gross_us = transaction .next() .unwrap() @@ -535,6 +593,7 @@ fn process_transaction( .ok_or("Error: missing transaction dates when parsing")?, gross_us, 0.0, // No tax info yet. It will be added later in Tax section + Some(symbol), )); log::info!("Completed parsing Dividend transaction"); } @@ -565,196 +624,6 @@ fn process_transaction( Ok(state) } -/// Parse borkerage statement document type -fn parse_brokerage_statement<'a, I>( - pages_iter: I, -) -> Result< - ( - Vec<(String, f32, f32)>, - Vec<(String, f32, f32)>, - Vec<(String, String, f32, f32, f32)>, - Vec<(String, String, i32, f32, f32, f32, f32, f32)>, - ), - String, -> -where - I: Iterator>, -{ - let mut div_transactions: Vec<(String, f32, f32)> = vec![]; - let mut sold_transactions: Vec<(String, String, f32, f32, f32)> = vec![]; - let mut trades: Vec<(String, String, i32, f32, f32, f32, f32, f32)> = vec![]; - let mut state = ParserState::SearchingTransactionEntry; - let mut sequence: std::collections::VecDeque> = - std::collections::VecDeque::new(); - let mut processed_sequence: Vec> = vec![]; - // Queue for transaction dates. Pop last one or last two as trade and settlement dates - let mut transaction_dates: Vec = vec![]; - - for page in pages_iter { - let page = page.unwrap(); - let contents = page.contents.as_ref().unwrap(); - for op in contents.operations.iter() { - match op.operator.as_ref() { - "TJ" => { - // Text show - if op.operands.len() > 0 { - //transaction_date = op.operands[0]; - let a = &op.operands[0]; - log::trace!("Detected PDF object: {a}"); - match a { - Primitive::Array(c) => { - for e in c { - if let Primitive::String(actual_string) = e { - match state { - ParserState::SearchingYear - | ParserState::ProcessingYear => { - log::error!("Brokerage documents do not have \"For the Period\" block!") - } - ParserState::SearchingCashFlowBlock => { - log::error!("Brokerage documents do not have cashflow block!") - } - ParserState::SearchingTransactionEntry => { - let rust_string = - actual_string.clone().into_string().unwrap(); - //println!("rust_string: {}", rust_string); - if rust_string == "Dividend" { - create_dividend_parsing_sequence(&mut sequence); - state = ParserState::ProcessingTransaction( - TransactionType::Dividends, - ); - } else if rust_string == "Sold" { - create_sold_parsing_sequence(&mut sequence); - state = ParserState::ProcessingTransaction( - TransactionType::Sold, - ); - } else if rust_string == "TYPE" { - create_trade_parsing_sequence(&mut sequence); - state = ParserState::ProcessingTransaction( - TransactionType::Trade, - ); - } else { - //if this is date then store it - if chrono::NaiveDate::parse_from_str( - &rust_string, - "%m/%d/%y", - ) - .is_ok() - { - transaction_dates.push(rust_string.clone()); - } - } - } - ParserState::ProcessingTransaction( - transaction_type, - ) => { - // So process transaction element and store it in SOLD - // or DIV - let possible_obj = sequence.pop_front(); - match possible_obj { - // Move executed parser objects into Vector - // attach only i32 and f32 elements to - // processed queue - Some(mut obj) => { - obj.parse(actual_string); - // attach to sequence the same string parser if pattern is not met - if obj.getstring().is_some() { - if obj.is_pattern() == false { - sequence.push_front(obj); - } - } else { - processed_sequence.push(obj); - } - // If sequence of expected entries is - // empty then extract data from - // processeed elements - if sequence.is_empty() { - state = - ParserState::SearchingTransactionEntry; - let mut transaction = - processed_sequence.iter(); - match transaction_type { - 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"); - div_transactions.push(( - transaction_dates.pop().expect("Error: missing transaction dates when parsing"), - gross_us, - tax_us, - )); - } - TransactionType::Sold => { - if let Some(trans_details) = - yield_sold_transaction( - &mut transaction, - &mut transaction_dates, - ) - { - sold_transactions - .push(trans_details); - } - } - TransactionType::Trade => { - let transaction_date = transaction.next().unwrap().getdate().expect("Prasing of Trade confirmation went wrong"); // quantity - let settlement_date = transaction.next().unwrap().getdate().expect("Prasing of Trade confirmation went wrong"); // quantity - transaction.next().unwrap(); // MKT?? - transaction.next().unwrap(); // CPT?? - let quantity = transaction.next().unwrap().geti32().expect("Prasing of Trade confirmation went wrong"); // quantity - let price = transaction.next().unwrap().getf32().expect("Prasing of Trade confirmation went wrong"); // price - let principal = transaction.next().unwrap().getf32().expect("Prasing of Trade confirmation went wrong"); // principal - let commission = transaction.next().unwrap().getf32().expect("Prasing of Trade confirmation went wrong"); // commission - let fee = transaction.next().unwrap().getf32().expect("Prasing of Trade confirmation went wrong"); // fee - let net = transaction.next().unwrap().getf32().expect("Prasing of Trade confirmation went wrong"); // net - trades.push(( - transaction_date, - settlement_date, - quantity, - price, - principal, - commission, - fee, - net, - )); - } - } - processed_sequence.clear(); - } else { - state = - ParserState::ProcessingTransaction( - transaction_type, - ); - } - } - - // In nothing more to be done then just extract - // parsed data from paser objects - None => { - state = ParserState::ProcessingTransaction( - transaction_type, - ); - } - } - } - } - } - } - } - _ => (), - } - } - } - _ => {} - } - } - } - Ok((vec![], div_transactions, sold_transactions, trades)) -} - fn check_if_transaction( candidate_string: &str, dates: &mut Vec, @@ -824,8 +693,8 @@ fn parse_account_statement<'a, I>( ) -> Result< ( Vec<(String, f32, f32)>, - Vec<(String, f32, f32)>, - Vec<(String, String, f32, f32, f32)>, + Vec<(String, f32, f32, Option)>, + Vec<(String, String, f32, f32, f32, Option)>, Vec<(String, String, i32, f32, f32, f32, f32, f32)>, ), String, @@ -834,8 +703,8 @@ where I: Iterator>, { let mut interests_transactions: Vec<(String, f32, f32)> = vec![]; - let mut div_transactions: Vec<(String, f32, f32)> = vec![]; - let mut sold_transactions: Vec<(String, String, f32, f32, f32)> = vec![]; + let mut div_transactions: Vec<(String, f32, f32, Option)> = vec![]; + let mut sold_transactions: Vec<(String, String, f32, f32, f32, Option)> = vec![]; let trades: Vec<(String, String, i32, f32, f32, f32, f32, f32)> = vec![]; let mut state = ParserState::SearchingYear; let mut sequence: std::collections::VecDeque> = @@ -941,16 +810,16 @@ where /// Sold stock transactions (sold_transactions) /// information on transactions in case of parsing trade document (trades) /// Dividends paid transaction is: -/// transaction date, gross_us, tax_us, +/// transaction date, gross_us, tax_us, company /// Sold stock transaction is : -/// (trade_date, settlement_date, quantity, price, amount_sold) +/// (trade_date, settlement_date, quantity, price, amount_sold, company) pub fn parse_statement( pdftoparse: &str, ) -> Result< ( Vec<(String, f32, f32)>, - Vec<(String, f32, f32)>, - Vec<(String, String, f32, f32, f32)>, + Vec<(String, f32, f32, Option)>, + Vec<(String, String, f32, f32, f32, Option)>, Vec<(String, String, i32, f32, f32, f32, f32, f32)>, ), String, @@ -978,7 +847,7 @@ pub fn parse_statement( } StatementType::BrokerageStatement => { log::info!("Processing brokerage statement PDF"); - parse_brokerage_statement(pdffile_iter)? + return Err(format!("Processing brokerage statement PDF is unsupported: document type: {pdftoparse}.To have it supported please use release 0.7.4 ")); } StatementType::AccountStatement => { log::info!("Processing Account statement PDF"); @@ -1065,10 +934,11 @@ mod tests { fn test_transaction_validation() -> Result<(), String> { let mut transaction_dates: Vec = vec!["11/29/22".to_string(), "12/01/22".to_string()]; - let mut sequence: std::collections::VecDeque> = - std::collections::VecDeque::new(); - create_sold_parsing_sequence(&mut sequence); let mut processed_sequence: Vec> = vec![]; + processed_sequence.push(Box::new(StringEntry { + val: String::new(), + patterns: vec!["INTC".to_owned(), "DLB".to_owned()], + })); // INTC, DLB processed_sequence.push(Box::new(F32Entry { val: 42.0 })); //quantity processed_sequence.push(Box::new(F32Entry { val: 28.8400 })); // Price processed_sequence.push(Box::new(F32Entry { val: 1210.83 })); // Amount Sold @@ -1085,10 +955,11 @@ mod tests { "11/29/22".to_string(), "12/01/22".to_string(), ]; - let mut sequence: std::collections::VecDeque> = - std::collections::VecDeque::new(); - create_sold_parsing_sequence(&mut sequence); let mut processed_sequence: Vec> = vec![]; + processed_sequence.push(Box::new(StringEntry { + val: String::new(), + patterns: vec!["INTC".to_owned(), "DLB".to_owned()], + })); // INTC, DLB processed_sequence.push(Box::new(F32Entry { val: 42.0 })); //quantity processed_sequence.push(Box::new(F32Entry { val: 28.8400 })); // Price processed_sequence.push(Box::new(F32Entry { val: 1210.83 })); // Amount Sold @@ -1101,10 +972,11 @@ mod tests { #[test] fn test_unsettled_transaction_validation() -> Result<(), String> { let mut transaction_dates: Vec = vec!["11/29/22".to_string()]; - let mut sequence: std::collections::VecDeque> = - std::collections::VecDeque::new(); - create_sold_parsing_sequence(&mut sequence); let mut processed_sequence: Vec> = vec![]; + processed_sequence.push(Box::new(StringEntry { + val: String::new(), + patterns: vec!["INTC".to_owned(), "DLB".to_owned()], + })); // INTC, DLB processed_sequence.push(Box::new(F32Entry { val: 42.0 })); //quantity processed_sequence.push(Box::new(F32Entry { val: 28.8400 })); // Price processed_sequence.push(Box::new(F32Entry { val: 1210.83 })); // Amount Sold @@ -1249,13 +1121,19 @@ mod tests { parse_statement("data/MS_ClientStatements_6557_202312.pdf"), (Ok(( vec![("12/1/23".to_owned(), 1.22, 0.00)], - vec![("12/1/23".to_owned(), 386.50, 57.98),], + vec![( + "12/1/23".to_owned(), + 386.50, + 57.98, + Some("INTEL CORP".to_string()) + ),], vec![( "12/21/23".to_owned(), "12/26/23".to_owned(), 82.0, 46.45, - 3808.86 + 3808.86, + Some("INTEL CORP".to_string()) )], vec![] ))) @@ -1297,8 +1175,18 @@ mod tests { ("1/2/24".to_owned(), 0.49, 0.00) ], vec![ - ("6/3/24".to_owned(), 57.25, 8.59), // Dividends date, gross, tax_us - ("3/1/24".to_owned(), 380.25, 57.04) + ( + "6/3/24".to_owned(), + 57.25, + 8.59, + Some("INTEL CORP".to_owned()) + ), // Dividends date, gross, tax_us + ( + "3/1/24".to_owned(), + 380.25, + 57.04, + Some("INTEL CORP".to_owned()) + ) ], vec![ ( @@ -1306,203 +1194,232 @@ mod tests { "12/5/24".to_owned(), 30.0, 22.5, - 674.98 + 674.98, + Some("INTEL CORP".to_string()) ), ( "12/5/24".to_owned(), "12/6/24".to_owned(), 55.0, 21.96, - 1207.76 + 1207.76, + Some("INTEL CORP".to_string()) ), ( "11/1/24".to_owned(), "11/4/24".to_owned(), 15.0, 23.32, - 349.79 + 349.79, + Some("INTEL CORP".to_string()) ), ( "9/3/24".to_owned(), "9/4/24".to_owned(), 17.0, 21.53, - 365.99 + 365.99, + Some("INTEL CORP".to_string()) ), // Sold ( "9/9/24".to_owned(), "9/10/24".to_owned(), 14.0, 18.98, - 265.71 + 265.71, + Some("INTEL CORP".to_string()) ), ( "8/5/24".to_owned(), "8/6/24".to_owned(), 14.0, 20.21, - 282.93 + 282.93, + Some("INTEL CORP".to_string()) ), ( "8/20/24".to_owned(), "8/21/24".to_owned(), 328.0, 21.0247, - 6895.89 + 6895.89, + Some("INTEL CORP".to_string()) ), ( "7/31/24".to_owned(), "8/1/24".to_owned(), 151.0, 30.44, - 4596.31 + 4596.31, + Some("INTEL CORP".to_string()) ), ( "6/3/24".to_owned(), "6/4/24".to_owned(), 14.0, 31.04, - 434.54 + 434.54, + Some("INTEL CORP".to_string()) ), ( "5/1/24".to_owned(), "5/3/24".to_owned(), 126.0, 30.14, - 3797.6 + 3797.6, + Some("INTEL CORP".to_string()) ), ( "5/1/24".to_owned(), "5/3/24".to_owned(), 124.0, 30.14, - 3737.33 + 3737.33, + Some("INTEL CORP".to_string()) ), ( "5/1/24".to_owned(), "5/3/24".to_owned(), 89.0, 30.6116, - 2724.4 + 2724.4, + Some("INTEL CORP".to_string()) ), ( "5/2/24".to_owned(), "5/6/24".to_owned(), 182.0, 30.56, - 5561.87 + 5561.87, + Some("INTEL CORP".to_string()) ), ( "5/3/24".to_owned(), "5/7/24".to_owned(), 440.0, 30.835, - 13567.29 + 13567.29, + Some("INTEL CORP".to_string()) ), ( "5/3/24".to_owned(), "5/7/24".to_owned(), 198.0, 30.835, - 6105.28 + 6105.28, + Some("INTEL CORP".to_string()) ), ( "5/3/24".to_owned(), "5/7/24".to_owned(), 146.0, 30.8603, - 4505.56 + 4505.56, + Some("INTEL CORP".to_string()) ), ( "5/3/24".to_owned(), "5/7/24".to_owned(), 145.0, 30.8626, - 4475.04 + 4475.04, + Some("INTEL CORP".to_string()) ), ( "5/3/24".to_owned(), "5/7/24".to_owned(), 75.0, 30.815, - 2311.11 + 2311.11, + Some("INTEL CORP".to_string()) ), ( "5/6/24".to_owned(), "5/8/24".to_owned(), 458.0, 31.11, - 14248.26 + 14248.26, + Some("INTEL CORP".to_string()) ), ( "5/31/24".to_owned(), "6/3/24".to_owned(), 18.0, 30.22, - 543.94 + 543.94, + Some("INTEL CORP".to_string()) ), ( "4/3/24".to_owned(), "4/5/24".to_owned(), 31.0, 40.625, - 1259.36 + 1259.36, + Some("INTEL CORP".to_string()) ), ( "4/11/24".to_owned(), "4/15/24".to_owned(), 209.0, 37.44, - 7824.89 + 7824.89, + Some("INTEL CORP".to_string()) ), ( "4/11/24".to_owned(), "4/15/24".to_owned(), 190.0, 37.44, - 7113.54 + 7113.54, + Some("INTEL CORP".to_string()) ), ( "4/16/24".to_owned(), "4/18/24".to_owned(), 310.0, 36.27, - 11243.61 + 11243.61, + Some("INTEL CORP".to_string()) ), ( "4/29/24".to_owned(), "5/1/24".to_owned(), 153.0, 31.87, - 4876.07 + 4876.07, + Some("INTEL CORP".to_string()) ), ( "4/29/24".to_owned(), "5/1/24".to_owned(), 131.0, 31.87, - 4174.93 + 4174.93, + Some("INTEL CORP".to_string()) ), ( "4/29/24".to_owned(), "5/1/24".to_owned(), 87.0, 31.87, - 2772.66 + 2772.66, + Some("INTEL CORP".to_string()) ), ( "3/11/24".to_owned(), "3/13/24".to_owned(), 38.0, 43.85, - 1666.28 + 1666.28, + Some("INTEL CORP".to_string()) ), ( "2/20/24".to_owned(), "2/22/24".to_owned(), 150.0, 43.9822, - 6597.27 + 6597.27, + Some("INTEL CORP".to_string()) ) ], vec![] @@ -1513,32 +1430,7 @@ mod tests { #[test] #[ignore] - fn test_parse_brokerage_statement() -> Result<(), String> { - assert_eq!( - parse_statement("data/example-divs.pdf"), - (Ok(( - vec![], - vec![("03/01/22".to_owned(), 698.25, 104.74)], - vec![], - vec![] - ))) - ); - assert_eq!( - parse_statement("data/example-sold-wire.pdf"), - Ok(( - vec![], - vec![], - vec![( - "05/02/22".to_owned(), - "05/04/22".to_owned(), - -1.0, - 43.69, - 43.67 - )], - vec![] - )) - ); - + fn test_parse_amd_statement() -> Result<(), String> { assert_eq!( parse_statement("data/example-sold-amd.pdf"), Ok(( @@ -1550,14 +1442,16 @@ mod tests { "11/14/23".to_owned(), 72.0, 118.13, - 8505.29 + 8505.29, + Some("ADVANCED MICRO DEVICES".to_string()) ), ( "11/22/23".to_owned(), "11/27/23".to_owned(), 162.0, 122.4511, - 19836.92 + 19836.92, + Some("ADVANCED MICRO DEVICES".to_string()) ), ], vec![] diff --git a/src/transactions.rs b/src/transactions.rs index a9b5fe4..4aebbcc 100644 --- a/src/transactions.rs +++ b/src/transactions.rs @@ -3,6 +3,8 @@ use chrono; use chrono::Datelike; +use polars::prelude::*; +use std::collections::HashMap; pub use crate::logging::ResultExt; use crate::{SoldTransaction, Transaction}; @@ -37,11 +39,11 @@ pub fn verify_interests_transactions(transactions: &Vec<(String, T, T)>) -> R /// Check if all dividends transaction come from the same year pub fn verify_dividends_transactions( - div_transactions: &Vec<(String, T, T)>, + div_transactions: &Vec<(String, T, T, Option)>, ) -> Result<(), String> { let mut trans = div_transactions.iter(); let transaction_date = match trans.next() { - Some((x, _, _)) => x, + Some((x, _, _, _)) => x, None => { log::info!("No Dividends transactions"); return Ok(()); @@ -52,7 +54,7 @@ pub fn verify_dividends_transactions( .map_err(|_| format!("Unable to parse transaction date: \"{transaction_date}\""))? .year(); let mut verification: Result<(), String> = Ok(()); - trans.try_for_each(|(tr_date, _, _)| { + trans.try_for_each(|(tr_date, _, _, _)| { let tr_year = chrono::NaiveDate::parse_from_str(tr_date, "%m/%d/%y") .map_err(|_| format!("Unable to parse transaction date: \"{tr_date}\""))? .year(); @@ -65,10 +67,12 @@ pub fn verify_dividends_transactions( verification } -pub fn verify_transactions(transactions: &Vec<(String, String, T, T)>) -> Result<(), String> { +pub fn verify_transactions( + transactions: &Vec<(String, String, T, T, Option)>, +) -> Result<(), String> { let mut trans = transactions.iter(); let transaction_date = match trans.next() { - Some((_, x, _, _)) => x, + Some((_, x, _, _, _)) => x, None => { log::info!("No revolut sold transactions"); return Ok(()); @@ -79,7 +83,7 @@ pub fn verify_transactions(transactions: &Vec<(String, String, T, T)>) -> Res .map_err(|_| format!("Unable to parse transaction date: \"{transaction_date}\""))? .year(); let mut verification: Result<(), String> = Ok(()); - trans.try_for_each(|(_, tr_date, _, _)| { + trans.try_for_each(|(_, tr_date, _, _, _)| { let tr_year = chrono::NaiveDate::parse_from_str(tr_date, "%m/%d/%y") .map_err(|_| format!("Unable to parse transaction date: \"{tr_date}\""))? .year(); @@ -97,16 +101,18 @@ pub fn verify_transactions(transactions: &Vec<(String, String, T, T)>) -> Res /// we ignore those and use net income rather than principal /// Actual Tax is to be paid from settlement_date pub fn reconstruct_sold_transactions( - sold_transactions: &Vec<(String, String, f32, f32, f32)>, + sold_transactions: &Vec<(String, String, f32, f32, f32, Option)>, gains_and_losses: &Vec<(String, String, f32, f32, f32)>, -) -> Result, String> { +) -> Result)>, String> { // Ok What do I need. // 1. trade date // 2. settlement date // 3. date of purchase // 4. gross income // 5. cost cost basis - let mut detailed_sold_transactions: Vec<(String, String, String, f32, f32)> = vec![]; + // 6. company symbol (ticker) + let mut detailed_sold_transactions: Vec<(String, String, String, f32, f32, Option)> = + vec![]; if sold_transactions.len() > 0 && gains_and_losses.is_empty() { return Err("\n\nERROR: Sold transaction detected, but corressponding Gain&Losses document is missing. Please download Gain&Losses XLSX document at:\n @@ -121,7 +127,7 @@ pub fn reconstruct_sold_transactions( let trade_date = chrono::NaiveDate::parse_from_str(&tr_date, "%m/%d/%Y") .expect_and_log(&format!("Unable to parse trade date: {tr_date}")); - let (_, settlement_date, _, _, _) = sold_transactions.iter().find(|(trade_dt, _, _, _, income)|{ + let (_, settlement_date, _, _, _, symbol) = sold_transactions.iter().find(|(trade_dt, _, _, _, income, _)|{ log::info!("Candidate Sold transaction from PDF: trade_date: {trade_dt} income: {income}"); let trade_date_pdf = chrono::NaiveDate::parse_from_str(&trade_dt, "%m/%d/%y").expect_and_log(&format!("Unable to parse trade date: {trade_dt}")); trade_date == trade_date_pdf @@ -142,6 +148,7 @@ pub fn reconstruct_sold_transactions( .to_string(), *inc, *cost_basis, + symbol.clone(), )); } @@ -149,14 +156,14 @@ pub fn reconstruct_sold_transactions( } pub fn create_detailed_revolut_transactions( - transactions: Vec<(String, crate::Currency, crate::Currency)>, + transactions: Vec<(String, crate::Currency, crate::Currency, Option)>, dates: &std::collections::HashMap>, ) -> Result, &str> { let mut detailed_transactions: Vec = Vec::new(); transactions .iter() - .try_for_each(|(transaction_date, gross, tax)| { + .try_for_each(|(transaction_date, gross, tax, company)| { let (exchange_rate_date, exchange_rate) = dates [&gross.derive_exchange(transaction_date.clone())] .clone() @@ -168,6 +175,7 @@ pub fn create_detailed_revolut_transactions( tax_paid: *tax, exchange_rate_date, exchange_rate, + company: company.clone(), }; let msg = transaction.format_to_print("REVOLUT")?; @@ -199,6 +207,7 @@ pub fn create_detailed_interests_transactions( tax_paid: crate::Currency::USD(*tax_us as f64), exchange_rate_date, exchange_rate, + company: None, // No company info when interests are paid on money }; let msg = transaction.format_to_print("INTERESTS")?; @@ -212,13 +221,13 @@ pub fn create_detailed_interests_transactions( } pub fn create_detailed_div_transactions( - transactions: Vec<(String, f32, f32)>, + transactions: Vec<(String, f32, f32, Option)>, dates: &std::collections::HashMap>, ) -> Result, &str> { let mut detailed_transactions: Vec = Vec::new(); transactions .iter() - .try_for_each(|(transaction_date, gross_us, tax_us)| { + .try_for_each(|(transaction_date, gross_us, tax_us, company)| { let (exchange_rate_date, exchange_rate) = dates [&crate::Exchange::USD(transaction_date.clone())] .clone() @@ -230,6 +239,7 @@ pub fn create_detailed_div_transactions( tax_paid: crate::Currency::USD(*tax_us as f64), exchange_rate_date, exchange_rate, + company: company.clone(), }; let msg = transaction.format_to_print("DIV")?; @@ -252,12 +262,12 @@ pub fn create_detailed_div_transactions( // pub exchange_rate_acquisition_date: String, // pub exchange_rate_acquisition: f32, pub fn create_detailed_sold_transactions( - transactions: Vec<(String, String, String, f32, f32)>, + transactions: Vec<(String, String, String, f32, f32, Option)>, dates: &std::collections::HashMap>, ) -> Result, &str> { let mut detailed_transactions: Vec = Vec::new(); transactions.iter().for_each( - |(trade_date, settlement_date, acquisition_date, income, cost_basis)| { + |(trade_date, settlement_date, acquisition_date, income, cost_basis, symbol)| { let (exchange_rate_settlement_date, exchange_rate_settlement) = dates [&crate::Exchange::USD(settlement_date.clone())] .clone() @@ -277,6 +287,7 @@ pub fn create_detailed_sold_transactions( exchange_rate_settlement, exchange_rate_acquisition_date, exchange_rate_acquisition, + company: symbol.clone(), }; let msg = transaction.format_to_print(""); @@ -291,13 +302,18 @@ pub fn create_detailed_sold_transactions( } pub fn create_detailed_revolut_sold_transactions( - transactions: Vec<(String, String, crate::Currency, crate::Currency)>, + transactions: Vec<( + String, + String, + crate::Currency, + crate::Currency, + Option, + )>, dates: &std::collections::HashMap>, ) -> Result, &str> { let mut detailed_transactions: Vec = Vec::new(); - transactions - .iter() - .for_each(|(acquired_date, sold_date, cost_basis, gross_income)| { + transactions.iter().for_each( + |(acquired_date, sold_date, cost_basis, gross_income, symbol)| { let (exchange_rate_settlement_date, exchange_rate_settlement) = dates [&gross_income.derive_exchange(sold_date.clone())] // TODO: settlement date??? .clone() @@ -317,6 +333,7 @@ pub fn create_detailed_revolut_sold_transactions( exchange_rate_settlement, exchange_rate_acquisition_date, exchange_rate_acquisition, + company: symbol.clone(), }; let msg = transaction.format_to_print("REVOLUT "); @@ -325,16 +342,280 @@ pub fn create_detailed_revolut_sold_transactions( log::info!("{}", msg); detailed_transactions.push(transaction); - }); + }, + ); Ok(detailed_transactions) } +// Make a dataframe with +pub(crate) fn create_per_company_report( + interests: &[Transaction], + dividends: &[Transaction], + sold_transactions: &[SoldTransaction], + revolut_dividends_transactions: &[Transaction], + revolut_sold_transactions: &[SoldTransaction], +) -> Result { + // Key: Company Name , Value : (gross_pl, tax_paid_in_us_pl, cost_pl) + let mut per_company_data: HashMap, (f32, f32, f32)> = HashMap::new(); + + let interests_or_dividends = interests + .iter() + .chain(dividends.iter()) + .chain(revolut_dividends_transactions.iter()); + + interests_or_dividends.for_each(|x| { + let entry = per_company_data + .entry(x.company.clone()) + .or_insert((0.0, 0.0, 0.0)); + entry.0 += x.exchange_rate * x.gross.value() as f32; + entry.1 += x.exchange_rate * x.tax_paid.value() as f32; + // No cost for dividends being paid + }); + + let sells = sold_transactions + .iter() + .chain(revolut_sold_transactions.iter()); + sells.for_each(|x| { + let entry = per_company_data + .entry(x.company.clone()) + .or_insert((0.0, 0.0, 0.0)); + entry.0 += x.income_us * x.exchange_rate_settlement; + // No tax from sold transactions + entry.2 += x.cost_basis * x.exchange_rate_acquisition; + }); + + // Convert my HashMap into DataFrame + let mut companies: Vec> = Vec::new(); + let mut gross: Vec = Vec::new(); + let mut tax: Vec = Vec::new(); + let mut cost: Vec = Vec::new(); + per_company_data + .iter() + .try_for_each(|(company, (gross_pl, tax_paid_in_us_pl, cost_pl))| { + log::info!( + "Company: {:?}, Gross PLN: {:.2}, Tax Paid in USD PLN: {:.2}, Cost PLN: {:.2}", + company, + gross_pl, + tax_paid_in_us_pl, + cost_pl + ); + companies.push(company.clone()); + gross.push(*gross_pl); + tax.push(*tax_paid_in_us_pl); + cost.push(*cost_pl); + + Ok::<(), &str>(()) + })?; + let series = vec![ + Series::new("Company", companies), + Series::new("Gross[PLN]", gross), + Series::new("Cost[PLN]", cost), + Series::new("Tax Paid in USD[PLN]", tax), + ]; + DataFrame::new(series) + .map_err(|_| "Unable to create per company report dataframe")? + .sort(["Company"], false, true) + .map_err(|_| "Unable to sort per company report dataframe") +} + #[cfg(test)] mod tests { use super::*; use crate::Currency; + fn round4(val: f64) -> f64 { + (val * 10_000.0).round() / 10_000.0 + } + + #[test] + fn test_create_per_company_report_interests() -> Result<(), String> { + let input = vec![ + Transaction { + transaction_date: "03/01/21".to_string(), + gross: crate::Currency::EUR(0.05), + tax_paid: crate::Currency::EUR(0.0), + exchange_rate_date: "02/28/21".to_string(), + exchange_rate: 2.0, + company: None, + }, + Transaction { + transaction_date: "04/11/21".to_string(), + gross: crate::Currency::EUR(0.07), + tax_paid: crate::Currency::EUR(0.0), + exchange_rate_date: "04/10/21".to_string(), + exchange_rate: 3.0, + company: None, + }, + ]; + let df = create_per_company_report(&input, &[], &[], &[], &[]) + .map_err(|e| format!("Error creating per company report: {}", e))?; + + // Interests are having company == None, and data should be folded to one row + assert_eq!(df.height(), 1); + assert_eq!(df.width(), 4); + + let company_col = df.column("Company").unwrap(); + assert_eq!(company_col.get(0).is_err(), false); // None company + let gross_col = df.column("Gross[PLN]").unwrap(); + assert_eq!( + round4(gross_col.get(0).unwrap().extract::().unwrap()), + round4(0.05 * 2.0 + 0.07 * 3.0) + ); + let cost_col = df.column("Cost[PLN]").unwrap(); + assert_eq!(cost_col.get(0).unwrap().extract::().unwrap(), 0.00); + let tax_col = df.column("Tax Paid in USD[PLN]").unwrap(); + assert_eq!(tax_col.get(0).unwrap().extract::().unwrap(), 0.00); + + Ok(()) + } + + #[test] + fn test_create_per_company_report_dividends() -> Result<(), String> { + let input = vec![ + Transaction { + transaction_date: "04/11/21".to_string(), + gross: crate::Currency::USD(100.0), + tax_paid: crate::Currency::USD(25.0), + exchange_rate_date: "04/10/21".to_string(), + exchange_rate: 3.0, + company: Some("INTEL CORP".to_owned()), + }, + Transaction { + transaction_date: "03/01/21".to_string(), + gross: crate::Currency::USD(126.0), + tax_paid: crate::Currency::USD(10.0), + exchange_rate_date: "02/28/21".to_string(), + exchange_rate: 2.0, + company: Some("INTEL CORP".to_owned()), + }, + Transaction { + transaction_date: "03/11/21".to_string(), + gross: crate::Currency::USD(100.0), + tax_paid: crate::Currency::USD(0.0), + exchange_rate_date: "02/28/21".to_string(), + exchange_rate: 10.0, + company: Some("ABEV".to_owned()), + }, + ]; + let df = create_per_company_report(&[], &input, &[], &[], &[]) + .map_err(|e| format!("Error creating per company report: {}", e))?; + + // Interests are having company == None, and data should be folded to one row + assert_eq!(df.height(), 2); + assert_eq!(df.width(), 4); + + let company_col = df.column("Company").unwrap().utf8().unwrap(); + let gross_col = df.column("Gross[PLN]").unwrap(); + let tax_col = df.column("Tax Paid in USD[PLN]").unwrap(); + let (abev_index, intc_index) = match company_col.get(0) { + Some("INTEL CORP") => (1, 0), + Some("ABEV") => (0, 1), + _ => return Err("Unexpected company name in first row".to_owned()), + }; + assert_eq!( + round4(gross_col.get(intc_index).unwrap().extract::().unwrap()), + round4(100.0 * 3.0 + 126.0 * 2.0) + ); + assert_eq!( + round4(gross_col.get(abev_index).unwrap().extract::().unwrap()), + round4(100.0 * 10.0) + ); + assert_eq!( + tax_col.get(intc_index).unwrap().extract::().unwrap(), + round4(25.0 * 3.0 + 10.0 * 2.0) + ); + assert_eq!( + tax_col.get(abev_index).unwrap().extract::().unwrap(), + round4(0.0) + ); + + let cost_col = df.column("Cost[PLN]").unwrap(); + assert_eq!(cost_col.get(0).unwrap().extract::().unwrap(), 0.00); + assert_eq!(cost_col.get(1).unwrap().extract::().unwrap(), 0.00); + + Ok(()) + } + + #[test] + fn test_create_per_company_report_sells() -> Result<(), String> { + let input = vec![ + SoldTransaction { + trade_date: "03/01/21".to_string(), + settlement_date: "03/03/21".to_string(), + acquisition_date: "01/01/21".to_string(), + income_us: 20.0, + cost_basis: 20.0, + exchange_rate_settlement_date: "03/02/21".to_string(), + exchange_rate_settlement: 2.5, + exchange_rate_acquisition_date: "02/28/21".to_string(), + exchange_rate_acquisition: 5.0, + company: Some("INTEL CORP".to_owned()), + }, + SoldTransaction { + trade_date: "06/01/21".to_string(), + settlement_date: "06/03/21".to_string(), + acquisition_date: "01/01/19".to_string(), + income_us: 25.0, + cost_basis: 10.0, + exchange_rate_settlement_date: "06/05/21".to_string(), + exchange_rate_settlement: 4.0, + exchange_rate_acquisition_date: "12/30/18".to_string(), + exchange_rate_acquisition: 6.0, + company: Some("INTEL CORP".to_owned()), + }, + SoldTransaction { + trade_date: "06/01/21".to_string(), + settlement_date: "06/03/21".to_string(), + acquisition_date: "01/01/19".to_string(), + income_us: 20.0, + cost_basis: 0.0, + exchange_rate_settlement_date: "06/05/21".to_string(), + exchange_rate_settlement: 4.0, + exchange_rate_acquisition_date: "12/30/18".to_string(), + exchange_rate_acquisition: 6.0, + company: Some("PXD".to_owned()), + }, + ]; + let df = create_per_company_report(&[], &[], &input, &[], &[]) + .map_err(|e| format!("Error creating per company report: {}", e))?; + + // Solds are having company + assert_eq!(df.height(), 2); + assert_eq!(df.width(), 4); + + let company_col = df.column("Company").unwrap().utf8().unwrap(); + let gross_col = df.column("Gross[PLN]").unwrap(); + let cost_col = df.column("Cost[PLN]").unwrap(); + let (abev_index, intc_index) = match company_col.get(0) { + Some("INTEL CORP") => (1, 0), + Some("PXD") => (0, 1), + _ => return Err("Unexpected company name in first row".to_owned()), + }; + assert_eq!( + round4(gross_col.get(intc_index).unwrap().extract::().unwrap()), + round4(20.0 * 2.5 + 25.0 * 4.0) + ); + assert_eq!( + round4(gross_col.get(abev_index).unwrap().extract::().unwrap()), + round4(20.0 * 4.0) + ); + assert_eq!( + cost_col.get(intc_index).unwrap().extract::().unwrap(), + round4(20.0 * 5.0 + 10.0 * 6.0) + ); + assert_eq!( + cost_col.get(abev_index).unwrap().extract::().unwrap(), + round4(0.0) + ); + + let tax_col = df.column("Tax Paid in USD[PLN]").unwrap(); + assert_eq!(tax_col.get(0).unwrap().extract::().unwrap(), 0.00); + assert_eq!(tax_col.get(1).unwrap().extract::().unwrap(), 0.00); + + Ok(()) + } + #[test] fn test_interests_verification_ok() -> Result<(), String> { let transactions: Vec<(String, f32, f32)> = vec![ @@ -346,18 +627,20 @@ mod tests { #[test] fn test_revolut_sold_verification_false() -> Result<(), String> { - let transactions: Vec<(String, String, Currency, Currency)> = vec![ + let transactions: Vec<(String, String, Currency, Currency, Option)> = vec![ ( "06/01/21".to_string(), "06/01/22".to_string(), Currency::PLN(10.0), Currency::PLN(2.0), + Some("INTEL CORP".to_owned()), ), ( "06/01/21".to_string(), "07/04/23".to_string(), Currency::PLN(10.0), Currency::PLN(2.0), + Some("INTEL CORP".to_owned()), ), ]; assert_eq!( @@ -369,25 +652,37 @@ mod tests { #[test] fn test_dividends_verification_ok() -> Result<(), String> { - let transactions: Vec<(String, f32, f32)> = vec![ - ("06/01/21".to_string(), 100.0, 25.0), - ("03/01/21".to_string(), 126.0, 10.0), + let transactions: Vec<(String, f32, f32, Option)> = vec![ + ( + "06/01/21".to_string(), + 100.0, + 25.0, + Some("INTEL CORP".to_owned()), + ), + ( + "03/01/21".to_string(), + 126.0, + 10.0, + Some("INTEL CORP".to_owned()), + ), ]; verify_dividends_transactions(&transactions) } #[test] fn test_dividends_verification_false() -> Result<(), String> { - let transactions: Vec<(String, Currency, Currency)> = vec![ + let transactions: Vec<(String, Currency, Currency, Option)> = vec![ ( "06/01/21".to_string(), Currency::PLN(10.0), Currency::PLN(2.0), + Some("INTEL CORP".to_owned()), ), ( "03/01/22".to_string(), Currency::PLN(126.0), Currency::PLN(10.0), + Some("INTEL CORP".to_owned()), ), ]; assert_eq!( @@ -404,11 +699,13 @@ mod tests { "03/01/21".to_owned(), crate::Currency::EUR(0.05), crate::Currency::EUR(0.00), + None, ), ( "04/11/21".to_owned(), crate::Currency::EUR(0.07), crate::Currency::EUR(0.00), + None, ), ]; @@ -435,6 +732,7 @@ mod tests { tax_paid: crate::Currency::EUR(0.0), exchange_rate_date: "02/28/21".to_string(), exchange_rate: 2.0, + company: None, }, Transaction { transaction_date: "04/11/21".to_string(), @@ -442,6 +740,7 @@ mod tests { tax_paid: crate::Currency::EUR(0.0), exchange_rate_date: "04/10/21".to_string(), exchange_rate: 3.0, + company: None, }, ]) ); @@ -455,11 +754,13 @@ mod tests { "03/01/21".to_owned(), crate::Currency::PLN(0.44), crate::Currency::PLN(0.00), + None, ), ( "04/11/21".to_owned(), crate::Currency::PLN(0.45), crate::Currency::PLN(0.00), + None, ), ]; @@ -486,6 +787,7 @@ mod tests { tax_paid: crate::Currency::PLN(0.0), exchange_rate_date: "N/A".to_string(), exchange_rate: 1.0, + company: None, }, Transaction { transaction_date: "04/11/21".to_string(), @@ -493,6 +795,7 @@ mod tests { tax_paid: crate::Currency::PLN(0.0), exchange_rate_date: "N/A".to_string(), exchange_rate: 1.0, + company: None, }, ]) ); @@ -529,6 +832,7 @@ mod tests { tax_paid: crate::Currency::USD(0.0), exchange_rate_date: "04/10/21".to_string(), exchange_rate: 3.0, + company: None, }, Transaction { transaction_date: "03/01/21".to_string(), @@ -536,6 +840,7 @@ mod tests { tax_paid: crate::Currency::USD(0.0), exchange_rate_date: "02/28/21".to_string(), exchange_rate: 2.0, + company: None, }, ]) ); @@ -544,9 +849,19 @@ mod tests { #[test] fn test_create_detailed_div_transactions() -> Result<(), String> { - let parsed_transactions: Vec<(String, f32, f32)> = vec![ - ("04/11/21".to_string(), 100.0, 25.0), - ("03/01/21".to_string(), 126.0, 10.0), + let parsed_transactions: Vec<(String, f32, f32, Option)> = vec![ + ( + "04/11/21".to_string(), + 100.0, + 25.0, + Some("INTEL CORP".to_owned()), + ), + ( + "03/01/21".to_string(), + 126.0, + 10.0, + Some("INTEL CORP".to_owned()), + ), ]; let mut dates: std::collections::HashMap> = @@ -572,6 +887,7 @@ mod tests { tax_paid: crate::Currency::USD(25.0), exchange_rate_date: "04/10/21".to_string(), exchange_rate: 3.0, + company: Some("INTEL CORP".to_owned()) }, Transaction { transaction_date: "03/01/21".to_string(), @@ -579,6 +895,7 @@ mod tests { tax_paid: crate::Currency::USD(10.0), exchange_rate_date: "02/28/21".to_string(), exchange_rate: 2.0, + company: Some("INTEL CORP".to_owned()) }, ]) ); @@ -587,12 +904,14 @@ mod tests { #[test] fn test_create_detailed_revolut_sold_transactions() -> Result<(), String> { - let parsed_transactions: Vec<(String, String, Currency, Currency)> = vec![( - "11/20/23".to_string(), - "12/08/24".to_string(), - Currency::USD(5000.0), - Currency::USD(5804.62), - )]; + let parsed_transactions: Vec<(String, String, Currency, Currency, Option)> = + vec![( + "11/20/23".to_string(), + "12/08/24".to_string(), + Currency::USD(5000.0), + Currency::USD(5804.62), + Some("INTEL CORP".to_owned()), + )]; let mut dates: std::collections::HashMap> = std::collections::HashMap::new(); @@ -620,6 +939,7 @@ mod tests { exchange_rate_settlement: 3.0, exchange_rate_acquisition_date: "11/19/23".to_string(), exchange_rate_acquisition: 2.0, + company: Some("INTEL CORP".to_owned()), },]) ); Ok(()) @@ -627,13 +947,14 @@ mod tests { #[test] fn test_create_detailed_sold_transactions() -> Result<(), String> { - let parsed_transactions: Vec<(String, String, String, f32, f32)> = vec![ + let parsed_transactions: Vec<(String, String, String, f32, f32, Option)> = vec![ ( "03/01/21".to_string(), "03/03/21".to_string(), "01/01/21".to_string(), 20.0, 20.0, + Some("INTEL CORP".to_owned()), ), ( "06/01/21".to_string(), @@ -641,6 +962,7 @@ mod tests { "01/01/19".to_string(), 25.0, 10.0, + Some("INTEL CORP".to_owned()), ), ]; @@ -695,6 +1017,7 @@ mod tests { exchange_rate_settlement: 2.5, exchange_rate_acquisition_date: "02/28/21".to_string(), exchange_rate_acquisition: 5.0, + company: Some("INTEL CORP".to_owned()), }, SoldTransaction { trade_date: "06/01/21".to_string(), @@ -706,6 +1029,7 @@ mod tests { exchange_rate_settlement: 4.0, exchange_rate_acquisition_date: "12/30/18".to_string(), exchange_rate_acquisition: 6.0, + company: Some("INTEL CORP".to_owned()), }, ]) ); @@ -714,15 +1038,25 @@ mod tests { #[test] fn test_dividends_verification_empty_ok() -> Result<(), String> { - let transactions: Vec<(String, f32, f32)> = vec![]; + let transactions: Vec<(String, f32, f32, Option)> = vec![]; verify_dividends_transactions(&transactions) } #[test] fn test_dividends_verification_fail() -> Result<(), String> { - let transactions: Vec<(String, f32, f32)> = vec![ - ("04/11/22".to_string(), 100.0, 25.0), - ("03/01/21".to_string(), 126.0, 10.0), + let transactions: Vec<(String, f32, f32, Option)> = vec![ + ( + "04/11/22".to_string(), + 100.0, + 25.0, + Some("INTEL CORP".to_owned()), + ), + ( + "03/01/21".to_string(), + 126.0, + 10.0, + Some("INTEL CORP".to_owned()), + ), ]; assert!(verify_dividends_transactions(&transactions).is_err()); Ok(()) @@ -730,7 +1064,7 @@ mod tests { #[test] fn test_sold_transaction_reconstruction_dividiends_only() -> Result<(), String> { - let parsed_sold_transactions: Vec<(String, String, f32, f32, f32)> = vec![]; + let parsed_sold_transactions: Vec<(String, String, f32, f32, f32, Option)> = vec![]; let parsed_gains_and_losses: Vec<(String, String, f32, f32, f32)> = vec![]; @@ -747,13 +1081,14 @@ mod tests { #[test] fn test_sold_transaction_reconstruction_ok() -> Result<(), String> { - let parsed_sold_transactions: Vec<(String, String, f32, f32, f32)> = vec![ + let parsed_sold_transactions: Vec<(String, String, f32, f32, f32, Option)> = vec![ ( "06/01/21".to_string(), "06/03/21".to_string(), 1.0, 25.0, 24.8, + Some("INTEL CORP".to_owned()), ), ( "03/01/21".to_string(), @@ -761,6 +1096,7 @@ mod tests { 2.0, 10.0, 19.8, + Some("INTEL CORP".to_owned()), ), ]; @@ -797,14 +1133,16 @@ mod tests { "06/03/21".to_string(), "01/01/19".to_string(), 24.8, - 10.0 + 10.0, + Some("INTEL CORP".to_owned()) ), ( "03/01/21".to_string(), "03/03/21".to_string(), "01/01/21".to_string(), 19.8, - 20.0 + 20.0, + Some("INTEL CORP".to_owned()) ), ] ); @@ -813,9 +1151,23 @@ mod tests { #[test] fn test_sold_transaction_reconstruction_single_digits_ok() -> Result<(), String> { - let parsed_sold_transactions: Vec<(String, String, f32, f32, f32)> = vec![ - ("6/1/21".to_string(), "6/3/21".to_string(), 1.0, 25.0, 24.8), - ("3/1/21".to_string(), "3/3/21".to_string(), 2.0, 10.0, 19.8), + let parsed_sold_transactions: Vec<(String, String, f32, f32, f32, Option)> = vec![ + ( + "6/1/21".to_string(), + "6/3/21".to_string(), + 1.0, + 25.0, + 24.8, + Some("INTEL CORP".to_owned()), + ), + ( + "3/1/21".to_string(), + "3/3/21".to_string(), + 2.0, + 10.0, + 19.8, + Some("INTEL CORP".to_owned()), + ), ]; let parsed_gains_and_losses: Vec<(String, String, f32, f32, f32)> = vec![ @@ -851,14 +1203,16 @@ mod tests { "6/3/21".to_string(), "01/01/19".to_string(), 24.8, - 10.0 + 10.0, + Some("INTEL CORP".to_owned()) ), ( "03/01/21".to_string(), "3/3/21".to_string(), "01/01/21".to_string(), 19.8, - 20.0 + 20.0, + Some("INTEL CORP".to_owned()) ), ] ); @@ -867,13 +1221,15 @@ mod tests { #[test] fn test_sold_transaction_reconstruction_second_fail() { - let parsed_sold_transactions: Vec<(String, String, f32, f32, f32)> = vec![( - "11/07/22".to_string(), // trade date - "11/09/22".to_string(), // settlement date - 173.0, // quantity - 28.2035, // price - 4877.36, // amount sold - )]; + let parsed_sold_transactions: Vec<(String, String, f32, f32, f32, Option)> = + vec![( + "11/07/22".to_string(), // trade date + "11/09/22".to_string(), // settlement date + 173.0, // quantity + 28.2035, // price + 4877.36, // amount sold + Some("INTEL CORP".to_owned()), // company symbol (ticker) + )]; let parsed_gains_and_losses: Vec<(String, String, f32, f32, f32)> = vec![ ( @@ -908,13 +1264,14 @@ mod tests { #[test] fn test_sold_transaction_reconstruction_multistock() -> Result<(), String> { - let parsed_sold_transactions: Vec<(String, String, f32, f32, f32)> = vec![ + let parsed_sold_transactions: Vec<(String, String, f32, f32, f32, Option)> = vec![ ( "12/21/22".to_string(), "12/23/22".to_string(), 163.0, 26.5900, 4332.44, + Some("INTEL CORP".to_owned()), ), ( "12/19/22".to_string(), @@ -922,6 +1279,7 @@ mod tests { 252.0, 26.5900, 6698.00, + Some("INTEL CORP".to_owned()), ), ]; @@ -968,6 +1326,7 @@ mod tests { "08/19/21".to_string(), 2711.0954, 4336.4874, + Some("INTEL CORP".to_owned()) ), ( "12/21/22".to_string(), @@ -975,6 +1334,7 @@ mod tests { "05/03/21".to_string(), 2046.61285, 0.0, + Some("INTEL CORP".to_owned()) ), ( "12/19/22".to_string(), @@ -982,6 +1342,7 @@ mod tests { "08/19/22".to_string(), 3986.9048, 5045.6257, + Some("INTEL CORP".to_owned()) ), ( "12/21/22".to_string(), @@ -989,6 +1350,7 @@ mod tests { "05/02/22".to_string(), 2285.82733, 0.0, + Some("INTEL CORP".to_owned()) ), ] ); @@ -997,13 +1359,14 @@ mod tests { #[test] fn test_sold_transaction_reconstruction_no_gains_fail() { - let parsed_sold_transactions: Vec<(String, String, f32, f32, f32)> = vec![ + let parsed_sold_transactions: Vec<(String, String, f32, f32, f32, Option)> = vec![ ( "06/01/21".to_string(), "06/03/21".to_string(), 1.0, 25.0, 24.8, + Some("INTEL CORP".to_owned()), ), ( "03/01/21".to_string(), @@ -1011,6 +1374,7 @@ mod tests { 2.0, 10.0, 19.8, + Some("INTEL CORP".to_owned()), ), ];