From 22949ae309335172a68089dbc585c5508cd6306a Mon Sep 17 00:00:00 2001 From: Jacek Czaja Date: Thu, 1 Jan 2026 17:34:12 +0100 Subject: [PATCH 01/20] nit compile --- src/lib.rs | 10 +++++++--- src/main.rs | 1 + src/pdfparser.rs | 29 ++++++++++++++++------------- src/transactions.rs | 11 ++++++++--- 4 files changed, 32 insertions(+), 19 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 874e6a6..e576b23 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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, + // TODO + //pub country : Option, + //pub company : Option, } impl SoldTransaction { @@ -371,11 +375,11 @@ pub fn run_taxation( ) -> 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_interests_transactions: Vec<(String, f32, f32, Option)> = vec![]; + let mut parsed_div_transactions: Vec<(String, f32, f32, Option)> = 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![]; - let mut parsed_revolut_dividends_transactions: Vec<(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)> = vec![]; // 1. Parse PDF,XLSX and CSV documents to get list of transactions diff --git a/src/main.rs b/src/main.rs index 7dbb99f..b9a34d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,7 @@ use etradeTaxReturnHelper::run_taxation; use etradeTaxReturnHelper::TaxCalculationResult; use logging::ResultExt; +// TODO: Extend structure of TaxCalculationResult with country and company // 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 diff --git a/src/pdfparser.rs b/src/pdfparser.rs index 79c95b5..3a8e585 100644 --- a/src/pdfparser.rs +++ b/src/pdfparser.rs @@ -450,8 +450,8 @@ fn recognize_statement(page: PageRc) -> Result { } fn process_transaction( - interests_transactions: &mut Vec<(String, f32, f32)>, - div_transactions: &mut Vec<(String, f32, f32)>, + interests_transactions: &mut Vec<(String, f32, f32,Option)>, + div_transactions: &mut Vec<(String, f32, f32, Option)>, sold_transactions: &mut Vec<(String, String, f32, f32, f32)>, actual_string: &pdf::primitive::PdfString, transaction_dates: &mut Vec, @@ -519,6 +519,7 @@ fn process_transaction( .ok_or("Error: missing transaction dates when parsing")?, gross_us, 0.0, // No tax info yet. It may be added later in Tax section + None, )); log::info!("Completed parsing Interests transaction"); } @@ -535,6 +536,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("KUPA".to_string()), )); log::info!("Completed parsing Dividend transaction"); } @@ -570,8 +572,8 @@ fn parse_brokerage_statement<'a, I>( pages_iter: I, ) -> Result< ( - Vec<(String, f32, f32)>, - Vec<(String, f32, f32)>, + Vec<(String, f32, f32, Option)>, + Vec<(String, f32, f32, Option)>, Vec<(String, String, f32, f32, f32)>, Vec<(String, String, i32, f32, f32, f32, f32, f32)>, ), @@ -580,7 +582,7 @@ fn parse_brokerage_statement<'a, I>( where I: Iterator>, { - let mut div_transactions: Vec<(String, f32, f32)> = vec![]; + let mut div_transactions: Vec<(String, f32, f32, Option)> = 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; @@ -686,6 +688,7 @@ where transaction_dates.pop().expect("Error: missing transaction dates when parsing"), gross_us, tax_us, + None, )); } TransactionType::Sold => { @@ -823,8 +826,8 @@ fn parse_account_statement<'a, I>( pages_iter: I, ) -> Result< ( - Vec<(String, f32, f32)>, - Vec<(String, f32, f32)>, + Vec<(String, f32, f32, Option)>, + Vec<(String, f32, f32, Option)>, Vec<(String, String, f32, f32, f32)>, Vec<(String, String, i32, f32, f32, f32, f32, f32)>, ), @@ -833,8 +836,8 @@ fn parse_account_statement<'a, I>( where I: Iterator>, { - let mut interests_transactions: Vec<(String, f32, f32)> = vec![]; - let mut div_transactions: Vec<(String, f32, f32)> = vec![]; + let mut interests_transactions: Vec<(String, f32, f32, Option)> = vec![]; + let mut div_transactions: Vec<(String, f32, f32, Option)> = vec![]; let mut sold_transactions: Vec<(String, String, f32, f32, f32)> = vec![]; let trades: Vec<(String, String, i32, f32, f32, f32, f32, f32)> = vec![]; let mut state = ParserState::SearchingYear; @@ -941,15 +944,15 @@ 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, f32, f32, Option)>, + Vec<(String, f32, f32, Option)>, Vec<(String, String, f32, f32, f32)>, Vec<(String, String, i32, f32, f32, f32, f32, f32)>, ), diff --git a/src/transactions.rs b/src/transactions.rs index a9b5fe4..ca570cb 100644 --- a/src/transactions.rs +++ b/src/transactions.rs @@ -37,11 +37,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 +52,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(); @@ -168,6 +168,7 @@ pub fn create_detailed_revolut_transactions( tax_paid: *tax, exchange_rate_date, exchange_rate, + company : Some("KUPA REVOLUTA".to_string()) }; let msg = transaction.format_to_print("REVOLUT")?; @@ -199,6 +200,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")?; @@ -230,6 +232,7 @@ pub fn create_detailed_div_transactions( tax_paid: crate::Currency::USD(*tax_us as f64), exchange_rate_date, exchange_rate, + company : Some("KUPA DIVIDENDOWA".to_string()) }; let msg = transaction.format_to_print("DIV")?; @@ -529,6 +532,7 @@ mod tests { tax_paid: crate::Currency::USD(0.0), exchange_rate_date: "04/10/21".to_string(), exchange_rate: 3.0, + None, }, Transaction { transaction_date: "03/01/21".to_string(), @@ -536,6 +540,7 @@ mod tests { tax_paid: crate::Currency::USD(0.0), exchange_rate_date: "02/28/21".to_string(), exchange_rate: 2.0, + None, }, ]) ); From 9f33752ae2317bc31e18a081a4018721cfa491db Mon Sep 17 00:00:00 2001 From: Jacek Czaja Date: Mon, 5 Jan 2026 07:40:19 +0100 Subject: [PATCH 02/20] - some more not working code --- src/csvparser.rs | 62 ++++++++++++++++++++++++++++++++++++++++++------ src/main.rs | 2 ++ 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/csvparser.rs b/src/csvparser.rs index 3049e92..8b10fe3 100644 --- a/src/csvparser.rs +++ b/src/csvparser.rs @@ -32,11 +32,12 @@ 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 dividend_transactions: Vec<(String, crate::Currency, crate::Currency, Option)>, pub sold_transactions: Vec<(String, String, crate::Currency, crate::Currency)>, pub crypto_transactions: Vec<(String, String, crate::Currency, crate::Currency)>, } @@ -115,10 +116,11 @@ 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", ]) @@ -223,6 +225,31 @@ 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(s.to_string()); + Ok::<(), &str>(()) + } else { + Err("Error: Missing Ticker Symbol") + } + })?; + + Ok(symbols) +} + + fn parse_investment_transaction_dates( df: &DataFrame, col_name: &str, @@ -403,6 +430,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,12 +476,12 @@ 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 dividend_transactions: Vec<(String, crate::Currency, crate::Currency, Option)> = vec![]; let mut sold_transactions: Vec<(String, String, crate::Currency, crate::Currency)> = vec![]; let mut crypto_transactions: Vec<(String, String, crate::Currency, crate::Currency)> = vec![]; @@ -669,9 +698,9 @@ pub fn parse_revolut_transactions(csvtoparse: &str) -> 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| 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"]; diff --git a/src/main.rs b/src/main.rs index b9a34d3..4b67746 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,8 @@ use etradeTaxReturnHelper::run_taxation; use etradeTaxReturnHelper::TaxCalculationResult; use logging::ResultExt; +// TODO: UT for csvparser::parse_symbols +// TODO: check if Tax from Terna company taken by IT goverment was taken into account // TODO: Extend structure of TaxCalculationResult with country and company // TODO: Make parsing of PDF start from first page not second so then reproduction of problem // require one page not two From ef8d707889c0768fc057969ecaab02ce422df632 Mon Sep 17 00:00:00 2001 From: Jacek Czaja Date: Mon, 5 Jan 2026 08:11:52 +0100 Subject: [PATCH 03/20] - compiles --- src/lib.rs | 6 +++--- src/pdfparser.rs | 18 +++++++++++------- src/transactions.rs | 12 ++++++------ 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e576b23..6df9c5d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -375,7 +375,7 @@ pub fn run_taxation( ) -> Result { validate_file_names(&names)?; - let mut parsed_interests_transactions: Vec<(String, f32, f32, Option)> = vec![]; + let mut parsed_interests_transactions: Vec<(String, 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)> = vec![]; let mut parsed_gain_and_losses: Vec<(String, String, f32, f32, f32)> = vec![]; @@ -436,7 +436,7 @@ 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); @@ -460,7 +460,7 @@ 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); diff --git a/src/pdfparser.rs b/src/pdfparser.rs index 3a8e585..22d6d07 100644 --- a/src/pdfparser.rs +++ b/src/pdfparser.rs @@ -450,7 +450,7 @@ fn recognize_statement(page: PageRc) -> Result { } fn process_transaction( - interests_transactions: &mut Vec<(String, f32, f32,Option)>, + interests_transactions: &mut Vec<(String, f32, f32)>, div_transactions: &mut Vec<(String, f32, f32, Option)>, sold_transactions: &mut Vec<(String, String, f32, f32, f32)>, actual_string: &pdf::primitive::PdfString, @@ -497,9 +497,14 @@ 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 mut interests_as_div: Vec<(String, f32, f32, Option)> = interests_transactions + .iter_mut() + .map(|x| (x.0.clone(), x.1, x.2, None)) + .collect(); + let subject_to_tax = div_transactions .iter_mut() - .chain(interests_transactions.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:?}"); @@ -519,7 +524,6 @@ fn process_transaction( .ok_or("Error: missing transaction dates when parsing")?, gross_us, 0.0, // No tax info yet. It may be added later in Tax section - None, )); log::info!("Completed parsing Interests transaction"); } @@ -572,7 +576,7 @@ fn parse_brokerage_statement<'a, I>( pages_iter: I, ) -> Result< ( - Vec<(String, f32, f32, Option)>, + Vec<(String, f32, f32)>, Vec<(String, f32, f32, Option)>, Vec<(String, String, f32, f32, f32)>, Vec<(String, String, i32, f32, f32, f32, f32, f32)>, @@ -826,7 +830,7 @@ fn parse_account_statement<'a, I>( pages_iter: I, ) -> Result< ( - Vec<(String, f32, f32, Option)>, + Vec<(String, f32, f32)>, Vec<(String, f32, f32, Option)>, Vec<(String, String, f32, f32, f32)>, Vec<(String, String, i32, f32, f32, f32, f32, f32)>, @@ -836,7 +840,7 @@ fn parse_account_statement<'a, I>( where I: Iterator>, { - let mut interests_transactions: Vec<(String, f32, f32, Option)> = vec![]; + let mut interests_transactions: Vec<(String, f32, f32)> = vec![]; let mut div_transactions: Vec<(String, f32, f32, Option)> = vec![]; let mut sold_transactions: Vec<(String, String, f32, f32, f32)> = vec![]; let trades: Vec<(String, String, i32, f32, f32, f32, f32, f32)> = vec![]; @@ -951,7 +955,7 @@ pub fn parse_statement( pdftoparse: &str, ) -> Result< ( - Vec<(String, f32, f32, Option)>, + Vec<(String, f32, f32)>, Vec<(String, f32, f32, Option)>, Vec<(String, String, f32, f32, f32)>, Vec<(String, String, i32, f32, f32, f32, f32, f32)>, diff --git a/src/transactions.rs b/src/transactions.rs index ca570cb..7dbb401 100644 --- a/src/transactions.rs +++ b/src/transactions.rs @@ -149,14 +149,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,7 +168,7 @@ pub fn create_detailed_revolut_transactions( tax_paid: *tax, exchange_rate_date, exchange_rate, - company : Some("KUPA REVOLUTA".to_string()) + company : company.clone() }; let msg = transaction.format_to_print("REVOLUT")?; @@ -214,13 +214,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() @@ -232,7 +232,7 @@ pub fn create_detailed_div_transactions( tax_paid: crate::Currency::USD(*tax_us as f64), exchange_rate_date, exchange_rate, - company : Some("KUPA DIVIDENDOWA".to_string()) + company: company.clone() }; let msg = transaction.format_to_print("DIV")?; From 259de5ceff9cac12127210db5949592ee9289489 Mon Sep 17 00:00:00 2001 From: Jacek Czaja Date: Mon, 5 Jan 2026 08:24:57 +0100 Subject: [PATCH 04/20] - in a process of fixing UT --- src/csvparser.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/csvparser.rs b/src/csvparser.rs index 8b10fe3..e6bec11 100644 --- a/src/csvparser.rs +++ b/src/csvparser.rs @@ -1027,7 +1027,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, _ => (), From b1859c9185309042b1ccfd4a2c2ef377d5182b29 Mon Sep 17 00:00:00 2001 From: Jacek Czaja Date: Wed, 7 Jan 2026 10:59:15 +0100 Subject: [PATCH 05/20] - Tests buildable just not working --- src/csvparser.rs | 67 +++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 7 +++++ src/main.rs | 2 +- src/pdfparser.rs | 8 +++--- src/transactions.rs | 38 ++++++++++++++++--------- 5 files changed, 104 insertions(+), 18 deletions(-) diff --git a/src/csvparser.rs b/src/csvparser.rs index e6bec11..9e8f87b 100644 --- a/src/csvparser.rs +++ b/src/csvparser.rs @@ -1064,16 +1064,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![], @@ -1095,58 +1098,71 @@ 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()) ), ], + // TODO: symbols sold_transactions: vec![ ( "07/29/24".to_owned(), "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(), @@ -1190,21 +1206,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![], @@ -1227,46 +1247,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![], @@ -1289,81 +1318,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![( @@ -1393,96 +1438,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![], @@ -1504,16 +1568,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/lib.rs b/src/lib.rs index 6df9c5d..60a03d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -581,6 +581,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(()) @@ -596,6 +597,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(), @@ -603,6 +605,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!( @@ -620,6 +623,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(), @@ -627,6 +631,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!( @@ -645,6 +650,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(), @@ -652,6 +658,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!( diff --git a/src/main.rs b/src/main.rs index 4b67746..a620fe9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,7 @@ use etradeTaxReturnHelper::run_taxation; use etradeTaxReturnHelper::TaxCalculationResult; use logging::ResultExt; -// TODO: UT for csvparser::parse_symbols +// TODO: Fixing UT. Why revolut transaction does have only dividend_transactions not interests? // TODO: check if Tax from Terna company taken by IT goverment was taken into account // TODO: Extend structure of TaxCalculationResult with country and company // TODO: Make parsing of PDF start from first page not second so then reproduction of problem diff --git a/src/pdfparser.rs b/src/pdfparser.rs index 22d6d07..df5f4b0 100644 --- a/src/pdfparser.rs +++ b/src/pdfparser.rs @@ -1256,7 +1256,7 @@ 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(), @@ -1304,8 +1304,8 @@ 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![ ( @@ -1525,7 +1525,7 @@ mod tests { parse_statement("data/example-divs.pdf"), (Ok(( vec![], - vec![("03/01/22".to_owned(), 698.25, 104.74)], + vec![("03/01/22".to_owned(), 698.25, 104.74, Some("INTC".to_owned()))], vec![], vec![] ))) diff --git a/src/transactions.rs b/src/transactions.rs index 7dbb401..1467195 100644 --- a/src/transactions.rs +++ b/src/transactions.rs @@ -372,25 +372,27 @@ 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!( @@ -407,11 +409,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, ), ]; @@ -438,6 +442,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(), @@ -445,6 +450,7 @@ mod tests { tax_paid: crate::Currency::EUR(0.0), exchange_rate_date: "04/10/21".to_string(), exchange_rate: 3.0, + company : None, }, ]) ); @@ -458,11 +464,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, ), ]; @@ -489,6 +497,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(), @@ -496,6 +505,7 @@ mod tests { tax_paid: crate::Currency::PLN(0.0), exchange_rate_date: "N/A".to_string(), exchange_rate: 1.0, + company : None, }, ]) ); @@ -532,7 +542,7 @@ mod tests { tax_paid: crate::Currency::USD(0.0), exchange_rate_date: "04/10/21".to_string(), exchange_rate: 3.0, - None, + company : None, }, Transaction { transaction_date: "03/01/21".to_string(), @@ -540,7 +550,7 @@ mod tests { tax_paid: crate::Currency::USD(0.0), exchange_rate_date: "02/28/21".to_string(), exchange_rate: 2.0, - None, + company : None, }, ]) ); @@ -549,9 +559,9 @@ 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> = @@ -577,6 +587,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(), @@ -584,6 +595,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()) }, ]) ); @@ -719,15 +731,15 @@ 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(()) From 63c21c10cecd7dab677612a1d2dfd9a391e38685 Mon Sep 17 00:00:00 2001 From: Jacek Czaja Date: Wed, 7 Jan 2026 13:52:22 +0100 Subject: [PATCH 06/20] - Fixed one unit test --- src/csvparser.rs | 133 +++++++++++++++++++++++++---------------------- 1 file changed, 72 insertions(+), 61 deletions(-) diff --git a/src/csvparser.rs b/src/csvparser.rs index 9e8f87b..31febcc 100644 --- a/src/csvparser.rs +++ b/src/csvparser.rs @@ -32,7 +32,7 @@ struct TransactionAccumulator { pub dates: Vec, pub incomes: Vec, pub taxes: Vec, - pub symbols: Vec, + pub symbols: Vec>, } #[derive(Debug, PartialEq)] @@ -116,7 +116,13 @@ 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", "Symbol", "Gross amount", "Withholding tax", "Currency"]) + df.select([ + "Date", + "Symbol", + "Gross amount", + "Withholding tax", + "Currency", + ]) } else { df.select([ "Date", @@ -225,10 +231,7 @@ fn extract_intrest_rate_transactions(df: &DataFrame) -> Result Result, &'static str> { +fn parse_symbols(df: &DataFrame, col_name: &str) -> Result, &'static str> { let symbol = df .column(col_name) .map_err(|_| "Error: Unable to select Symbol")?; @@ -237,19 +240,18 @@ fn parse_symbols( .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(s.to_string()); - Ok::<(), &str>(()) - } else { - Err("Error: Missing Ticker Symbol") - } + possible_symbols.into_iter().try_for_each(|maybe_symbol| { + if let Some(s) = maybe_symbol { + symbols.push(s.to_string()); + Ok::<(), &str>(()) + } else { + Err("Error: Missing Ticker Symbol") + } })?; Ok(symbols) } - fn parse_investment_transaction_dates( df: &DataFrame, col_name: &str, @@ -382,6 +384,8 @@ fn process_tax_consolidated_data( let ltaxes: Vec = lincomes.iter().map(|i| i.derive(0.0)).collect(); ta.taxes.extend(ltaxes); ta.incomes.extend(lincomes); + ta.symbols + .extend(std::iter::repeat(None).take(ta.incomes.len())); } ParsingState::SellEUR(s) | ParsingState::SellUSD(s) => { log::trace!("String to parse of Sells: {s}"); @@ -430,7 +434,12 @@ fn process_tax_consolidated_data( ta.dates .extend(parse_investment_transaction_dates(&filtred_df, "Date")?); - ta.symbols.extend(parse_symbols(&filtred_df, "Symbol")?); + ta.symbols.extend( + parse_symbols(&filtred_df, "Symbol")? + .into_iter() + .map(|s| Some(s)) + .collect::>>(), + ); // parse income let lincomes = parse_incomes(&filtred_df, "Gross amount base currency")?; @@ -481,7 +490,8 @@ fn process_tax_consolidated_data( /// 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, Option)> = vec![]; + let mut dividend_transactions: Vec<(String, crate::Currency, crate::Currency, Option)> = + vec![]; let mut sold_transactions: Vec<(String, String, crate::Currency, crate::Currency)> = vec![]; let mut crypto_transactions: Vec<(String, String, crate::Currency, crate::Currency)> = vec![]; @@ -698,9 +708,12 @@ pub fn parse_revolut_transactions(csvtoparse: &str) -> 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| s.to_string()).collect::>(); + let expected_symbols = symbols + .iter() + .map(|s| s.to_string()) + .collect::>(); let input_date_series = Series::new("Date", dates); let input_symbols = Series::new("Symbol", symbols); @@ -837,10 +853,7 @@ mod tests { let df = DataFrame::new(vec![input_date_series, input_symbols]) .map_err(|_| "Error creating DataFrame")?; - assert_eq!( - parse_symbols(&df, "Symbol"), - Ok(expected_symbols) - ); + assert_eq!(parse_symbols(&df, "Symbol"), Ok(expected_symbols)); Ok(()) } @@ -1124,26 +1137,26 @@ mod tests { "08/26/24".to_owned(), crate::Currency::PLN(302.43), crate::Currency::PLN(302.43 - 222.65), - Some("DE000A289XJ2".to_string()) + 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()) + Some("TFC".to_string()), ), ( "03/21/24".to_owned(), crate::Currency::PLN(259.17), crate::Currency::PLN(0.0), - Some("AMCR".to_string()) + 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()) + Some("EPR".to_string()), ), ], // TODO: symbols @@ -1153,16 +1166,14 @@ mod tests { "10/28/24".to_owned(), crate::Currency::PLN(13037.94 + 65.94), crate::Currency::PLN(13348.22), -// Some("EU000A3K4DJ5".to_string()) - + // 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()) - + // Some("XS1218821756".to_string()) ), ( "11/20/23".to_owned(), @@ -1206,25 +1217,25 @@ mod tests { "06/04/24".to_owned(), crate::Currency::PLN(2.80), crate::Currency::PLN(0.68), - Some("QDVY".to_string()) + Some("QDVY".to_string()), ), ( "06/20/24".to_owned(), crate::Currency::PLN(0.34), crate::Currency::PLN(0.08), - Some("EXI2".to_string()) + Some("EXI2".to_string()), ), ( "06/28/24".to_owned(), crate::Currency::PLN(3.79), crate::Currency::PLN(0.94), - Some("IS3K".to_string()) + Some("IS3K".to_string()), ), ( "07/01/24".to_owned(), crate::Currency::PLN(1.07), crate::Currency::PLN(0.25), - Some("IBCD".to_string()) + Some("IBCD".to_string()), ), ], sold_transactions: vec![], @@ -1247,55 +1258,55 @@ mod tests { "06/04/24".to_owned(), crate::Currency::PLN(2.80), crate::Currency::PLN(0.68), - Some("QDVY".to_string()) + Some("QDVY".to_string()), ), ( "06/20/24".to_owned(), crate::Currency::PLN(0.34), crate::Currency::PLN(0.08), - Some("EXI2".to_string()) + Some("EXI2".to_string()), ), ( "06/28/24".to_owned(), crate::Currency::PLN(3.79), crate::Currency::PLN(0.94), - Some("IS3K".to_string()) + Some("IS3K".to_string()), ), ( "07/01/24".to_owned(), crate::Currency::PLN(1.07), crate::Currency::PLN(0.25), - Some("IBCD".to_string()) + Some("IBCD".to_string()), ), ( "09/27/24".to_owned(), crate::Currency::PLN(1.02), crate::Currency::PLN(0.25), - Some("IBCD".to_string()) + Some("IBCD".to_string()), ), ( "09/27/24".to_owned(), crate::Currency::PLN(1.71), crate::Currency::PLN(0.42), - Some("IUSU".to_string()) + Some("IUSU".to_string()), ), ( "11/29/24".to_owned(), crate::Currency::PLN(2.92), crate::Currency::PLN(0.73), - Some("QDVY".to_string()) + Some("QDVY".to_string()), ), ( "12/17/24".to_owned(), crate::Currency::PLN(0.04), crate::Currency::PLN(0.0), - Some("EXI2".to_string()) + Some("EXI2".to_string()), ), ( "12/31/24".to_owned(), crate::Currency::PLN(1.07), crate::Currency::PLN(0.25), - Some("IBCD".to_string()) + Some("IBCD".to_string()), ), ], sold_transactions: vec![], @@ -1318,97 +1329,97 @@ mod tests { "03/04/24".to_owned(), crate::Currency::PLN(617.00), crate::Currency::PLN(92.57), - Some("TFC".to_string()) + Some("TFC".to_string()), ), ( "03/21/24".to_owned(), crate::Currency::PLN(259.17), crate::Currency::PLN(0.0), - Some("AMCR".to_string()) + Some("AMCR".to_string()), ), ( "03/25/24".to_owned(), crate::Currency::PLN(212.39), crate::Currency::PLN(31.87), - Some("PXD".to_string()) + Some("PXD".to_string()), ), ( "05/16/24".to_owned(), crate::Currency::PLN(700.17), crate::Currency::PLN(105.04), - Some("EPR".to_string()) + Some("EPR".to_string()), ), ( "05/31/24".to_owned(), crate::Currency::PLN(875.82), crate::Currency::PLN(131.38), - Some("UPS".to_string()) + Some("UPS".to_string()), ), ( "06/03/24".to_owned(), crate::Currency::PLN(488.26), crate::Currency::PLN(73.25), - Some("ABR".to_string()) + Some("ABR".to_string()), ), ( "06/04/24".to_owned(), crate::Currency::PLN(613.2), crate::Currency::PLN(92.00), - Some("TFC".to_string()) + Some("TFC".to_string()), ), ( "06/11/24".to_owned(), crate::Currency::PLN(186.16), crate::Currency::PLN(27.92), - Some("XOM".to_string()) + Some("XOM".to_string()), ), ( "06/13/24".to_owned(), crate::Currency::PLN(264.74), crate::Currency::PLN(0.00), - Some("AMCR".to_string()) + Some("AMCR".to_string()), ), ( "06/18/24".to_owned(), crate::Currency::PLN(858.33), crate::Currency::PLN(128.74), - Some("EPR".to_string()) + Some("EPR".to_string()), ), ( "07/12/24".to_owned(), crate::Currency::PLN(421.5), crate::Currency::PLN(63.23), - Some("BBY".to_string()) + Some("BBY".to_string()), ), ( "07/16/24".to_owned(), crate::Currency::PLN(834.55), crate::Currency::PLN(125.18), - Some("EPR".to_string()) + Some("EPR".to_string()), ), ( "08/16/24".to_owned(), crate::Currency::PLN(834.79), crate::Currency::PLN(125.23), - Some("EPR".to_string()) + Some("EPR".to_string()), ), ( "08/26/24".to_owned(), crate::Currency::PLN(302.43), crate::Currency::PLN(79.77), - Some("DE000A289XJ2".to_string()) + Some("DE000A289XJ2".to_string()), ), ( "08/29/24".to_owned(), crate::Currency::PLN(801.25), crate::Currency::PLN(0.0), - Some("BMO".to_string()) + Some("BMO".to_string()), ), ( "08/30/24".to_owned(), crate::Currency::PLN(872.56), crate::Currency::PLN(130.90), - Some("CAG".to_string()) + Some("CAG".to_string()), ), ], sold_transactions: vec![( @@ -1580,7 +1591,7 @@ mod tests { "12/14/23".to_owned(), crate::Currency::USD(2.94), crate::Currency::USD(0.00), - Some("AMCR".to_string()) + Some("AMCR".to_string()), ), ], sold_transactions: vec![], From 4d2e63fdaebb95bb2269125d587013bc7436fbb8 Mon Sep 17 00:00:00 2001 From: Jacek Czaja Date: Wed, 7 Jan 2026 17:03:58 +0100 Subject: [PATCH 07/20] - Fix another test --- src/csvparser.rs | 43 +++++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/csvparser.rs b/src/csvparser.rs index 31febcc..12e6422 100644 --- a/src/csvparser.rs +++ b/src/csvparser.rs @@ -163,7 +163,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 @@ -231,22 +231,22 @@ fn extract_intrest_rate_transactions(df: &DataFrame) -> Result Result, &'static str> { +fn parse_symbols(df: &DataFrame, col_name: &str) -> Result>, &'static str> { let symbol = df .column(col_name) .map_err(|_| "Error: Unable to select Symbol")?; - let mut symbols: Vec = vec![]; + 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(s.to_string()); - Ok::<(), &str>(()) + symbols.push(Some(s.to_string())); } else { - Err("Error: Missing Ticker Symbol") + symbols.push(None); } + Ok::<(), &str>(()) })?; Ok(symbols) @@ -434,12 +434,7 @@ fn process_tax_consolidated_data( ta.dates .extend(parse_investment_transaction_dates(&filtred_df, "Date")?); - ta.symbols.extend( - parse_symbols(&filtred_df, "Symbol")? - .into_iter() - .map(|s| Some(s)) - .collect::>>(), - ); + ta.symbols.extend(parse_symbols(&filtred_df, "Symbol")?); // parse income let lincomes = parse_incomes(&filtred_df, "Gross amount base currency")?; @@ -535,6 +530,8 @@ pub fn parse_revolut_transactions(csvtoparse: &str) -> Result Result Result Result>(); + .map(|s| Some(s.to_string())) + .collect::>>(); let input_date_series = Series::new("Date", dates); let input_symbols = Series::new("Symbol", symbols); From 9e40e078ea42f44f70d36cf7372d073c5f8635eb Mon Sep 17 00:00:00 2001 From: Jacek Czaja Date: Thu, 8 Jan 2026 09:10:54 +0100 Subject: [PATCH 08/20] - Fixed unit tests --- src/csvparser.rs | 3 +- src/lib.rs | 23 +++++++++------ src/pdfparser.rs | 39 ++++++++++++++++++++------ src/transactions.rs | 68 ++++++++++++++++++++++++++++++++------------- 4 files changed, 94 insertions(+), 39 deletions(-) diff --git a/src/csvparser.rs b/src/csvparser.rs index 12e6422..3dfbdb0 100644 --- a/src/csvparser.rs +++ b/src/csvparser.rs @@ -381,11 +381,10 @@ 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); - ta.symbols - .extend(std::iter::repeat(None).take(ta.incomes.len())); } ParsingState::SellEUR(s) | ParsingState::SellUSD(s) => { log::trace!("String to parse of Sells: {s}"); diff --git a/src/lib.rs b/src/lib.rs index 60a03d5..aaff2a4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,7 +65,7 @@ pub struct Transaction { pub tax_paid: Currency, pub exchange_rate_date: String, pub exchange_rate: f32, - pub company : Option, + pub company: Option, } impl Transaction { @@ -379,7 +379,12 @@ pub fn run_taxation( let mut parsed_div_transactions: Vec<(String, f32, f32, Option)> = 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![]; - let mut parsed_revolut_dividends_transactions: Vec<(String, Currency, Currency, Option)> = vec![]; + let mut parsed_revolut_dividends_transactions: Vec<( + String, + Currency, + Currency, + Option, + )> = vec![]; let mut parsed_revolut_sold_transactions: Vec<(String, String, Currency, Currency)> = vec![]; // 1. Parse PDF,XLSX and CSV documents to get list of transactions @@ -581,7 +586,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()) + company: Some("INTEL CORP".to_owned()), }]; assert_eq!(compute_div_taxation(&transactions), (400.0, 100.0)); Ok(()) @@ -597,7 +602,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()) + company: Some("INTEL CORP".to_owned()), }, Transaction { transaction_date: "N/A".to_string(), @@ -605,7 +610,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()) + company: Some("INTEL CORP".to_owned()), }, ]; assert_eq!( @@ -623,7 +628,7 @@ mod tests { tax_paid: crate::Currency::PLN(0.0), exchange_rate_date: "N/A".to_string(), exchange_rate: 1.0, - company : None, + company: None, }, Transaction { transaction_date: "04/11/21".to_string(), @@ -631,7 +636,7 @@ mod tests { tax_paid: crate::Currency::PLN(0.0), exchange_rate_date: "N/A".to_string(), exchange_rate: 1.0, - company : None, + company: None, }, ]; assert_eq!( @@ -650,7 +655,7 @@ mod tests { tax_paid: crate::Currency::EUR(0.0), exchange_rate_date: "02/28/21".to_string(), exchange_rate: 2.0, - company : None, + company: None, }, Transaction { transaction_date: "04/11/21".to_string(), @@ -658,7 +663,7 @@ mod tests { tax_paid: crate::Currency::EUR(0.0), exchange_rate_date: "04/10/21".to_string(), exchange_rate: 3.0, - company : None, + company: None, }, ]; assert_eq!( diff --git a/src/pdfparser.rs b/src/pdfparser.rs index df5f4b0..ee6c36d 100644 --- a/src/pdfparser.rs +++ b/src/pdfparser.rs @@ -497,10 +497,11 @@ 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 mut interests_as_div: Vec<(String, f32, f32, Option)> = interests_transactions - .iter_mut() - .map(|x| (x.0.clone(), x.1, x.2, None)) - .collect(); + let mut interests_as_div: Vec<(String, f32, f32, Option)> = + interests_transactions + .iter_mut() + .map(|x| (x.0.clone(), x.1, x.2, None)) + .collect(); let subject_to_tax = div_transactions .iter_mut() @@ -948,7 +949,7 @@ 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, company +/// transaction date, gross_us, tax_us, company /// Sold stock transaction is : /// (trade_date, settlement_date, quantity, price, amount_sold, company) pub fn parse_statement( @@ -1256,7 +1257,12 @@ 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, Some("INTEL CORP".to_string())),], + 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(), @@ -1304,8 +1310,18 @@ mod tests { ("1/2/24".to_owned(), 0.49, 0.00) ], vec![ - ("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())) + ( + "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![ ( @@ -1525,7 +1541,12 @@ mod tests { parse_statement("data/example-divs.pdf"), (Ok(( vec![], - vec![("03/01/22".to_owned(), 698.25, 104.74, Some("INTC".to_owned()))], + vec![( + "03/01/22".to_owned(), + 698.25, + 104.74, + Some("INTC".to_owned()) + )], vec![], vec![] ))) diff --git a/src/transactions.rs b/src/transactions.rs index 1467195..ff43817 100644 --- a/src/transactions.rs +++ b/src/transactions.rs @@ -168,7 +168,7 @@ pub fn create_detailed_revolut_transactions( tax_paid: *tax, exchange_rate_date, exchange_rate, - company : company.clone() + company: company.clone(), }; let msg = transaction.format_to_print("REVOLUT")?; @@ -200,7 +200,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 + company: None, // No company info when interests are paid on money }; let msg = transaction.format_to_print("INTERESTS")?; @@ -232,7 +232,7 @@ pub fn create_detailed_div_transactions( tax_paid: crate::Currency::USD(*tax_us as f64), exchange_rate_date, exchange_rate, - company: company.clone() + company: company.clone(), }; let msg = transaction.format_to_print("DIV")?; @@ -373,8 +373,18 @@ mod tests { #[test] fn test_dividends_verification_ok() -> Result<(), String> { 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())), + ( + "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) } @@ -386,13 +396,13 @@ mod tests { "06/01/21".to_string(), Currency::PLN(10.0), Currency::PLN(2.0), - Some("INTEL CORP".to_owned()) + Some("INTEL CORP".to_owned()), ), ( "03/01/22".to_string(), Currency::PLN(126.0), Currency::PLN(10.0), - Some("INTEL CORP".to_owned()) + Some("INTEL CORP".to_owned()), ), ]; assert_eq!( @@ -442,7 +452,7 @@ mod tests { tax_paid: crate::Currency::EUR(0.0), exchange_rate_date: "02/28/21".to_string(), exchange_rate: 2.0, - company : None, + company: None, }, Transaction { transaction_date: "04/11/21".to_string(), @@ -450,7 +460,7 @@ mod tests { tax_paid: crate::Currency::EUR(0.0), exchange_rate_date: "04/10/21".to_string(), exchange_rate: 3.0, - company : None, + company: None, }, ]) ); @@ -497,7 +507,7 @@ mod tests { tax_paid: crate::Currency::PLN(0.0), exchange_rate_date: "N/A".to_string(), exchange_rate: 1.0, - company : None, + company: None, }, Transaction { transaction_date: "04/11/21".to_string(), @@ -505,7 +515,7 @@ mod tests { tax_paid: crate::Currency::PLN(0.0), exchange_rate_date: "N/A".to_string(), exchange_rate: 1.0, - company : None, + company: None, }, ]) ); @@ -542,7 +552,7 @@ mod tests { tax_paid: crate::Currency::USD(0.0), exchange_rate_date: "04/10/21".to_string(), exchange_rate: 3.0, - company : None, + company: None, }, Transaction { transaction_date: "03/01/21".to_string(), @@ -550,7 +560,7 @@ mod tests { tax_paid: crate::Currency::USD(0.0), exchange_rate_date: "02/28/21".to_string(), exchange_rate: 2.0, - company : None, + company: None, }, ]) ); @@ -560,8 +570,18 @@ mod tests { #[test] fn test_create_detailed_div_transactions() -> Result<(), String> { 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())), + ( + "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> = @@ -587,7 +607,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()) + company: Some("INTEL CORP".to_owned()) }, Transaction { transaction_date: "03/01/21".to_string(), @@ -595,7 +615,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()) + company: Some("INTEL CORP".to_owned()) }, ]) ); @@ -738,8 +758,18 @@ mod tests { #[test] fn test_dividends_verification_fail() -> Result<(), String> { 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())), + ( + "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(()) From 7680b0a1265ef8703d6e86c914dae43efb2447ec Mon Sep 17 00:00:00 2001 From: Jacek Czaja Date: Thu, 8 Jan 2026 11:26:11 +0100 Subject: [PATCH 09/20] - Removing brokerage statement support --- src/pdfparser.rs | 274 +++++++---------------------------------------- 1 file changed, 39 insertions(+), 235 deletions(-) diff --git a/src/pdfparser.rs b/src/pdfparser.rs index ee6c36d..5bf0227 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 @@ -331,6 +335,11 @@ fn yield_sold_transaction( transaction: &mut std::slice::Iter<'_, Box>, transaction_dates: &mut Vec, ) -> Option<(String, String, f32, f32, f32)> { + let _symbol = transaction + .next() + .unwrap() + .getstring() + .expect_and_log("Processing of Sold transaction went wrong"); // TODO: make sold to report symbol let quantity = transaction .next() .unwrap() @@ -470,8 +479,16 @@ 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!["INTEL CORP".to_owned(), "ADVANCED MICRO DEVICES".to_owned()]; + if obj.is_pattern() == true { + if support_companies.contains(&token) == true { + processed_sequence.push(obj); + } + } else { + if token != "$" { + sequence.push_front(obj); + } } } @@ -529,6 +546,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() @@ -541,7 +563,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("KUPA".to_string()), + Some(symbol), )); log::info!("Completed parsing Dividend transaction"); } @@ -572,197 +594,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, Option)>, - 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, Option)> = 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, - None, - )); - } - 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, @@ -986,7 +817,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"); @@ -1073,10 +904,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 @@ -1093,10 +925,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 @@ -1109,10 +942,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 @@ -1536,37 +1370,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, - Some("INTC".to_owned()) - )], - 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(( From 7eeefe9b54e068a8231a2102024cf51bdf0e0646 Mon Sep 17 00:00:00 2001 From: Jacek Czaja Date: Thu, 8 Jan 2026 12:43:43 +0100 Subject: [PATCH 10/20] - Fix another test --- src/main.rs | 194 ++++++++++++++--------------------------------- src/pdfparser.rs | 38 ++++++++-- 2 files changed, 89 insertions(+), 143 deletions(-) diff --git a/src/main.rs b/src/main.rs index a620fe9..dd65a83 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,9 +16,8 @@ use etradeTaxReturnHelper::run_taxation; use etradeTaxReturnHelper::TaxCalculationResult; use logging::ResultExt; -// TODO: Fixing UT. Why revolut transaction does have only dividend_transactions not interests? // TODO: check if Tax from Terna company taken by IT goverment was taken into account -// TODO: Extend structure of TaxCalculationResult with country and company +// 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 @@ -43,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 @@ -94,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}"), }; @@ -215,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> { @@ -285,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 @@ -428,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, @@ -461,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, @@ -494,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, @@ -514,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, @@ -537,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(()) } @@ -558,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 5bf0227..e697bc0 100644 --- a/src/pdfparser.rs +++ b/src/pdfparser.rs @@ -479,8 +479,11 @@ fn process_transaction( // attach to sequence the same string parser if pattern is not met match obj.getstring() { Some(token) => { - let support_companies = - vec!["INTEL CORP".to_owned(), "ADVANCED MICRO DEVICES".to_owned()]; + let support_companies = vec![ + "TREASURY LIQUIDITY FUND".to_owned(), + "INTEL CORP".to_owned(), + "ADVANCED MICRO DEVICES".to_owned(), + ]; if obj.is_pattern() == true { if support_companies.contains(&token) == true { processed_sequence.push(obj); @@ -503,6 +506,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 @@ -514,22 +522,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 mut interests_as_div: Vec<(String, f32, f32, Option)> = - interests_transactions + let mut interests_as_div: Vec<( + &mut String, + &mut f32, + &mut f32, + Option, + )> = interests_transactions + .iter_mut() + .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| (x.0.clone(), x.1, x.2, None)) + .map(|x| (&mut x.0, &mut x.1, &mut x.2, x.3.clone())) .collect(); - let subject_to_tax = div_transactions + 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) + .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() From 7cc984a066bddc4855f9102da28fbf40a8bb7789 Mon Sep 17 00:00:00 2001 From: Jacek Czaja Date: Thu, 8 Jan 2026 13:30:38 +0100 Subject: [PATCH 11/20] - added UT --- src/pdfparser.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pdfparser.rs b/src/pdfparser.rs index e697bc0..7f1e182 100644 --- a/src/pdfparser.rs +++ b/src/pdfparser.rs @@ -483,6 +483,7 @@ fn process_transaction( "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 { From 06a8ed9958c4b0369fbfaf464627c8cb338aa644 Mon Sep 17 00:00:00 2001 From: Jacek Czaja Date: Fri, 9 Jan 2026 10:00:09 +0100 Subject: [PATCH 12/20] - compiles --- src/gui.rs | 2 +- src/lib.rs | 5 +++++ src/main.rs | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) 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 aaff2a4..ac16770 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -372,6 +372,7 @@ 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)?; @@ -495,6 +496,10 @@ pub fn run_taxation( let revolut_sold_transactions = create_detailed_revolut_sold_transactions(parsed_revolut_sold_transactions, &dates)?; + if per_company { + todo!(); + } + 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); diff --git a/src/main.rs b/src/main.rs index dd65a83..cd3ab23 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,7 @@ use etradeTaxReturnHelper::run_taxation; use etradeTaxReturnHelper::TaxCalculationResult; use logging::ResultExt; +// TODO: add option --per-company // 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 @@ -260,6 +261,40 @@ mod tests { }; Ok(()) } + #[test] + fn test_cmdline_per_company() -> Result<(), clap::Error> { + // Init Transactions + let myapp = App::new("E-trade tax helper"); + let matches = create_cmd_line_pattern(myapp).get_matches_from_safe(vec![ + "mytest", + "data/example.pdf", + ])?; + let per_company = matches.is_present("per-company"); + match per_company { + false => (), + true => return Err(clap::Error { + message: "Wrong per-company value".to_owned(), + kind: ErrorKind::InvalidValue, + info: None, + }), + }; + let myapp = App::new("E-trade tax helper"); + let matches = create_cmd_line_pattern(myapp).get_matches_from_safe(vec![ + "mytest", + "--per-company", + "data/example.pdf", + ])?; + let per_company = matches.is_present("per-company"); + match per_company { + true => return Ok(()), + false => clap::Error { + message: "Wrong per-company value".to_owned(), + kind: ErrorKind::InvalidValue, + info: None, + }, + }; + Ok(()) + } #[test] fn test_cmdline_pl() -> Result<(), clap::Error> { From 21b7e8b4f6454ac822a55a624e82dfd0b9d2d3cb Mon Sep 17 00:00:00 2001 From: Jacek Czaja Date: Fri, 9 Jan 2026 11:36:03 +0100 Subject: [PATCH 13/20] - Added implementation of create report per company --- src/lib.rs | 4 +- src/transactions.rs | 114 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index ac16770..1b5d049 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,7 @@ use transactions::{ create_detailed_revolut_sold_transactions, create_detailed_revolut_transactions, create_detailed_sold_transactions, reconstruct_sold_transactions, verify_dividends_transactions, verify_interests_transactions, verify_transactions, + create_per_company_report, }; #[derive(Debug, PartialEq, PartialOrd, Copy, Clone)] @@ -497,7 +498,8 @@ pub fn run_taxation( create_detailed_revolut_sold_transactions(parsed_revolut_sold_transactions, &dates)?; if per_company { - todo!(); + 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); diff --git a/src/transactions.rs b/src/transactions.rs index ff43817..ddaea84 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}; @@ -332,12 +334,124 @@ pub fn create_detailed_revolut_sold_transactions( 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(None).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") +} + #[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_interests_verification_ok() -> Result<(), String> { let transactions: Vec<(String, f32, f32)> = vec![ From df428e6ba9711ccaa42034d339743ffe2f208779 Mon Sep 17 00:00:00 2001 From: Jacek Czaja Date: Fri, 9 Jan 2026 12:05:12 +0100 Subject: [PATCH 14/20] - added another unit test --- src/transactions.rs | 75 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 5 deletions(-) diff --git a/src/transactions.rs b/src/transactions.rs index ddaea84..1b8e04d 100644 --- a/src/transactions.rs +++ b/src/transactions.rs @@ -377,12 +377,10 @@ pub(crate) fn create_per_company_report( per_company_data .iter() .try_for_each(|(company, (gross_pl, tax_paid_in_us_pl, cost_pl))| { - log::info!( + //log::info!( + println!( "Company: {:?}, Gross PLN: {:.2}, Tax Paid in USD PLN: {:.2}, Cost PLN: {:.2}", - company, - gross_pl, - tax_paid_in_us_pl, - cost_pl + company, gross_pl, tax_paid_in_us_pl, cost_pl ); companies.push(company.clone()); gross.push(*gross_pl); @@ -452,6 +450,73 @@ mod tests { 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_interests_verification_ok() -> Result<(), String> { let transactions: Vec<(String, f32, f32)> = vec![ From d62bc545cf7aa9122630b575437da5ac61b15207 Mon Sep 17 00:00:00 2001 From: Jacek Czaja Date: Fri, 9 Jan 2026 12:25:14 +0100 Subject: [PATCH 15/20] - cosmetic fixes --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index 1b5d049..23df618 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -499,6 +499,7 @@ pub fn run_taxation( 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); } From a73bbe10e2957158d67814a3de061059f815b720 Mon Sep 17 00:00:00 2001 From: Jacek Czaja Date: Fri, 9 Jan 2026 15:50:02 +0100 Subject: [PATCH 16/20] - Added symbols in transactions of selling --- src/csvparser.rs | 71 ++++++++++++++++++++++++---- src/lib.rs | 13 +++-- src/pdfparser.rs | 112 ++++++++++++++++++++++++++++---------------- src/transactions.rs | 80 +++++++++++++++++++++---------- 4 files changed, 196 insertions(+), 80 deletions(-) diff --git a/src/csvparser.rs b/src/csvparser.rs index 3dfbdb0..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 { @@ -38,7 +39,13 @@ struct TransactionAccumulator { #[derive(Debug, PartialEq)] pub struct RevolutTransactions { pub dividend_transactions: Vec<(String, crate::Currency, crate::Currency, Option)>, - pub sold_transactions: Vec<(String, String, crate::Currency, crate::Currency)>, + pub sold_transactions: Vec<( + String, + String, + crate::Currency, + crate::Currency, + Option, + )>, pub crypto_transactions: Vec<(String, String, crate::Currency, crate::Currency)>, } @@ -141,6 +148,7 @@ fn extract_sold_transactions(df: &DataFrame) -> Result df.select([ "Date acquired", "Date sold", + "Symbol", "Cost basis", "Gross proceeds", "Currency", @@ -149,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", @@ -406,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 @@ -486,7 +498,13 @@ fn process_tax_consolidated_data( pub fn parse_revolut_transactions(csvtoparse: &str) -> Result { let mut dividend_transactions: Vec<(String, crate::Currency, crate::Currency, Option)> = vec![]; - let mut sold_transactions: Vec<(String, String, crate::Currency, crate::Currency)> = 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(); @@ -583,6 +601,18 @@ pub fn parse_revolut_transactions(csvtoparse: &str) -> Result Result Result, // TODO //pub country : Option, - //pub company : Option, } impl SoldTransaction { @@ -379,7 +379,7 @@ pub fn run_taxation( let mut parsed_interests_transactions: Vec<(String, 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)> = 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, @@ -387,7 +387,7 @@ pub fn run_taxation( Currency, Option, )> = vec![]; - let mut parsed_revolut_sold_transactions: Vec<(String, String, Currency, Currency)> = 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| { @@ -450,7 +450,7 @@ pub fn run_taxation( } }); 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); @@ -475,7 +475,7 @@ pub fn run_taxation( }); parsed_revolut_sold_transactions .iter() - .for_each(|(acquired_date, sold_date, cost, gross)| { + .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); @@ -694,6 +694,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), @@ -716,6 +717,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(), @@ -727,6 +729,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/pdfparser.rs b/src/pdfparser.rs index 7f1e182..da0c364 100644 --- a/src/pdfparser.rs +++ b/src/pdfparser.rs @@ -334,12 +334,12 @@ fn create_trade_parsing_sequence(sequence: &mut std::collections::VecDeque>, transaction_dates: &mut Vec, -) -> Option<(String, String, f32, f32, f32)> { - let _symbol = transaction +) -> Option<(String, String, f32, f32, f32, Option)> { + let symbol = transaction .next() .unwrap() .getstring() - .expect_and_log("Processing of Sold transaction went wrong"); // TODO: make sold to report symbol + .expect_and_log("Processing of Sold transaction went wrong"); let quantity = transaction .next() .unwrap() @@ -380,7 +380,7 @@ 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 @@ -461,7 +461,7 @@ fn recognize_statement(page: PageRc) -> Result { fn process_transaction( interests_transactions: &mut Vec<(String, f32, f32)>, div_transactions: &mut Vec<(String, f32, f32, Option)>, - sold_transactions: &mut Vec<(String, String, f32, f32, f32)>, + sold_transactions: &mut Vec<(String, String, f32, f32, f32, Option)>, actual_string: &pdf::primitive::PdfString, transaction_dates: &mut Vec, processed_sequence: &mut Vec>, @@ -687,7 +687,7 @@ fn parse_account_statement<'a, I>( ( Vec<(String, f32, f32)>, Vec<(String, f32, f32, Option)>, - Vec<(String, String, f32, f32, f32)>, + Vec<(String, String, f32, f32, f32, Option)>, Vec<(String, String, i32, f32, f32, f32, f32, f32)>, ), String, @@ -697,7 +697,7 @@ where { let mut interests_transactions: Vec<(String, f32, f32)> = vec![]; let mut div_transactions: Vec<(String, f32, f32, Option)> = vec![]; - let mut sold_transactions: Vec<(String, String, f32, f32, f32)> = 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> = @@ -812,7 +812,7 @@ pub fn parse_statement( ( Vec<(String, f32, f32)>, Vec<(String, f32, f32, Option)>, - Vec<(String, String, f32, f32, f32)>, + Vec<(String, String, f32, f32, f32, Option)>, Vec<(String, String, i32, f32, f32, f32, f32, f32)>, ), String, @@ -1125,7 +1125,8 @@ mod tests { "12/26/23".to_owned(), 82.0, 46.45, - 3808.86 + 3808.86, + Some("INTEL CORP".to_string()) )], vec![] ))) @@ -1186,203 +1187,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![] @@ -1405,14 +1435,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 1b8e04d..27fb456 100644 --- a/src/transactions.rs +++ b/src/transactions.rs @@ -67,10 +67,10 @@ 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(()); @@ -81,7 +81,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(); @@ -99,16 +99,17 @@ 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 @@ -123,7 +124,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 @@ -144,6 +145,7 @@ pub fn reconstruct_sold_transactions( .to_string(), *inc, *cost_basis, + symbol.clone(), )); } @@ -257,12 +259,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() @@ -282,6 +284,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(""); @@ -296,13 +299,13 @@ 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)| { + .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() @@ -322,6 +325,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 "); @@ -528,18 +532,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!( @@ -803,11 +809,13 @@ mod tests { #[test] fn test_create_detailed_revolut_sold_transactions() -> Result<(), String> { - let parsed_transactions: Vec<(String, String, Currency, Currency)> = vec![( + + 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> = @@ -836,6 +844,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(()) @@ -843,13 +852,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(), @@ -857,6 +867,7 @@ mod tests { "01/01/19".to_string(), 25.0, 10.0, + Some("INTEL CORP".to_owned()), ), ]; @@ -911,6 +922,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(), @@ -922,6 +934,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()), }, ]) ); @@ -956,7 +969,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![]; @@ -973,13 +986,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(), @@ -987,6 +1001,7 @@ mod tests { 2.0, 10.0, 19.8, + Some("INTEL CORP".to_owned()) ), ]; @@ -1023,14 +1038,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()) ), ] ); @@ -1039,9 +1056,9 @@ 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![ @@ -1077,14 +1094,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()) ), ] ); @@ -1093,12 +1112,13 @@ mod tests { #[test] fn test_sold_transaction_reconstruction_second_fail() { - let parsed_sold_transactions: Vec<(String, String, f32, f32, f32)> = vec![( + 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![ @@ -1134,13 +1154,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(), @@ -1148,6 +1169,7 @@ mod tests { 252.0, 26.5900, 6698.00, + Some("INTEL CORP".to_owned()) ), ]; @@ -1194,6 +1216,7 @@ mod tests { "08/19/21".to_string(), 2711.0954, 4336.4874, + Some("INTEL CORP".to_owned()) ), ( "12/21/22".to_string(), @@ -1201,6 +1224,7 @@ mod tests { "05/03/21".to_string(), 2046.61285, 0.0, + Some("INTEL CORP".to_owned()) ), ( "12/19/22".to_string(), @@ -1208,6 +1232,7 @@ mod tests { "08/19/22".to_string(), 3986.9048, 5045.6257, + Some("INTEL CORP".to_owned()) ), ( "12/21/22".to_string(), @@ -1215,6 +1240,7 @@ mod tests { "05/02/22".to_string(), 2285.82733, 0.0, + Some("INTEL CORP".to_owned()) ), ] ); @@ -1223,13 +1249,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(), @@ -1237,6 +1264,7 @@ mod tests { 2.0, 10.0, 19.8, + Some("INTEL CORP".to_owned()) ), ]; From 779f73367bc098e9e9f829793507e79d51c5722f Mon Sep 17 00:00:00 2001 From: Jacek Czaja Date: Fri, 9 Jan 2026 15:51:46 +0100 Subject: [PATCH 17/20] - formatting --- src/lib.rs | 39 +++++++++++------- src/main.rs | 19 +++++---- src/pdfparser.rs | 9 ++++- src/transactions.rs | 96 ++++++++++++++++++++++++++++----------------- 4 files changed, 102 insertions(+), 61 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2e71be1..b6b6324 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,9 +14,8 @@ 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, - create_per_company_report, }; #[derive(Debug, PartialEq, PartialOrd, Copy, Clone)] @@ -116,7 +115,7 @@ pub struct SoldTransaction { pub exchange_rate_settlement: f32, pub exchange_rate_acquisition_date: String, pub exchange_rate_acquisition: f32, - pub company : Option, + pub company: Option, // TODO //pub country : Option, } @@ -387,7 +386,13 @@ pub fn run_taxation( Currency, Option, )> = vec![]; - let mut parsed_revolut_sold_transactions: Vec<(String, 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| { @@ -473,9 +478,8 @@ pub fn run_taxation( 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); @@ -484,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())?; @@ -498,9 +503,15 @@ pub fn run_taxation( 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 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); @@ -694,7 +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()) + company: Some("TFC".to_owned()), }]; assert_eq!( compute_sold_taxation(&transactions), @@ -717,7 +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()) + company: Some("PXD".to_owned()), }, SoldTransaction { trade_date: "N/A".to_string(), @@ -729,7 +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()) + company: Some("TFC".to_owned()), }, ]; assert_eq!( diff --git a/src/main.rs b/src/main.rs index cd3ab23..d321799 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,6 @@ use etradeTaxReturnHelper::run_taxation; use etradeTaxReturnHelper::TaxCalculationResult; use logging::ResultExt; -// TODO: add option --per-company // 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 @@ -265,18 +264,18 @@ mod tests { fn test_cmdline_per_company() -> Result<(), clap::Error> { // Init Transactions let myapp = App::new("E-trade tax helper"); - let matches = create_cmd_line_pattern(myapp).get_matches_from_safe(vec![ - "mytest", - "data/example.pdf", - ])?; + let matches = create_cmd_line_pattern(myapp) + .get_matches_from_safe(vec!["mytest", "data/example.pdf"])?; let per_company = matches.is_present("per-company"); match per_company { false => (), - true => return Err(clap::Error { - message: "Wrong per-company value".to_owned(), - kind: ErrorKind::InvalidValue, - info: None, - }), + true => { + return Err(clap::Error { + message: "Wrong per-company value".to_owned(), + kind: ErrorKind::InvalidValue, + info: None, + }) + } }; let myapp = App::new("E-trade tax helper"); let matches = create_cmd_line_pattern(myapp).get_matches_from_safe(vec![ diff --git a/src/pdfparser.rs b/src/pdfparser.rs index da0c364..027a986 100644 --- a/src/pdfparser.rs +++ b/src/pdfparser.rs @@ -380,7 +380,14 @@ fn yield_sold_transaction( } }; - Some((trade_date, settlement_date, quantity, price, amount_sold, Some(symbol))) + 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 diff --git a/src/transactions.rs b/src/transactions.rs index 27fb456..e9436f7 100644 --- a/src/transactions.rs +++ b/src/transactions.rs @@ -67,7 +67,9 @@ pub fn verify_dividends_transactions( verification } -pub fn verify_transactions(transactions: &Vec<(String, String, T, T, Option)>) -> 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, @@ -109,7 +111,8 @@ pub fn reconstruct_sold_transactions( // 4. gross income // 5. cost cost basis // 6. company symbol (ticker) - let mut detailed_sold_transactions: Vec<(String, String, String, f32, f32, Option)> = vec![]; + 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 @@ -284,7 +287,7 @@ pub fn create_detailed_sold_transactions( exchange_rate_settlement, exchange_rate_acquisition_date, exchange_rate_acquisition, - company : symbol.clone(), + company: symbol.clone(), }; let msg = transaction.format_to_print(""); @@ -299,13 +302,18 @@ pub fn create_detailed_sold_transactions( } pub fn create_detailed_revolut_sold_transactions( - transactions: Vec<(String, String, crate::Currency, crate::Currency, Option)>, + 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, symbol)| { + 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() @@ -325,7 +333,7 @@ pub fn create_detailed_revolut_sold_transactions( exchange_rate_settlement, exchange_rate_acquisition_date, exchange_rate_acquisition, - company : symbol.clone(), + company: symbol.clone(), }; let msg = transaction.format_to_print("REVOLUT "); @@ -334,7 +342,8 @@ pub fn create_detailed_revolut_sold_transactions( log::info!("{}", msg); detailed_transactions.push(transaction); - }); + }, + ); Ok(detailed_transactions) } @@ -809,14 +818,14 @@ mod tests { #[test] fn test_create_detailed_revolut_sold_transactions() -> Result<(), String> { - - 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 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(); @@ -844,7 +853,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()), + company: Some("INTEL CORP".to_owned()), },]) ); Ok(()) @@ -922,7 +931,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()), + company: Some("INTEL CORP".to_owned()), }, SoldTransaction { trade_date: "06/01/21".to_string(), @@ -934,7 +943,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()), + company: Some("INTEL CORP".to_owned()), }, ]) ); @@ -993,7 +1002,7 @@ mod tests { 1.0, 25.0, 24.8, - Some("INTEL CORP".to_owned()) + Some("INTEL CORP".to_owned()), ), ( "03/01/21".to_string(), @@ -1001,7 +1010,7 @@ mod tests { 2.0, 10.0, 19.8, - Some("INTEL CORP".to_owned()) + Some("INTEL CORP".to_owned()), ), ]; @@ -1057,8 +1066,22 @@ mod tests { #[test] fn test_sold_transaction_reconstruction_single_digits_ok() -> Result<(), String> { 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())), + ( + "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![ @@ -1112,14 +1135,15 @@ mod tests { #[test] fn test_sold_transaction_reconstruction_second_fail() { - 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_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![ ( @@ -1161,7 +1185,7 @@ mod tests { 163.0, 26.5900, 4332.44, - Some("INTEL CORP".to_owned()) + Some("INTEL CORP".to_owned()), ), ( "12/19/22".to_string(), @@ -1169,7 +1193,7 @@ mod tests { 252.0, 26.5900, 6698.00, - Some("INTEL CORP".to_owned()) + Some("INTEL CORP".to_owned()), ), ]; @@ -1256,7 +1280,7 @@ mod tests { 1.0, 25.0, 24.8, - Some("INTEL CORP".to_owned()) + Some("INTEL CORP".to_owned()), ), ( "03/01/21".to_string(), @@ -1264,7 +1288,7 @@ mod tests { 2.0, 10.0, 19.8, - Some("INTEL CORP".to_owned()) + Some("INTEL CORP".to_owned()), ), ]; From d3b06f1de85664615620b75b21577e7c002e0b58 Mon Sep 17 00:00:00 2001 From: Jacek Czaja Date: Fri, 9 Jan 2026 16:33:26 +0100 Subject: [PATCH 18/20] - fmt --- src/transactions.rs | 91 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 4 deletions(-) diff --git a/src/transactions.rs b/src/transactions.rs index e9436f7..0236e0c 100644 --- a/src/transactions.rs +++ b/src/transactions.rs @@ -376,7 +376,9 @@ pub(crate) fn create_per_company_report( .iter() .chain(revolut_sold_transactions.iter()); sells.for_each(|x| { - let entry = per_company_data.entry(None).or_insert((0.0, 0.0, 0.0)); + 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; @@ -390,10 +392,12 @@ pub(crate) fn create_per_company_report( per_company_data .iter() .try_for_each(|(company, (gross_pl, tax_paid_in_us_pl, cost_pl))| { - //log::info!( - println!( + 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 + company, + gross_pl, + tax_paid_in_us_pl, + cost_pl ); companies.push(company.clone()); gross.push(*gross_pl); @@ -530,6 +534,85 @@ mod tests { 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![ From 7415050100fbd6caf1bb4c1b9637b38f7f1798ef Mon Sep 17 00:00:00 2001 From: Jacek Czaja Date: Fri, 9 Jan 2026 17:14:05 +0100 Subject: [PATCH 19/20] - Sorting DataFrame --- src/transactions.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/transactions.rs b/src/transactions.rs index 0236e0c..4aebbcc 100644 --- a/src/transactions.rs +++ b/src/transactions.rs @@ -412,7 +412,10 @@ pub(crate) fn create_per_company_report( 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") + 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)] From ce157ccc3ebb6ba914b8e3c65e27946b93ba132b Mon Sep 17 00:00:00 2001 From: Jacek Czaja Date: Fri, 9 Jan 2026 19:15:47 +0100 Subject: [PATCH 20/20] Removed redundant UT --- src/main.rs | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/src/main.rs b/src/main.rs index d321799..dd65a83 100644 --- a/src/main.rs +++ b/src/main.rs @@ -260,40 +260,6 @@ mod tests { }; Ok(()) } - #[test] - fn test_cmdline_per_company() -> Result<(), clap::Error> { - // Init Transactions - let myapp = App::new("E-trade tax helper"); - let matches = create_cmd_line_pattern(myapp) - .get_matches_from_safe(vec!["mytest", "data/example.pdf"])?; - let per_company = matches.is_present("per-company"); - match per_company { - false => (), - true => { - return Err(clap::Error { - message: "Wrong per-company value".to_owned(), - kind: ErrorKind::InvalidValue, - info: None, - }) - } - }; - let myapp = App::new("E-trade tax helper"); - let matches = create_cmd_line_pattern(myapp).get_matches_from_safe(vec![ - "mytest", - "--per-company", - "data/example.pdf", - ])?; - let per_company = matches.is_present("per-company"); - match per_company { - true => return Ok(()), - false => clap::Error { - message: "Wrong per-company value".to_owned(), - kind: ErrorKind::InvalidValue, - info: None, - }, - }; - Ok(()) - } #[test] fn test_cmdline_pl() -> Result<(), clap::Error> {