diff --git a/Cargo.lock b/Cargo.lock index 503c1e0..12125d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -624,9 +624,9 @@ dependencies = [ [[package]] name = "ethnum" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca81e6b4777c89fd810c25a4be2b1bd93ea034fbe58e6a75216a34c6b82c539b" +checksum = "40404c3f5f511ec4da6fe866ddf6a717c309fdbb69fbbad7b0f3edab8f2e835f" [[package]] name = "etradeTaxReturnHelper" @@ -997,7 +997,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.62.2", ] [[package]] @@ -1570,9 +1570,9 @@ dependencies = [ [[package]] name = "polars" -version = "0.35.4" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8e52f9236eb722da0990a70bbb1216dcc7a77bcb00c63439d2d982823e90d5" +checksum = "938048fcda6a8e2ace6eb168bee1b415a92423ce51e418b853bf08fc40349b6b" dependencies = [ "getrandom 0.2.16", "polars-core", @@ -1586,9 +1586,9 @@ dependencies = [ [[package]] name = "polars-arrow" -version = "0.35.4" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd503430a6d9779b07915d858865fe998317ef3cfef8973881f578ac5d4baae7" +checksum = "ce68a02f698ff7787c261aea1b4c040a8fe183a8fb200e2436d7f35d95a1b86f" dependencies = [ "ahash", "arrow-format", @@ -1608,19 +1608,32 @@ dependencies = [ "num-traits", "polars-error", "polars-utils", - "rustc_version", "ryu", "simdutf8", "streaming-iterator", "strength_reduce", + "version_check", "zstd", ] +[[package]] +name = "polars-compute" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b14fbc5f141b29b656a4cec4802632e5bff10bf801c6809c6bbfbd4078a044dd" +dependencies = [ + "bytemuck", + "num-traits", + "polars-arrow", + "polars-utils", + "version_check", +] + [[package]] name = "polars-core" -version = "0.35.4" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae73d5b8e55decde670caba1cc82b61f14bfb9a72503198f0997d657a98dcfd6" +checksum = "d0f5efe734b6cbe5f97ea769be8360df5324fade396f1f3f5ad7fe9360ca4a23" dependencies = [ "ahash", "bitflags 2.10.0", @@ -1633,6 +1646,7 @@ dependencies = [ "num-traits", "once_cell", "polars-arrow", + "polars-compute", "polars-error", "polars-row", "polars-utils", @@ -1648,9 +1662,9 @@ dependencies = [ [[package]] name = "polars-error" -version = "0.35.4" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb0520d68eaa9993ae0c741409d1526beff5b8f48e1d73e4381616f8152cf488" +checksum = "6396de788f99ebfc9968e7b6f523e23000506cde4ba6dfc62ae4ce949002a886" dependencies = [ "arrow-format", "regex", @@ -1660,9 +1674,9 @@ dependencies = [ [[package]] name = "polars-io" -version = "0.35.4" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96e10a0745acd6009db64bef0ceb9e23a70b1c27b26a0a6517c91f3e6363bc06" +checksum = "7d0458efe8946f4718fd352f230c0db5a37926bd0d2bd25af79dc24746abaaea" dependencies = [ "ahash", "atoi_simd", @@ -1690,9 +1704,9 @@ dependencies = [ [[package]] name = "polars-lazy" -version = "0.35.4" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3555f759705be6dd0d3762d16a0b8787b2dc4da73b57465f3b2bf1a070ba8f20" +checksum = "9d7105b40905bb38e8fc4a7fd736594b7491baa12fad3ac492969ca221a1b5d5" dependencies = [ "ahash", "bitflags 2.10.0", @@ -1713,9 +1727,9 @@ dependencies = [ [[package]] name = "polars-ops" -version = "0.35.4" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7eb218296aaa7f79945f08288ca32ca3cf25fa505649eeee689ec21eebf636" +checksum = "2e09afc456ab11e75e5dcb43e00a01c71f3a46a2781e450054acb6bb096ca78e" dependencies = [ "ahash", "argminmax", @@ -1726,6 +1740,7 @@ dependencies = [ "memchr", "num-traits", "polars-arrow", + "polars-compute", "polars-core", "polars-error", "polars-utils", @@ -1737,9 +1752,9 @@ dependencies = [ [[package]] name = "polars-pipe" -version = "0.35.4" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66094e7df64c932a9a7bdfe7df0c65efdcb192096e11a6a765a9778f78b4bdec" +checksum = "d9b7ead073cc3917027d77b59861a9f071db47125de9314f8907db1a0a3e4100" dependencies = [ "crossbeam-channel", "crossbeam-queue", @@ -1747,6 +1762,7 @@ dependencies = [ "hashbrown 0.14.5", "num-traits", "polars-arrow", + "polars-compute", "polars-core", "polars-io", "polars-ops", @@ -1760,9 +1776,9 @@ dependencies = [ [[package]] name = "polars-plan" -version = "0.35.4" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10e32a0958ef854b132bad7f8369cb3237254635d5e864c99505bc0bc1035fbc" +checksum = "384a175624d050c31c473ee11df9d7af5d729ae626375e522158cfb3d150acd0" dependencies = [ "ahash", "bytemuck", @@ -1783,9 +1799,9 @@ dependencies = [ [[package]] name = "polars-row" -version = "0.35.4" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d135ab81cac2906ba74ea8984c7e6025d081ae5867615bcefb4d84dfdb456dac" +checksum = "32322f7acbb83db3e9c7697dc821be73d06238da89c817dcc8bc1549a5e9c72f" dependencies = [ "polars-arrow", "polars-error", @@ -1794,9 +1810,9 @@ dependencies = [ [[package]] name = "polars-sql" -version = "0.35.4" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dbd7786849a5e3ad1fde188bf38141632f626e3a57319b0bbf7a5f1d75519e" +checksum = "9f0b4c6ddffdfd0453e84bc3918572c633014d661d166654399cf93752aa95b5" dependencies = [ "polars-arrow", "polars-core", @@ -1811,9 +1827,9 @@ dependencies = [ [[package]] name = "polars-time" -version = "0.35.4" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae56f79e9cedd617773c1c8f5ca84a31a8b1d593714959d5f799e7bdd98fe51" +checksum = "dee2649fc96bd1b6584e0e4a4b3ca7d22ed3d117a990e63ad438ecb26f7544d0" dependencies = [ "atoi", "chrono", @@ -1830,9 +1846,9 @@ dependencies = [ [[package]] name = "polars-utils" -version = "0.35.4" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da6ce68169fe61d46958c8eab7447360f30f2f23f6e24a0ce703a14b0a3cfbfc" +checksum = "b174ca4a77ad47d7b91a0460aaae65bbf874c8bfbaaa5308675dadef3976bbda" dependencies = [ "ahash", "bytemuck", @@ -2049,15 +2065,6 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - [[package]] name = "rustix" version = "1.1.2" @@ -2130,12 +2137,6 @@ dependencies = [ "libc", ] -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - [[package]] name = "serde" version = "1.0.228" @@ -2401,16 +2402,16 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.29.11" +version = "0.30.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd727fc423c2060f6c92d9534cef765c65a6ed3f428a03d7def74a8c4348e666" +checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3" dependencies = [ "cfg-if", "core-foundation-sys", "libc", "ntapi", "once_cell", - "winapi", + "windows", ] [[package]] @@ -2815,6 +2816,25 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.62.2" diff --git a/Cargo.toml b/Cargo.toml index 70309e8..1bb5eac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,7 +45,7 @@ calamine = "0.22.1" wild = "2.2.0" fltk = { version = "=1.3.24", features = ["fltk-bundled"], optional = true } nom = "7.1.3" -polars = "0.35.4" +polars = "0.36.2" csv = "1.3.0" serde_json = { version = "=1.0.133", optional = true } holidays = { version = "0.1.0", default-features = false, features = ["PL"] } diff --git a/src/csvparser.rs b/src/csvparser.rs index afdba0d..b97fbe4 100644 --- a/src/csvparser.rs +++ b/src/csvparser.rs @@ -44,6 +44,7 @@ struct InvestmentTransactions { pub costs: Vec, pub gross: Vec, pub symbols: Vec>, + pub countries: Vec>, } #[derive(Default)] struct TransactionAccumulator { @@ -53,17 +54,25 @@ struct TransactionAccumulator { pub incomes: Vec, pub taxes: Vec, pub symbols: Vec>, + pub countries: Vec>, } #[derive(Debug, PartialEq)] pub struct RevolutTransactions { - pub dividend_transactions: Vec<(String, crate::Currency, crate::Currency, Option)>, + pub dividend_transactions: Vec<( + String, + crate::Currency, + crate::Currency, + Option, + Option, + )>, pub sold_transactions: Vec<( String, String, crate::Currency, crate::Currency, - Option, + Option, // company + Option, // country )>, pub crypto_transactions: Vec<(String, String, crate::Currency, crate::Currency)>, } @@ -190,7 +199,7 @@ fn extract_cash(cashline: &str) -> Result { fn sanitize_df(df: &DataFrame) -> DataFrame { if let Ok(col) = df.column("Description") { - if let Ok(utf) = col.utf8() { + if let Ok(utf) = col.str() { let vals: Vec = utf .into_iter() .map(|opt| { @@ -222,6 +231,7 @@ fn extract_dividends_transactions(df: &DataFrame) -> Result Result Result Result Result "Cost basis", "Gross proceeds", "Currency", + "Country", ]) } else if df .get_column_names() @@ -377,7 +410,21 @@ fn extract_sold_transactions(df: &DataFrame) -> Result .copied() .unwrap_or("Fees"); - df.select([date_col, symbol_col, value_col, other_taxes_col, fees_col]) + let country_col = df + .get_column_names() + .iter() + .find(|&col| col.contains("Kraj") || col == &"Country") + .copied() + .unwrap_or("Country"); + + df.select([ + date_col, + symbol_col, + value_col, + other_taxes_col, + fees_col, + country_col, + ]) } else { df.select([ "Date acquired", @@ -386,6 +433,7 @@ fn extract_sold_transactions(df: &DataFrame) -> Result "Cost basis base currency", "Gross proceeds base currency", "Fees base currency", + "Country", ]) } .map_err(|_| "Error: Unable to select collumns in Revolut sold transactions")?; @@ -451,6 +499,18 @@ fn extract_sold_transactions(df: &DataFrame) -> Result .clone(); } + if let Some(country_col) = df_transactions + .get_column_names() + .iter() + .find(|&col| col.contains("Kraj")) + .map(|s| s.to_string()) + { + df_transactions = df_transactions + .rename(&country_col, "Country") + .expect("Unable to rename Kraj") + .clone(); + } + Ok(df_transactions) } @@ -531,7 +591,7 @@ fn extract_intrest_rate_transactions(df: &DataFrame) -> Result { + AnyValue::String(x) => { if x.contains("Odsetki brutto") || x.contains("Oprocentowanie brutto") || x.contains("Gross interest") @@ -592,10 +652,10 @@ fn extract_intrest_rate_transactions(df: &DataFrame) -> Result Result>, &'static str> { let symbol = df .column(col_name) - .map_err(|_| "Error: Unable to select Symbol")?; + .map_err(|_| "Error: Unable to select Symbol/Country")?; let mut symbols: Vec> = vec![]; let possible_symbols = symbol - .utf8() + .str() .map_err(|_| "Error: Unable to convert to utf8")?; possible_symbols.into_iter().try_for_each(|maybe_symbol| { @@ -621,9 +681,7 @@ fn parse_investment_pairs_transaction_dates( let mut sold_dates: Vec = vec![]; let mut acquire_dates: Vec = vec![]; - let possible_dates = date - .utf8() - .map_err(|_| "Error: Unable to convert to utf8")?; + let possible_dates = date.str().map_err(|_| "Error: Unable to convert to utf8")?; possible_dates.into_iter().try_for_each(|x| { if let Some(d) = x { @@ -703,9 +761,7 @@ fn parse_investment_transaction_dates( .column(col_name) .map_err(|_| "Error: Unable to select Date")?; let mut dates: Vec = vec![]; - let possible_dates = date - .utf8() - .map_err(|_| "Error: Unable to convert to utf8")?; + let possible_dates = date.str().map_err(|_| "Error: Unable to convert to utf8")?; possible_dates.into_iter().try_for_each(|x| { if let Some(d) = x { // Replace Polish month abbreviations with English ones @@ -752,7 +808,7 @@ fn parse_sold_incomes( .column(col) .map_err(|_| format!("Error: Unable to select column '{}'", col))?; let possible_incomes = moneyin - .utf8() + .str() .map_err(|_| format!("Error: Unable to convert column '{}' to utf8", col))?; possible_incomes @@ -768,7 +824,7 @@ fn parse_incomes(df: &DataFrame, col: &str) -> Result, Stri .column(col) .map_err(|_| format!("Error: Unable to select Money In column '{}'", col))?; let possible_incomes = moneyin - .utf8() + .str() .map_err(|_| format!("Error: Unable to convert column '{}' to utf8", col))?; possible_incomes @@ -791,7 +847,7 @@ fn parse_income_with_currency( .column(currency_col) .map_err(|_| "Error: Unable to select Currency column")?; let possible_currency = currency - .utf8() + .str() .map_err(|e| format!("Unable to convert to utf8. Error: {e}"))?; match moneyin.dtype() { DataType::Float64 => { @@ -809,9 +865,9 @@ fn parse_income_with_currency( Ok::<(), String>(()) })?; } - DataType::Utf8 => { + DataType::String => { let possible_incomes = moneyin - .utf8() + .str() .map_err(|e| format!("Unable to convert to utf8. Error: {e}"))?; possible_incomes @@ -852,6 +908,8 @@ fn process_tax_consolidated_data_v2( .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())); + ta.countries + .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); @@ -883,6 +941,9 @@ fn process_tax_consolidated_data_v2( ta.stock .symbols .extend(parse_symbols(&filtred_df, "Description, symbol and ISIN")?); + ta.stock + .countries + .extend(parse_symbols(&filtred_df, "Country")?); let (lcosts, lsells) = parse_sold_incomes(&filtred_df, "Value (of Sale, of Purchase)")?; ta.stock.gross.extend(lsells); let fees = parse_incomes(&filtred_df, "Fees")?; @@ -914,6 +975,7 @@ fn process_tax_consolidated_data_v2( ta.symbols .extend(parse_symbols(&filtred_df, "Description & symbol")?); + ta.countries.extend(parse_symbols(&filtred_df, "Country")?); // parse income let lincomes = parse_incomes(&filtred_df, "Gross dividend / income")?; @@ -953,6 +1015,8 @@ fn process_tax_consolidated_data( .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())); + ta.countries + .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); @@ -980,6 +1044,9 @@ fn process_tax_consolidated_data( ta.stock .symbols .extend(parse_symbols(&filtred_df, "Symbol")?); + ta.stock + .countries + .extend(parse_symbols(&filtred_df, "Country")?); let lcosts = parse_incomes(&filtred_df, "Cost basis base currency")?; ta.stock .gross @@ -1008,6 +1075,7 @@ fn process_tax_consolidated_data( .extend(parse_investment_transaction_dates(&filtred_df, "Date")?); ta.symbols.extend(parse_symbols(&filtred_df, "Symbol")?); + ta.countries.extend(parse_symbols(&filtred_df, "Country")?); // parse income let lincomes = parse_incomes(&filtred_df, "Gross amount base currency")?; @@ -1122,19 +1190,26 @@ fn process_tax_consolidated_statement_v2( /// Parse revolut CSV documents (savings account, trading, crypto) /// returns: ( -/// dividend transactions in a form: date, gross income, tax taken, company name (if available) +/// dividend transactions in a form: date, gross income, tax taken, +/// company name (if available), country (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, Option)> = - vec![]; + let mut dividend_transactions: Vec<( + String, + crate::Currency, + crate::Currency, + Option, + Option, + )> = vec![]; let mut sold_transactions: Vec<( String, String, crate::Currency, crate::Currency, Option, + Option, )> = vec![]; let mut crypto_transactions: Vec<(String, String, crate::Currency, crate::Currency)> = vec![]; @@ -1178,6 +1253,8 @@ pub fn parse_revolut_transactions(csvtoparse: &str) -> Result Result Result Result Result Result<(), &'static str> { + let dates = vec!["25 Aug 2023", "1 Sep 2023"]; + let countries = vec!["US", "DE"]; + let expected_symbols = countries + .iter() + .map(|s| Some(s.to_string())) + .collect::>>(); + + let input_date_series = Series::new("Date", dates); + let input_countries = Series::new("Country", countries); + + let df = DataFrame::new(vec![input_date_series, input_countries]) + .map_err(|_| "Error creating DataFrame")?; + + assert_eq!(parse_symbols(&df, "Country"), Ok(expected_symbols)); + Ok(()) + } + #[test] fn test_parse_transaction_dates_us() -> Result<(), String> { let description = vec!["odsetki", "odsetki"]; @@ -1818,7 +1928,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, _ => (), @@ -1856,18 +1966,21 @@ mod tests { crate::Currency::EUR(0.01), crate::Currency::EUR(0.00), None, + None, ), ( "01/04/24".to_owned(), crate::Currency::EUR(0.02), crate::Currency::EUR(0.00), None, + None, ), ( "12/31/24".to_owned(), crate::Currency::EUR(0.01), crate::Currency::EUR(0.00), None, + None, ), ], sold_transactions: vec![], @@ -1890,12 +2003,14 @@ mod tests { crate::Currency::EUR(0.26), crate::Currency::EUR(0.00), None, + None, ), ( "04/12/24".to_owned(), crate::Currency::EUR(0.24), crate::Currency::EUR(0.00), None, + None, ), // PLN interests ( @@ -1903,12 +2018,14 @@ mod tests { crate::Currency::PLN(0.86), crate::Currency::PLN(0.00), None, + None, ), ( "05/31/24".to_owned(), crate::Currency::PLN(1.26), crate::Currency::PLN(0.00), None, + None, ), // Euro dividends ( @@ -1916,6 +2033,7 @@ mod tests { crate::Currency::PLN(302.43), crate::Currency::PLN(302.43 - 222.65), Some("DE000A289XJ2".to_string()), + Some("DE".to_string()), ), // USD dividends ( @@ -1923,18 +2041,21 @@ mod tests { crate::Currency::PLN(617.00), crate::Currency::PLN(617.00 - 524.43), Some("TFC".to_string()), + Some("US".to_string()), ), ( "03/21/24".to_owned(), crate::Currency::PLN(259.17), crate::Currency::PLN(0.0), Some("AMCR".to_string()), + Some("JE".to_string()), ), ( "12/17/24".to_owned(), crate::Currency::PLN(903.35), crate::Currency::PLN(903.35 - 767.83), Some("EPR".to_string()), + Some("US".to_string()), ), ], sold_transactions: vec![ @@ -1944,6 +2065,7 @@ mod tests { crate::Currency::PLN(13037.94 + 65.94), crate::Currency::PLN(13348.22), Some("EU000A3K4DJ5".to_string()), + Some("BE".to_string()), ), ( "09/09/24".to_owned(), @@ -1951,6 +2073,7 @@ mod tests { crate::Currency::PLN(16097.86 + 81.41), crate::Currency::PLN(16477.91), Some("XS1218821756".to_string()), + Some("NL".to_string()), ), ( "11/20/23".to_owned(), @@ -1958,6 +2081,7 @@ mod tests { crate::Currency::PLN(19863.25 + 0.66), crate::Currency::PLN(22865.17), Some("XOM".to_string()), + Some("US".to_string()), ), ( "06/11/24".to_owned(), @@ -1965,6 +2089,7 @@ mod tests { crate::Currency::PLN(525.08 + 0.0), crate::Currency::PLN(624.00), Some("TFC".to_string()), + Some("US".to_string()), ), ( "10/23/23".to_owned(), @@ -1972,6 +2097,7 @@ mod tests { crate::Currency::PLN(835.88 + 0.03), crate::Currency::PLN(1046.20), Some("AMCR".to_string()), + Some("JE".to_string()), ), ( "08/22/24".to_owned(), @@ -1979,6 +2105,7 @@ mod tests { crate::Currency::PLN(25135.50 + 128.17), crate::Currency::PLN(26130.41), Some("US13607LNF66".to_string()), + Some("CA".to_string()), ), ], crypto_transactions: vec![], @@ -2003,72 +2130,84 @@ mod tests { crate::Currency::EUR(0.01), crate::Currency::EUR(0.00), None, + None, ), ( "01/30/26".to_owned(), crate::Currency::EUR(0.01), crate::Currency::EUR(0.00), None, + None, ), ( "02/03/26".to_owned(), crate::Currency::EUR(0.01), crate::Currency::EUR(0.00), None, + None, ), ( "02/06/26".to_owned(), crate::Currency::EUR(0.01), crate::Currency::EUR(0.00), None, + None, ), ( "02/09/26".to_owned(), crate::Currency::EUR(0.01), crate::Currency::EUR(0.00), None, + None, ), ( "02/13/26".to_owned(), crate::Currency::EUR(0.01), crate::Currency::EUR(0.00), None, + None, ), ( "02/16/26".to_owned(), crate::Currency::EUR(0.01), crate::Currency::EUR(0.00), None, + None, ), ( "02/19/26".to_owned(), crate::Currency::EUR(0.01), crate::Currency::EUR(0.00), None, + None, ), ( "02/23/26".to_owned(), crate::Currency::EUR(0.01), crate::Currency::EUR(0.00), None, + None, ), ( "02/25/26".to_owned(), crate::Currency::EUR(0.23), crate::Currency::EUR(0.00), None, + None, ), ( "02/26/26".to_owned(), crate::Currency::EUR(0.24), crate::Currency::EUR(0.00), None, + None, ), ( "02/27/26".to_owned(), crate::Currency::EUR(0.23), crate::Currency::EUR(0.00), None, + None, ), // PLN interests (Aion account) ( @@ -2076,30 +2215,35 @@ mod tests { crate::Currency::PLN(1.81), crate::Currency::PLN(0.00), None, + None, ), ( "01/01/26".to_owned(), crate::Currency::PLN(4.39), crate::Currency::PLN(0.00), None, + None, ), ( "01/02/26".to_owned(), crate::Currency::PLN(1.81), crate::Currency::PLN(0.00), None, + None, ), ( "01/02/26".to_owned(), crate::Currency::PLN(4.40), crate::Currency::PLN(0.00), None, + None, ), ( "01/03/26".to_owned(), crate::Currency::PLN(1.79), crate::Currency::PLN(0.00), None, + None, ), // USD dividends - CSV: $112.69 (405.87 PLN), parser returns USD amount ( @@ -2107,24 +2251,28 @@ mod tests { crate::Currency::USD(112.69), crate::Currency::USD(16.90), Some("Best Buy dividend".to_string()), + Some("US".to_string()), ), ( "01/07/26".to_owned(), crate::Currency::USD(27.32), crate::Currency::USD(6.83), Some("Canadian Natural Resources dividend".to_string()), + Some("CA".to_string()), ), ( "01/09/26".to_owned(), crate::Currency::USD(25.50), crate::Currency::USD(3.82), Some("Dentsply dividend".to_string()), + Some("US".to_string()), ), ( "01/09/26".to_owned(), crate::Currency::USD(68.89), crate::Currency::USD(0.00), Some("Ambev dividend".to_string()), + Some("US".to_string()), ), // EUR dividend - CSV: €130.75 (554.74 PLN), parser returns EUR amount ( @@ -2132,6 +2280,7 @@ mod tests { crate::Currency::EUR(130.75), crate::Currency::EUR(19.61), Some("Ahold Delhaize N.V. dividend".to_string()), + Some("NL".to_string()), ), ], sold_transactions: vec![ @@ -2147,6 +2296,7 @@ mod tests { crate::Currency::USD(20000.13), crate::Currency::USD(10961.04), Some("ConAgra Foods CAG (US2058871029)".to_string()), + Some("US".to_string()), ), // Sale: Jan 16, 2026, Purchase: Feb 26, 2025 // CSV: +US$328.85, -US$500, no fee @@ -2156,6 +2306,7 @@ mod tests { crate::Currency::USD(500.00), crate::Currency::USD(328.85), Some("ConAgra Foods CAG (US2058871029)".to_string()), + Some("US".to_string()), ), // Sale: Jan 16, 2026, Purchase: Apr 9, 2025 // CSV: +US$668.10, -US$981.99, Fee: US$0.01 (0.03 PLN) @@ -2166,6 +2317,7 @@ mod tests { crate::Currency::USD(982.00), crate::Currency::USD(668.10), Some("ConAgra Foods CAG (US2058871029)".to_string()), + Some("US".to_string()), ), // Dentsply - Sale: Mar 2, 2026, Purchase: Feb 26, 2025 // CSV: +US$2,298.25, -US$3,000, Fee: US$0.03 (0.10 PLN) @@ -2176,6 +2328,7 @@ mod tests { crate::Currency::USD(3000.03), crate::Currency::USD(2298.25), Some("Dentsply XRAY (US24906P1093)".to_string()), + Some("US".to_string()), ), // IBM - Sale: Mar 4, 2026, Purchase: Feb 24, 2026 // CSV: +US$747.61, -US$698.24, Fee: US$1.74 (6.23 PLN) + US$0.01 (0.03 PLN) @@ -2186,6 +2339,7 @@ mod tests { crate::Currency::USD(699.99), crate::Currency::USD(747.61), Some("IBM IBM (US4592001014)".to_string()), + Some("US".to_string()), ), ], crypto_transactions: vec![], @@ -2212,18 +2366,21 @@ mod tests { crate::Currency::EUR(0.01), crate::Currency::EUR(0.00), None, + None, ), ( "01/30/26".to_owned(), crate::Currency::EUR(0.01), crate::Currency::EUR(0.00), None, + None, ), ( "02/03/26".to_owned(), crate::Currency::EUR(0.01), crate::Currency::EUR(0.00), None, + None, ), // PLN interests (Aion account) ( @@ -2231,24 +2388,28 @@ mod tests { crate::Currency::PLN(1.81), crate::Currency::PLN(0.00), None, + None, ), ( "01/01/26".to_owned(), crate::Currency::PLN(4.39), crate::Currency::PLN(0.00), None, + None, ), ( "01/02/26".to_owned(), crate::Currency::PLN(1.81), crate::Currency::PLN(0.00), None, + None, ), ( "01/02/26".to_owned(), crate::Currency::PLN(4.40), crate::Currency::PLN(0.00), None, + None, ), // USD dividends from Polish CSV with non-breaking spaces in headers ( @@ -2256,36 +2417,42 @@ mod tests { crate::Currency::USD(112.69), crate::Currency::USD(16.90), Some("Best Buy dividend".to_string()), + Some("US".to_string()), ), ( "01/07/26".to_owned(), crate::Currency::USD(27.32), crate::Currency::USD(6.83), Some("Canadian Natural Resources dividend".to_string()), + Some("CA".to_string()), ), ( "01/09/26".to_owned(), crate::Currency::USD(25.50), crate::Currency::USD(3.82), Some("Dentsply dividend".to_string()), + Some("US".to_string()), ), ( "01/09/26".to_owned(), crate::Currency::USD(68.89), crate::Currency::USD(0.00), Some("Ambev dividend".to_string()), + Some("US".to_string()), ), ( "01/15/26".to_owned(), crate::Currency::USD(235.48), crate::Currency::USD(35.32), Some("EPR Properties dividend".to_string()), + Some("US".to_string()), ), ( "02/02/26".to_owned(), crate::Currency::USD(31.79), crate::Currency::USD(4.77), Some("Edison International dividend".to_string()), + Some("US".to_string()), ), ], sold_transactions: vec![ @@ -2298,6 +2465,7 @@ mod tests { crate::Currency::USD(20000.13), crate::Currency::USD(10961.04), Some("ConAgra Foods CAG (US2058871029)".to_string()), + Some("US".to_string()), ), ( "02/26/25".to_owned(), @@ -2305,6 +2473,7 @@ mod tests { crate::Currency::USD(500.00), crate::Currency::USD(328.85), Some("ConAgra Foods CAG (US2058871029)".to_string()), + Some("US".to_string()), ), ( "04/09/25".to_owned(), @@ -2312,6 +2481,7 @@ mod tests { crate::Currency::USD(982.00), crate::Currency::USD(668.10), Some("ConAgra Foods CAG (US2058871029)".to_string()), + Some("US".to_string()), ), ], crypto_transactions: vec![], @@ -2332,24 +2502,28 @@ mod tests { crate::Currency::PLN(2.80), crate::Currency::PLN(0.68), Some("QDVY".to_string()), + Some("IE".to_string()), ), ( "06/20/24".to_owned(), crate::Currency::PLN(0.34), crate::Currency::PLN(0.08), Some("EXI2".to_string()), + Some("DE".to_string()), ), ( "06/28/24".to_owned(), crate::Currency::PLN(3.79), crate::Currency::PLN(0.94), Some("IS3K".to_string()), + Some("IE".to_string()), ), ( "07/01/24".to_owned(), crate::Currency::PLN(1.07), crate::Currency::PLN(0.25), Some("IBCD".to_string()), + Some("IE".to_string()), ), ], sold_transactions: vec![], @@ -2373,54 +2547,63 @@ mod tests { crate::Currency::PLN(2.80), crate::Currency::PLN(0.68), Some("QDVY".to_string()), + Some("IE".to_string()), ), ( "06/20/24".to_owned(), crate::Currency::PLN(0.34), crate::Currency::PLN(0.08), Some("EXI2".to_string()), + Some("DE".to_string()), ), ( "06/28/24".to_owned(), crate::Currency::PLN(3.79), crate::Currency::PLN(0.94), Some("IS3K".to_string()), + Some("IE".to_string()), ), ( "07/01/24".to_owned(), crate::Currency::PLN(1.07), crate::Currency::PLN(0.25), Some("IBCD".to_string()), + Some("IE".to_string()), ), ( "09/27/24".to_owned(), crate::Currency::PLN(1.02), crate::Currency::PLN(0.25), Some("IBCD".to_string()), + Some("IE".to_string()), ), ( "09/27/24".to_owned(), crate::Currency::PLN(1.71), crate::Currency::PLN(0.42), Some("IUSU".to_string()), + Some("IE".to_string()), ), ( "11/29/24".to_owned(), crate::Currency::PLN(2.92), crate::Currency::PLN(0.73), Some("QDVY".to_string()), + Some("IE".to_string()), ), ( "12/17/24".to_owned(), crate::Currency::PLN(0.04), crate::Currency::PLN(0.0), Some("EXI2".to_string()), + Some("DE".to_string()), ), ( "12/31/24".to_owned(), crate::Currency::PLN(1.07), crate::Currency::PLN(0.25), Some("IBCD".to_string()), + Some("IE".to_string()), ), ], sold_transactions: vec![], @@ -2444,96 +2627,112 @@ mod tests { crate::Currency::PLN(617.00), crate::Currency::PLN(92.57), Some("TFC".to_string()), + Some("US".to_string()), ), ( "03/21/24".to_owned(), crate::Currency::PLN(259.17), crate::Currency::PLN(0.0), Some("AMCR".to_string()), + Some("JE".to_string()), ), ( "03/25/24".to_owned(), crate::Currency::PLN(212.39), crate::Currency::PLN(31.87), Some("PXD".to_string()), + Some("US".to_string()), ), ( "05/16/24".to_owned(), crate::Currency::PLN(700.17), crate::Currency::PLN(105.04), Some("EPR".to_string()), + Some("US".to_string()), ), ( "05/31/24".to_owned(), crate::Currency::PLN(875.82), crate::Currency::PLN(131.38), Some("UPS".to_string()), + Some("US".to_string()), ), ( "06/03/24".to_owned(), crate::Currency::PLN(488.26), crate::Currency::PLN(73.25), Some("ABR".to_string()), + Some("US".to_string()), ), ( "06/04/24".to_owned(), crate::Currency::PLN(613.2), crate::Currency::PLN(92.00), Some("TFC".to_string()), + Some("US".to_string()), ), ( "06/11/24".to_owned(), crate::Currency::PLN(186.16), crate::Currency::PLN(27.92), Some("XOM".to_string()), + Some("US".to_string()), ), ( "06/13/24".to_owned(), crate::Currency::PLN(264.74), crate::Currency::PLN(0.00), Some("AMCR".to_string()), + Some("JE".to_string()), ), ( "06/18/24".to_owned(), crate::Currency::PLN(858.33), crate::Currency::PLN(128.74), Some("EPR".to_string()), + Some("US".to_string()), ), ( "07/12/24".to_owned(), crate::Currency::PLN(421.5), crate::Currency::PLN(63.23), Some("BBY".to_string()), + Some("US".to_string()), ), ( "07/16/24".to_owned(), crate::Currency::PLN(834.55), crate::Currency::PLN(125.18), Some("EPR".to_string()), + Some("US".to_string()), ), ( "08/16/24".to_owned(), crate::Currency::PLN(834.79), crate::Currency::PLN(125.23), Some("EPR".to_string()), + Some("US".to_string()), ), ( "08/26/24".to_owned(), crate::Currency::PLN(302.43), crate::Currency::PLN(79.77), Some("DE000A289XJ2".to_string()), + Some("DE".to_string()), ), ( "08/29/24".to_owned(), crate::Currency::PLN(801.25), crate::Currency::PLN(0.0), Some("BMO".to_string()), + Some("CA".to_string()), ), ( "08/30/24".to_owned(), crate::Currency::PLN(872.56), crate::Currency::PLN(130.90), Some("CAG".to_string()), + Some("US".to_string()), ), ], sold_transactions: vec![( @@ -2542,6 +2741,7 @@ mod tests { crate::Currency::USD(5000.0), crate::Currency::USD(5804.62), Some("XOM".to_string()), + Some("US".to_string()), )], crypto_transactions: vec![], }); @@ -2565,114 +2765,133 @@ mod tests { crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), None, + None, ), ( "12/13/23".to_owned(), crate::Currency::PLN(0.20), crate::Currency::PLN(0.00), None, + None, ), ( "12/15/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), None, + None, ), ( "12/16/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), None, + None, ), ( "12/17/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), None, + None, ), ( "12/18/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), None, + None, ), ( "12/19/23".to_owned(), crate::Currency::PLN(0.41), crate::Currency::PLN(0.00), None, + None, ), ( "12/20/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), None, + None, ), ( "12/21/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), None, + None, ), ( "12/22/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), None, + None, ), ( "12/23/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), None, + None, ), ( "12/24/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), None, + None, ), ( "12/25/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), None, + None, ), ( "12/26/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), None, + None, ), ( "12/27/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), None, + None, ), ( "12/28/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), None, + None, ), ( "12/29/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), None, + None, ), ( "12/30/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), None, + None, ), ( "12/31/23".to_owned(), crate::Currency::PLN(0.21), crate::Currency::PLN(0.00), None, + None, ), ], sold_transactions: vec![], @@ -2695,18 +2914,21 @@ mod tests { crate::Currency::USD(-0.02), crate::Currency::USD(0.00), None, + None, ), ( "12/01/23".to_owned(), crate::Currency::USD(-0.51), crate::Currency::USD(0.00), None, + None, ), ( "12/14/23".to_owned(), crate::Currency::USD(2.94), crate::Currency::USD(0.00), Some("AMCR".to_string()), + None, ), ], sold_transactions: vec![], @@ -2733,12 +2955,14 @@ mod tests { crate::Currency::EUR(0.01), crate::Currency::EUR(0.00), None, + None, ), ( "03/23/25".to_owned(), crate::Currency::EUR(0.01), crate::Currency::EUR(0.00), None, + None, ), ]; assert_eq!(dividends, expected_result); diff --git a/src/gui.rs b/src/gui.rs index 789aca9..b94ce41 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,false, false) { + } = match run_taxation(&rd, file_names, etradeTaxReturnHelper::ReportMode::None, 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 e5cdd9a..b3e9385 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,8 +14,9 @@ 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, create_per_company_report, reconstruct_sold_transactions, - verify_dividends_transactions, verify_interests_transactions, verify_transactions, + create_detailed_sold_transactions, create_per_company_report, create_per_country_report, + reconstruct_sold_transactions, verify_dividends_transactions, verify_interests_transactions, + verify_transactions, }; #[derive(Debug, PartialEq, PartialOrd, Copy, Clone)] @@ -50,6 +51,14 @@ impl Currency { } } +/// If taxation report should be extended +/// with aggregation of computed data per country or per company etc.. +pub enum ReportMode { + None, + PerCompany, + PerCountry, +} + /// #[derive(Debug, PartialEq, Eq, Hash)] pub enum Exchange { @@ -66,6 +75,7 @@ pub struct Transaction { pub exchange_rate_date: String, pub exchange_rate: f32, pub company: Option, + pub country: Option, } impl Transaction { @@ -116,8 +126,7 @@ pub struct SoldTransaction { pub exchange_rate_acquisition_date: String, pub exchange_rate_acquisition: f32, pub company: Option, - // TODO - //pub country : Option, + pub country: Option, } impl SoldTransaction { @@ -372,20 +381,30 @@ pub fn validate_file_names(files: &Vec) -> Result<(), String> { pub fn run_taxation( rd: &Box, names: Vec, - per_company: bool, + report_mode: ReportMode, multiyear: bool, ) -> Result { validate_file_names(&names)?; let mut parsed_interests_transactions: Vec<(String, f32, f32)> = vec![]; - let mut parsed_div_transactions: Vec<(String, f32, f32, Option)> = vec![]; - let mut parsed_sold_transactions: Vec<(String, String, f32, f32, f32, Option)> = vec![]; + let mut parsed_div_transactions: Vec<(String, f32, f32, Option, Option)> = + vec![]; + let mut parsed_sold_transactions: Vec<( + String, + String, + f32, + f32, + f32, + Option, + Option, + )> = vec![]; let mut parsed_gain_and_losses: Vec<(String, String, f32, f32, f32)> = vec![]; let mut parsed_revolut_dividends_transactions: Vec<( String, Currency, Currency, Option, + Option, )> = vec![]; let mut parsed_revolut_sold_transactions: Vec<( String, @@ -393,6 +412,7 @@ pub fn run_taxation( Currency, Currency, Option, + Option, )> = vec![]; // 1. Parse PDF,XLSX and CSV documents to get list of transactions @@ -453,14 +473,14 @@ pub fn run_taxation( }); parsed_div_transactions .iter() - .for_each(|(trade_date, _, _, _)| { + .for_each(|(trade_date, _, _, _, _)| { let ex = Exchange::USD(trade_date.clone()); if dates.contains_key(&ex) == false { dates.insert(ex, None); } }); detailed_sold_transactions.iter().for_each( - |(trade_date, settlement_date, acquisition_date, _, _, _)| { + |(trade_date, settlement_date, acquisition_date, _, _, _, _)| { let ex = Exchange::USD(trade_date.clone()); if dates.contains_key(&ex) == false { dates.insert(ex, None); @@ -477,14 +497,14 @@ pub fn run_taxation( ); parsed_revolut_dividends_transactions .iter() - .for_each(|(trade_date, gross, _, _)| { + .for_each(|(trade_date, gross, _, _, _)| { let ex = gross.derive_exchange(trade_date.clone()); if dates.contains_key(&ex) == false { dates.insert(ex, None); } }); parsed_revolut_sold_transactions.iter().for_each( - |(acquired_date, sold_date, cost, gross, _)| { + |(acquired_date, sold_date, cost, gross, _, _)| { let ex = cost.derive_exchange(acquired_date.clone()); if dates.contains_key(&ex) == false { dates.insert(ex, None); @@ -507,16 +527,30 @@ pub fn run_taxation( let revolut_sold_transactions = create_detailed_revolut_sold_transactions(parsed_revolut_sold_transactions, &dates)?; - if per_company { - let per_company_report = create_per_company_report( - &interests, - &transactions, - &sold_transactions, - &revolut_dividends_transactions, - &revolut_sold_transactions, - )?; - - println!("{}", per_company_report); + match report_mode { + ReportMode::PerCompany => { + let per_company_report = create_per_company_report( + &interests, + &transactions, + &sold_transactions, + &revolut_dividends_transactions, + &revolut_sold_transactions, + )?; + + println!("{}", per_company_report); + } + ReportMode::PerCountry => { + let per_country_report = create_per_country_report( + &interests, + &transactions, + &sold_transactions, + &revolut_dividends_transactions, + &revolut_sold_transactions, + )?; + + println!("{}", per_country_report); + } + ReportMode::None => (), } let (gross_interests, _) = compute_div_taxation(&interests); @@ -611,6 +645,7 @@ mod tests { exchange_rate_date: "N/A".to_string(), exchange_rate: 4.0, company: Some("INTEL CORP".to_owned()), + country: Some("US".to_string()), }]; assert_eq!(compute_div_taxation(&transactions), (400.0, 100.0)); Ok(()) @@ -627,6 +662,7 @@ mod tests { exchange_rate_date: "N/A".to_string(), exchange_rate: 4.0, company: Some("INTEL CORP".to_owned()), + country: Some("US".to_string()), }, Transaction { transaction_date: "N/A".to_string(), @@ -635,6 +671,7 @@ mod tests { exchange_rate_date: "N/A".to_string(), exchange_rate: 3.5, company: Some("INTEL CORP".to_owned()), + country: Some("US".to_string()), }, ]; assert_eq!( @@ -653,6 +690,7 @@ mod tests { exchange_rate_date: "N/A".to_string(), exchange_rate: 1.0, company: None, + country: None, }, Transaction { transaction_date: "04/11/21".to_string(), @@ -661,6 +699,7 @@ mod tests { exchange_rate_date: "N/A".to_string(), exchange_rate: 1.0, company: None, + country: None, }, ]; assert_eq!( @@ -680,6 +719,7 @@ mod tests { exchange_rate_date: "02/28/21".to_string(), exchange_rate: 2.0, company: None, + country: None, }, Transaction { transaction_date: "04/11/21".to_string(), @@ -688,6 +728,7 @@ mod tests { exchange_rate_date: "04/10/21".to_string(), exchange_rate: 3.0, company: None, + country: None, }, ]; assert_eq!( @@ -711,6 +752,7 @@ mod tests { exchange_rate_acquisition_date: "N/A".to_string(), exchange_rate_acquisition: 6.0, company: Some("TFC".to_owned()), + country: Some("US".to_string()), }]; assert_eq!( compute_sold_taxation(&transactions), @@ -734,6 +776,7 @@ mod tests { exchange_rate_acquisition_date: "N/A".to_string(), exchange_rate_acquisition: 6.0, company: Some("PXD".to_owned()), + country: Some("US".to_string()), }, SoldTransaction { trade_date: "N/A".to_string(), @@ -746,6 +789,7 @@ mod tests { exchange_rate_acquisition_date: "N/A".to_string(), exchange_rate_acquisition: 3.0, company: Some("TFC".to_owned()), + country: Some("US".to_string()), }, ]; assert_eq!( diff --git a/src/main.rs b/src/main.rs index 3ddad3e..6eac0ca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,6 +52,14 @@ fn create_cmd_line_pattern(myapp: Command) -> Command { .help("Enable per-company mode") .action(clap::ArgAction::SetTrue) ) + .arg( + Arg::new("per-country") + .long("per-country") + .help("Enable per-country mode") + .action(clap::ArgAction::SetTrue) + .conflicts_with("per-company"), // either per-company is active or per-country. Not + // both + ) .arg( Arg::new("multiyear") .long("multiyear") @@ -107,18 +115,21 @@ fn main() { let pdfnames: Vec = pdfnames.map(|x| x.to_string()).collect(); + let report_mode = if matches.get_flag("per-company") { + etradeTaxReturnHelper::ReportMode::PerCompany + } else if matches.get_flag("per-country") { + etradeTaxReturnHelper::ReportMode::PerCountry + } else { + etradeTaxReturnHelper::ReportMode::None + }; + let TaxCalculationResult { gross_income: gross_div, tax: tax_div, gross_sold, cost_sold, .. - } = match run_taxation( - &rd, - pdfnames, - matches.get_flag("per-company"), - matches.get_flag("multiyear"), - ) { + } = match run_taxation(&rd, pdfnames, report_mode, matches.get_flag("multiyear")) { Ok(res) => res, Err(msg) => panic!("\nError: Unable to compute taxes. \n\nDetails: {msg}"), }; @@ -271,6 +282,51 @@ mod tests { }; Ok(()) } + #[test] + fn test_cmdline_per_country() -> 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_country = matches.get_flag("per-country"); + match per_country { + 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-country", + "data/example.pdf", + ]); + let per_country = matches.get_flag("per-country"); + match per_country { + true => (), + false => { + return Err(clap::error::Error::::new( + clap::error::ErrorKind::InvalidValue, + )) + } + }; + let myapp = Command::new("E-trade tax helper"); + let res = create_cmd_line_pattern(myapp).try_get_matches_from(vec![ + "mytest", + "--per-country", + "--per-company", + "data/example.pdf", + ]); + assert!(res.is_err()); + assert_eq!( + res.unwrap_err().kind(), + clap::error::ErrorKind::ArgumentConflict + ); + + Ok(()) + } #[test] fn test_cmdline_multiyear() -> Result<(), clap::Error> { @@ -374,7 +430,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, false, false) { + match etradeTaxReturnHelper::run_taxation( + &rd, + pdfnames, + etradeTaxReturnHelper::ReportMode::None, + false, + ) { Ok(_) => panic!("Expected an error from run_taxation, but got Ok"), Err(_) => Ok(()), // Expected error, test passes } @@ -395,7 +456,12 @@ 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, false, false) { + match etradeTaxReturnHelper::run_taxation( + &rd, + pdfnames, + etradeTaxReturnHelper::ReportMode::None, + false, + ) { Ok(TaxCalculationResult { gross_income: gross_div, tax: tax_div, @@ -428,7 +494,12 @@ 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, false, false) { + match etradeTaxReturnHelper::run_taxation( + &rd, + pdfnames, + etradeTaxReturnHelper::ReportMode::None, + false, + ) { Ok(TaxCalculationResult { gross_income: gross_div, tax: tax_div, @@ -461,7 +532,12 @@ 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, false, false) { + match etradeTaxReturnHelper::run_taxation( + &rd, + pdfnames, + etradeTaxReturnHelper::ReportMode::None, + false, + ) { Ok(TaxCalculationResult { gross_income: gross_div, tax: tax_div, @@ -495,7 +571,12 @@ 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, false, false) { + match etradeTaxReturnHelper::run_taxation( + &rd, + pdfnames, + etradeTaxReturnHelper::ReportMode::None, + false, + ) { Ok(TaxCalculationResult { gross_income: gross_div, tax: tax_div, @@ -526,7 +607,12 @@ 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, false, false) { + match etradeTaxReturnHelper::run_taxation( + &rd, + pdfnames, + etradeTaxReturnHelper::ReportMode::None, + false, + ) { Ok(TaxCalculationResult { gross_income: gross_div, tax: tax_div, diff --git a/src/pdfparser.rs b/src/pdfparser.rs index 4522036..33d5fc0 100644 --- a/src/pdfparser.rs +++ b/src/pdfparser.rs @@ -329,7 +329,15 @@ fn create_trade_parsing_sequence(sequence: &mut std::collections::VecDeque>, transaction_dates: &mut Vec, -) -> Option<(String, String, f32, f32, f32, Option)> { +) -> Option<( + String, + String, + f32, + f32, + f32, + Option, + Option, +)> { let symbol = transaction .next() .unwrap() @@ -382,6 +390,7 @@ fn yield_sold_transaction( price, amount_sold, Some(symbol), + Some("US".to_string()), )) } @@ -462,8 +471,16 @@ 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, Option)>, + div_transactions: &mut Vec<(String, f32, f32, Option, Option)>, + sold_transactions: &mut Vec<( + String, + String, + f32, + f32, + f32, + Option, + Option, + )>, actual_string: &pdf::primitive::PdfString, transaction_dates: &mut Vec, processed_sequence: &mut Vec>, @@ -590,6 +607,7 @@ fn process_transaction( gross_us, 0.0, // No tax info yet. It will be added later in Tax section Some(symbol), + Some("US".to_string()), // All e-trade companies INTC, AMD are from US )); log::info!("Completed parsing Dividend transaction"); } @@ -689,8 +707,16 @@ fn parse_account_statement<'a, I>( ) -> Result< ( Vec<(String, f32, f32)>, - Vec<(String, f32, f32, Option)>, - Vec<(String, String, f32, f32, f32, Option)>, + Vec<(String, f32, f32, Option, Option)>, + Vec<( + String, + String, + f32, + f32, + f32, + Option, + Option, + )>, Vec<(String, String, i32, f32, f32, f32, f32, f32)>, ), String, @@ -699,8 +725,16 @@ where I: Iterator>, { 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, Option)> = vec![]; + let mut div_transactions: Vec<(String, f32, f32, Option, Option)> = vec![]; + let mut sold_transactions: Vec<( + String, + String, + f32, + f32, + f32, + Option, + 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> = @@ -814,8 +848,16 @@ pub fn parse_statement( ) -> Result< ( Vec<(String, f32, f32)>, - Vec<(String, f32, f32, Option)>, - Vec<(String, String, f32, f32, f32, Option)>, + Vec<(String, f32, f32, Option, Option)>, + Vec<( + String, + String, + f32, + f32, + f32, + Option, + Option, + )>, Vec<(String, String, i32, f32, f32, f32, f32, f32)>, ), String, @@ -1121,7 +1163,8 @@ mod tests { "12/1/23".to_owned(), 386.50, 57.98, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()), ),], vec![( "12/21/23".to_owned(), @@ -1129,7 +1172,8 @@ mod tests { 82.0, 46.45, 3808.86, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()), )], vec![] ))) @@ -1175,13 +1219,15 @@ mod tests { "6/3/24".to_owned(), 57.25, 8.59, - Some("INTEL CORP".to_owned()) + Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), // Dividends date, gross, tax_us ( "3/1/24".to_owned(), 380.25, 57.04, - Some("INTEL CORP".to_owned()) + Some("INTEL CORP".to_owned()), + Some("US".to_string()), ) ], vec![ @@ -1191,7 +1237,8 @@ mod tests { 30.0, 22.5, 674.98, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()), ), ( "12/5/24".to_owned(), @@ -1199,7 +1246,8 @@ mod tests { 55.0, 21.96, 1207.76, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()), ), ( "11/1/24".to_owned(), @@ -1207,7 +1255,8 @@ mod tests { 15.0, 23.32, 349.79, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()), ), ( "9/3/24".to_owned(), @@ -1215,7 +1264,8 @@ mod tests { 17.0, 21.53, 365.99, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()), ), // Sold ( "9/9/24".to_owned(), @@ -1223,7 +1273,8 @@ mod tests { 14.0, 18.98, 265.71, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()), ), ( "8/5/24".to_owned(), @@ -1231,7 +1282,8 @@ mod tests { 14.0, 20.21, 282.93, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()), ), ( "8/20/24".to_owned(), @@ -1239,7 +1291,8 @@ mod tests { 328.0, 21.0247, 6895.89, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()) ), ( "7/31/24".to_owned(), @@ -1247,7 +1300,8 @@ mod tests { 151.0, 30.44, 4596.31, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()) ), ( "6/3/24".to_owned(), @@ -1255,7 +1309,8 @@ mod tests { 14.0, 31.04, 434.54, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()) ), ( "5/1/24".to_owned(), @@ -1263,7 +1318,8 @@ mod tests { 126.0, 30.14, 3797.6, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()) ), ( "5/1/24".to_owned(), @@ -1271,7 +1327,8 @@ mod tests { 124.0, 30.14, 3737.33, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()) ), ( "5/1/24".to_owned(), @@ -1279,7 +1336,8 @@ mod tests { 89.0, 30.6116, 2724.4, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()) ), ( "5/2/24".to_owned(), @@ -1287,7 +1345,8 @@ mod tests { 182.0, 30.56, 5561.87, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()) ), ( "5/3/24".to_owned(), @@ -1295,7 +1354,8 @@ mod tests { 440.0, 30.835, 13567.29, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()) ), ( "5/3/24".to_owned(), @@ -1303,7 +1363,8 @@ mod tests { 198.0, 30.835, 6105.28, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()) ), ( "5/3/24".to_owned(), @@ -1311,7 +1372,8 @@ mod tests { 146.0, 30.8603, 4505.56, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()) ), ( "5/3/24".to_owned(), @@ -1319,7 +1381,8 @@ mod tests { 145.0, 30.8626, 4475.04, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()) ), ( "5/3/24".to_owned(), @@ -1327,7 +1390,8 @@ mod tests { 75.0, 30.815, 2311.11, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()) ), ( "5/6/24".to_owned(), @@ -1335,7 +1399,8 @@ mod tests { 458.0, 31.11, 14248.26, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()) ), ( "5/31/24".to_owned(), @@ -1343,7 +1408,8 @@ mod tests { 18.0, 30.22, 543.94, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()) ), ( "4/3/24".to_owned(), @@ -1351,7 +1417,8 @@ mod tests { 31.0, 40.625, 1259.36, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()) ), ( "4/11/24".to_owned(), @@ -1359,7 +1426,8 @@ mod tests { 209.0, 37.44, 7824.89, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()) ), ( "4/11/24".to_owned(), @@ -1367,7 +1435,8 @@ mod tests { 190.0, 37.44, 7113.54, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()) ), ( "4/16/24".to_owned(), @@ -1375,7 +1444,8 @@ mod tests { 310.0, 36.27, 11243.61, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()) ), ( "4/29/24".to_owned(), @@ -1383,7 +1453,8 @@ mod tests { 153.0, 31.87, 4876.07, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()) ), ( "4/29/24".to_owned(), @@ -1391,7 +1462,8 @@ mod tests { 131.0, 31.87, 4174.93, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()) ), ( "4/29/24".to_owned(), @@ -1399,7 +1471,8 @@ mod tests { 87.0, 31.87, 2772.66, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()) ), ( "3/11/24".to_owned(), @@ -1407,7 +1480,8 @@ mod tests { 38.0, 43.85, 1666.28, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()) ), ( "2/20/24".to_owned(), @@ -1415,7 +1489,8 @@ mod tests { 150.0, 43.9822, 6597.27, - Some("INTEL CORP".to_string()) + Some("INTEL CORP".to_string()), + Some("US".to_string()) ) ], vec![] @@ -1439,7 +1514,8 @@ mod tests { 72.0, 118.13, 8505.29, - Some("ADVANCED MICRO DEVICES".to_string()) + Some("ADVANCED MICRO DEVICES".to_string()), + Some("US".to_string()) ), ( "11/22/23".to_owned(), @@ -1447,43 +1523,14 @@ mod tests { 162.0, 122.4511, 19836.92, - Some("ADVANCED MICRO DEVICES".to_string()) + Some("ADVANCED MICRO DEVICES".to_string()), + Some("US".to_string()) ), ], vec![] )) ); - //TODO(jczaja): Renable reinvest dividends case as soon as you get some PDFs - //assert_eq!( - // parse_statement("data/example3.pdf"), - // ( - // vec![ - // ("06/01/21".to_owned(), 0.17, 0.03), - // ("06/01/21".to_owned(), 45.87, 6.88) - // ], - // vec![], - // vec![] - // ) - //); - - //assert_eq!( - // parse_statement("data/example5.pdf"), - // ( - // vec![], - // vec![], - // vec![( - // "04/11/22".to_owned(), - // "04/13/22".to_owned(), - // 1, - // 46.92, - // 46.92, - // 0.01, - // 0.01, - // 46.9 - // )] - // ) - //); Ok(()) } } diff --git a/src/transactions.rs b/src/transactions.rs index da2103f..284be10 100644 --- a/src/transactions.rs +++ b/src/transactions.rs @@ -39,11 +39,11 @@ pub fn verify_interests_transactions(transactions: &[(String, T, T)]) -> Resu /// Check if all dividends transaction come from the same year pub fn verify_dividends_transactions( - div_transactions: &[(String, T, T, Option)], + div_transactions: &[(String, T, T, Option, 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(()); @@ -54,7 +54,7 @@ pub fn verify_dividends_transactions( .map_err(|_| format!("Unable to parse transaction date: \"{transaction_date}\""))? .year(); let mut verification: Result<(), String> = Ok(()); - trans.try_for_each(|(tr_date, _, _, _)| { + trans.try_for_each(|(tr_date, _, _, _, _)| { let tr_year = chrono::NaiveDate::parse_from_str(tr_date, "%m/%d/%y") .map_err(|_| format!("Unable to parse transaction date: \"{tr_date}\""))? .year(); @@ -68,11 +68,11 @@ pub fn verify_dividends_transactions( } pub fn verify_transactions( - transactions: &[(String, String, T, T, Option)], + transactions: &[(String, String, T, T, Option, 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(()); @@ -83,7 +83,7 @@ pub fn verify_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(); @@ -107,9 +107,28 @@ pub fn verify_transactions( /// transfered on settlement date pub fn reconstruct_sold_transactions( - sold_transactions: &Vec<(String, String, f32, f32, f32, Option)>, + sold_transactions: &Vec<( + String, + String, + f32, + f32, + f32, + Option, + Option, + )>, gains_and_losses: &Vec<(String, String, f32, f32, f32)>, -) -> Result)>, String> { +) -> Result< + Vec<( + String, + String, + String, + f32, + f32, + Option, + Option, + )>, + String, +> { // Ok What do I need. // 1. trade date // 2. settlement date @@ -117,8 +136,15 @@ 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, + 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 @@ -133,7 +159,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, _, _, _, symbol) = sold_transactions.iter().find(|(trade_dt, _, _, _, income, _)|{ + let (_, settlement_date, _, _, _, symbol, country) = 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 @@ -155,6 +181,7 @@ pub fn reconstruct_sold_transactions( *inc, *cost_basis, symbol.clone(), + country.clone(), )); } @@ -162,14 +189,20 @@ pub fn reconstruct_sold_transactions( } pub fn create_detailed_revolut_transactions( - transactions: Vec<(String, crate::Currency, crate::Currency, Option)>, + transactions: Vec<( + String, + crate::Currency, + crate::Currency, + Option, + Option, + )>, dates: &std::collections::HashMap>, ) -> Result, &str> { let mut detailed_transactions: Vec = Vec::new(); transactions .iter() - .try_for_each(|(transaction_date, gross, tax, company)| { + .try_for_each(|(transaction_date, gross, tax, company, country)| { let (exchange_rate_date, exchange_rate) = dates [&gross.derive_exchange(transaction_date.clone())] .clone() @@ -182,6 +215,7 @@ pub fn create_detailed_revolut_transactions( exchange_rate_date, exchange_rate, company: company.clone(), + country: country.clone(), }; let msg = transaction.format_to_print("REVOLUT")?; @@ -214,6 +248,7 @@ pub fn create_detailed_interests_transactions( exchange_rate_date, exchange_rate, company: None, // No company info when interests are paid on money + country: None, // TODO: check interests country of origin }; let msg = transaction.format_to_print("INTERESTS")?; @@ -227,13 +262,12 @@ pub fn create_detailed_interests_transactions( } pub fn create_detailed_div_transactions( - transactions: Vec<(String, f32, f32, Option)>, + transactions: Vec<(String, f32, f32, Option, 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, company)| { + transactions.iter().try_for_each( + |(transaction_date, gross_us, tax_us, company, country)| { let (exchange_rate_date, exchange_rate) = dates [&crate::Exchange::USD(transaction_date.clone())] .clone() @@ -246,6 +280,7 @@ pub fn create_detailed_div_transactions( exchange_rate_date, exchange_rate, company: company.clone(), + country: country.clone(), }; let msg = transaction.format_to_print("DIV")?; @@ -254,7 +289,8 @@ pub fn create_detailed_div_transactions( log::info!("{}", msg); detailed_transactions.push(transaction); Ok::<(), &str>(()) - })?; + }, + )?; Ok(detailed_transactions) } @@ -268,12 +304,20 @@ 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, Option)>, + transactions: Vec<( + String, + String, + String, + f32, + f32, + Option, + 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, symbol)| { + |(trade_date, settlement_date, acquisition_date, income, cost_basis, symbol, country)| { let (exchange_rate_settlement_date, exchange_rate_settlement) = dates [&crate::Exchange::USD(settlement_date.clone())] .clone() @@ -294,6 +338,7 @@ pub fn create_detailed_sold_transactions( exchange_rate_acquisition_date, exchange_rate_acquisition, company: symbol.clone(), + country: country.clone(), }; let msg = transaction.format_to_print(""); @@ -314,12 +359,13 @@ pub fn create_detailed_revolut_sold_transactions( crate::Currency, crate::Currency, Option, + 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)| { + |(acquired_date, sold_date, cost_basis, gross_income, symbol, country)| { let (exchange_rate_settlement_date, exchange_rate_settlement) = dates [&gross_income.derive_exchange(sold_date.clone())] // TODO: settlement date??? .clone() @@ -340,6 +386,7 @@ pub fn create_detailed_revolut_sold_transactions( exchange_rate_acquisition_date, exchange_rate_acquisition, company: symbol.clone(), + country: country.clone(), }; let msg = transaction.format_to_print("REVOLUT "); @@ -424,6 +471,76 @@ pub(crate) fn create_per_company_report( .map_err(|_| "Unable to sort per company report dataframe") } +pub(crate) fn create_per_country_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_country_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_country_data + .entry(x.country.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_country_data + .entry(x.country.clone()) + .or_insert((0.0, 0.0, 0.0)); + entry.0 += x.income_us * x.exchange_rate_settlement; + // No tax from sold transactions + entry.2 += x.cost_basis * x.exchange_rate_acquisition; + }); + + // Convert my HashMap into DataFrame + let mut countries: Vec> = Vec::new(); + let mut gross: Vec = Vec::new(); + let mut tax: Vec = Vec::new(); + let mut cost: Vec = Vec::new(); + per_country_data + .iter() + .try_for_each(|(country, (gross_pl, tax_paid_in_us_pl, cost_pl))| { + log::info!( + "Country: {:?}, Gross PLN: {:.2}, Tax Paid in USD PLN: {:.2}, Cost PLN: {:.2}", + country, + gross_pl, + tax_paid_in_us_pl, + cost_pl + ); + countries.push(country.clone()); + gross.push(*gross_pl); + tax.push(*tax_paid_in_us_pl); + cost.push(*cost_pl); + + Ok::<(), &str>(()) + })?; + let series = vec![ + Series::new("Country", countries), + 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 country report dataframe")? + .sort(["Country"], false, true) + .map_err(|_| "Unable to sort per country report dataframe") +} + #[cfg(test)] mod tests { @@ -434,6 +551,50 @@ mod tests { (val * 10_000.0).round() / 10_000.0 } + #[test] + fn test_create_per_country_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, + country: 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, + country: None, + }, + ]; + let df = create_per_country_report(&input, &[], &[], &[], &[]) + .map_err(|e| format!("Error creating per country 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 country_col = df.column("Country").unwrap(); + assert_eq!(country_col.get(0).is_err(), false); // None company + let gross_col = df.column("Gross[PLN]").unwrap(); + assert_eq!( + round4(gross_col.get(0).unwrap().extract::().unwrap()), + round4(0.05 * 2.0 + 0.07 * 3.0) + ); + let cost_col = df.column("Cost[PLN]").unwrap(); + assert_eq!(cost_col.get(0).unwrap().extract::().unwrap(), 0.00); + let tax_col = df.column("Tax Paid in USD[PLN]").unwrap(); + assert_eq!(tax_col.get(0).unwrap().extract::().unwrap(), 0.00); + + Ok(()) + } + #[test] fn test_create_per_company_report_interests() -> Result<(), String> { let input = vec![ @@ -444,6 +605,7 @@ mod tests { exchange_rate_date: "02/28/21".to_string(), exchange_rate: 2.0, company: None, + country: None, }, Transaction { transaction_date: "04/11/21".to_string(), @@ -452,6 +614,7 @@ mod tests { exchange_rate_date: "04/10/21".to_string(), exchange_rate: 3.0, company: None, + country: None, }, ]; let df = create_per_company_report(&input, &[], &[], &[], &[]) @@ -475,6 +638,65 @@ mod tests { Ok(()) } + #[test] + fn test_create_per_country_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()), + country: Some("US".to_string()), + }, + 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()), + country: Some("US".to_string()), + }, + 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()), + country: Some("US".to_string()), + }, + ]; + let df = create_per_country_report(&[], &input, &[], &[], &[]) + .map_err(|e| format!("Error creating per country 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 country_col = df.column("Country").unwrap().str().unwrap(); + let gross_col = df.column("Gross[PLN]").unwrap(); + let tax_col = df.column("Tax Paid in USD[PLN]").unwrap(); + let us_index = match country_col.get(0) { + Some("US") => 0, + _ => return Err("Unexpected country name in first row".to_owned()), + }; + assert_eq!( + round4(gross_col.get(us_index).unwrap().extract::().unwrap()), + round4(100.0 * 3.0 + 126.0 * 2.0 + 10.0 * 100.0) + ); + assert_eq!( + tax_col.get(us_index).unwrap().extract::().unwrap(), + round4(25.0 * 3.0 + 10.0 * 2.0 + 10.0 * 0.0) + ); + + let cost_col = df.column("Cost[PLN]").unwrap(); + assert_eq!(cost_col.get(0).unwrap().extract::().unwrap(), 0.00); + + Ok(()) + } #[test] fn test_create_per_company_report_dividends() -> Result<(), String> { @@ -486,6 +708,7 @@ mod tests { exchange_rate_date: "04/10/21".to_string(), exchange_rate: 3.0, company: Some("INTEL CORP".to_owned()), + country: Some("US".to_string()), }, Transaction { transaction_date: "03/01/21".to_string(), @@ -494,6 +717,7 @@ mod tests { exchange_rate_date: "02/28/21".to_string(), exchange_rate: 2.0, company: Some("INTEL CORP".to_owned()), + country: Some("US".to_string()), }, Transaction { transaction_date: "03/11/21".to_string(), @@ -502,6 +726,7 @@ mod tests { exchange_rate_date: "02/28/21".to_string(), exchange_rate: 10.0, company: Some("ABEV".to_owned()), + country: Some("US".to_string()), }, ]; let df = create_per_company_report(&[], &input, &[], &[], &[]) @@ -511,7 +736,7 @@ mod tests { assert_eq!(df.height(), 2); assert_eq!(df.width(), 4); - let company_col = df.column("Company").unwrap().utf8().unwrap(); + let company_col = df.column("Company").unwrap().str().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) { @@ -557,6 +782,7 @@ mod tests { exchange_rate_acquisition_date: "02/28/21".to_string(), exchange_rate_acquisition: 5.0, company: Some("INTEL CORP".to_owned()), + country: Some("US".to_string()), }, SoldTransaction { trade_date: "06/01/21".to_string(), @@ -569,6 +795,7 @@ mod tests { exchange_rate_acquisition_date: "12/30/18".to_string(), exchange_rate_acquisition: 6.0, company: Some("INTEL CORP".to_owned()), + country: Some("US".to_string()), }, SoldTransaction { trade_date: "06/01/21".to_string(), @@ -581,6 +808,7 @@ mod tests { exchange_rate_acquisition_date: "12/30/18".to_string(), exchange_rate_acquisition: 6.0, company: Some("PXD".to_owned()), + country: Some("US".to_string()), }, ]; let df = create_per_company_report(&[], &[], &input, &[], &[]) @@ -590,7 +818,7 @@ mod tests { assert_eq!(df.height(), 2); assert_eq!(df.width(), 4); - let company_col = df.column("Company").unwrap().utf8().unwrap(); + let company_col = df.column("Company").unwrap().str().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) { @@ -622,6 +850,88 @@ mod tests { Ok(()) } + #[test] + fn test_create_per_country_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()), + country: Some("US".to_string()), + }, + 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()), + country: Some("US".to_string()), + }, + 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("BMO".to_owned()), + country: Some("CA".to_string()), + }, + ]; + let df = create_per_country_report(&[], &[], &input, &[], &[]) + .map_err(|e| format!("Error creating per country report: {}", e))?; + + // Solds are having company + assert_eq!(df.height(), 2); + assert_eq!(df.width(), 4); + + let country_col = df.column("Country").unwrap().str().unwrap(); + let gross_col = df.column("Gross[PLN]").unwrap(); + let cost_col = df.column("Cost[PLN]").unwrap(); + let (ca_index, us_index) = match country_col.get(0) { + Some("US") => (1, 0), + Some("CA") => (0, 1), + _ => return Err("Unexpected country name in first row".to_owned()), + }; + assert_eq!( + round4(gross_col.get(us_index).unwrap().extract::().unwrap()), + round4(20.0 * 2.5 + 25.0 * 4.0) + ); + assert_eq!( + round4(gross_col.get(ca_index).unwrap().extract::().unwrap()), + round4(20.0 * 4.0) + ); + assert_eq!( + cost_col.get(us_index).unwrap().extract::().unwrap(), + round4(20.0 * 5.0 + 10.0 * 6.0) + ); + assert_eq!( + cost_col.get(ca_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![ @@ -633,13 +943,21 @@ mod tests { #[test] fn test_revolut_sold_verification_false() -> Result<(), String> { - let transactions: Vec<(String, String, Currency, Currency, Option)> = vec![ + let transactions: Vec<( + String, + String, + Currency, + Currency, + Option, + 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()), + Some("US".to_string()), ), ( "06/01/21".to_string(), @@ -647,6 +965,7 @@ mod tests { Currency::PLN(10.0), Currency::PLN(2.0), Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), ]; assert_eq!( @@ -658,18 +977,20 @@ mod tests { #[test] fn test_dividends_verification_ok() -> Result<(), String> { - let transactions: Vec<(String, f32, f32, Option)> = vec![ + let transactions: Vec<(String, f32, f32, Option, Option)> = vec![ ( "06/01/21".to_string(), 100.0, 25.0, Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), ( "03/01/21".to_string(), 126.0, 10.0, Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), ]; verify_dividends_transactions(&transactions) @@ -677,18 +998,20 @@ mod tests { #[test] fn test_dividends_verification_false() -> Result<(), String> { - let transactions: Vec<(String, Currency, Currency, Option)> = vec![ + let transactions: Vec<(String, Currency, Currency, Option, Option)> = vec![ ( "06/01/21".to_string(), Currency::PLN(10.0), Currency::PLN(2.0), Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), ( "03/01/22".to_string(), Currency::PLN(126.0), Currency::PLN(10.0), Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), ]; assert_eq!( @@ -706,12 +1029,14 @@ mod tests { crate::Currency::EUR(0.05), crate::Currency::EUR(0.00), None, + None, ), ( "04/11/21".to_owned(), crate::Currency::EUR(0.07), crate::Currency::EUR(0.00), None, + None, ), ]; @@ -739,6 +1064,7 @@ mod tests { exchange_rate_date: "02/28/21".to_string(), exchange_rate: 2.0, company: None, + country: None, }, Transaction { transaction_date: "04/11/21".to_string(), @@ -747,6 +1073,7 @@ mod tests { exchange_rate_date: "04/10/21".to_string(), exchange_rate: 3.0, company: None, + country: None, }, ]) ); @@ -761,12 +1088,14 @@ mod tests { crate::Currency::PLN(0.44), crate::Currency::PLN(0.00), None, + None, ), ( "04/11/21".to_owned(), crate::Currency::PLN(0.45), crate::Currency::PLN(0.00), None, + None, ), ]; @@ -794,6 +1123,7 @@ mod tests { exchange_rate_date: "N/A".to_string(), exchange_rate: 1.0, company: None, + country: None, }, Transaction { transaction_date: "04/11/21".to_string(), @@ -802,6 +1132,7 @@ mod tests { exchange_rate_date: "N/A".to_string(), exchange_rate: 1.0, company: None, + country: None, }, ]) ); @@ -839,6 +1170,7 @@ mod tests { exchange_rate_date: "04/10/21".to_string(), exchange_rate: 3.0, company: None, + country: None, }, Transaction { transaction_date: "03/01/21".to_string(), @@ -847,6 +1179,7 @@ mod tests { exchange_rate_date: "02/28/21".to_string(), exchange_rate: 2.0, company: None, + country: None, }, ]) ); @@ -855,18 +1188,20 @@ mod tests { #[test] fn test_create_detailed_div_transactions() -> Result<(), String> { - let parsed_transactions: Vec<(String, f32, f32, Option)> = vec![ + let parsed_transactions: Vec<(String, f32, f32, Option, Option)> = vec![ ( "04/11/21".to_string(), 100.0, 25.0, Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), ( "03/01/21".to_string(), 126.0, 10.0, Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), ]; @@ -893,7 +1228,8 @@ 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()), + country: Some("US".to_string()), }, Transaction { transaction_date: "03/01/21".to_string(), @@ -901,7 +1237,8 @@ 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()), + country: Some("US".to_string()), }, ]) ); @@ -910,14 +1247,21 @@ 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, + 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()), + Some("US".to_string()), + )]; let mut dates: std::collections::HashMap> = std::collections::HashMap::new(); @@ -946,6 +1290,7 @@ mod tests { exchange_rate_acquisition_date: "11/19/23".to_string(), exchange_rate_acquisition: 2.0, company: Some("INTEL CORP".to_owned()), + country: Some("US".to_string()), },]) ); Ok(()) @@ -953,7 +1298,15 @@ mod tests { #[test] fn test_create_detailed_sold_transactions() -> Result<(), String> { - let parsed_transactions: Vec<(String, String, String, f32, f32, Option)> = vec![ + let parsed_transactions: Vec<( + String, + String, + String, + f32, + f32, + Option, + Option, + )> = vec![ ( "03/01/21".to_string(), "03/03/21".to_string(), @@ -961,6 +1314,7 @@ mod tests { 20.0, 20.0, Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), ( "06/01/21".to_string(), @@ -969,6 +1323,7 @@ mod tests { 25.0, 10.0, Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), ]; @@ -1024,6 +1379,7 @@ mod tests { exchange_rate_acquisition_date: "02/28/21".to_string(), exchange_rate_acquisition: 5.0, company: Some("INTEL CORP".to_owned()), + country: Some("US".to_string()), }, SoldTransaction { trade_date: "06/01/21".to_string(), @@ -1036,6 +1392,7 @@ mod tests { exchange_rate_acquisition_date: "12/30/18".to_string(), exchange_rate_acquisition: 6.0, company: Some("INTEL CORP".to_owned()), + country: Some("US".to_string()), }, ]) ); @@ -1044,24 +1401,26 @@ mod tests { #[test] fn test_dividends_verification_empty_ok() -> Result<(), String> { - let transactions: Vec<(String, f32, f32, Option)> = vec![]; + let transactions: Vec<(String, f32, f32, Option, Option)> = vec![]; verify_dividends_transactions(&transactions) } #[test] fn test_dividends_verification_fail() -> Result<(), String> { - let transactions: Vec<(String, f32, f32, Option)> = vec![ + let transactions: Vec<(String, f32, f32, Option, Option)> = vec![ ( "04/11/22".to_string(), 100.0, 25.0, Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), ( "03/01/21".to_string(), 126.0, 10.0, Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), ]; assert!(verify_dividends_transactions(&transactions).is_err()); @@ -1070,7 +1429,15 @@ mod tests { #[test] fn test_sold_transaction_reconstruction_dividiends_only() -> Result<(), String> { - let parsed_sold_transactions: Vec<(String, String, f32, f32, f32, Option)> = vec![]; + let parsed_sold_transactions: Vec<( + String, + String, + f32, + f32, + f32, + Option, + Option, + )> = vec![]; let parsed_gains_and_losses: Vec<(String, String, f32, f32, f32)> = vec![]; @@ -1087,7 +1454,15 @@ mod tests { #[test] fn test_sold_transaction_reconstruction_ok() -> Result<(), String> { - let parsed_sold_transactions: Vec<(String, String, f32, f32, f32, Option)> = vec![ + let parsed_sold_transactions: Vec<( + String, + String, + f32, + f32, + f32, + Option, + Option, + )> = vec![ ( "06/01/21".to_string(), "06/03/21".to_string(), @@ -1095,6 +1470,7 @@ mod tests { 25.0, 24.8, Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), ( "03/01/21".to_string(), @@ -1103,6 +1479,7 @@ mod tests { 10.0, 19.8, Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), ]; @@ -1140,7 +1517,8 @@ mod tests { "01/01/19".to_string(), 24.8, 10.0, - Some("INTEL CORP".to_owned()) + Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), ( "03/01/21".to_string(), @@ -1148,7 +1526,8 @@ mod tests { "01/01/21".to_string(), 19.8, 20.0, - Some("INTEL CORP".to_owned()) + Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), ] ); @@ -1157,7 +1536,15 @@ 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![ + let parsed_sold_transactions: Vec<( + String, + String, + f32, + f32, + f32, + Option, + Option, + )> = vec![ ( "6/1/21".to_string(), "6/3/21".to_string(), @@ -1165,6 +1552,7 @@ mod tests { 25.0, 24.8, Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), ( "3/1/21".to_string(), @@ -1173,6 +1561,7 @@ mod tests { 10.0, 19.8, Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), ]; @@ -1210,7 +1599,8 @@ mod tests { "01/01/19".to_string(), 24.8, 10.0, - Some("INTEL CORP".to_owned()) + Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), ( "03/01/21".to_string(), @@ -1218,7 +1608,8 @@ mod tests { "01/01/21".to_string(), 19.8, 20.0, - Some("INTEL CORP".to_owned()) + Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), ] ); @@ -1227,15 +1618,23 @@ 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, + 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) + Some("US".to_string()), // country of origin of company + )]; let parsed_gains_and_losses: Vec<(String, String, f32, f32, f32)> = vec![ ( @@ -1270,7 +1669,15 @@ mod tests { #[test] fn test_sold_transaction_reconstruction_multistock() -> Result<(), String> { - let parsed_sold_transactions: Vec<(String, String, f32, f32, f32, Option)> = vec![ + let parsed_sold_transactions: Vec<( + String, + String, + f32, + f32, + f32, + Option, + Option, + )> = vec![ ( "12/21/22".to_string(), "12/23/22".to_string(), @@ -1278,6 +1685,7 @@ mod tests { 26.5900, 4332.44, Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), ( "12/19/22".to_string(), @@ -1286,6 +1694,7 @@ mod tests { 26.5900, 6698.00, Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), ]; @@ -1332,7 +1741,8 @@ mod tests { "08/19/21".to_string(), 2711.0954, 4336.4874, - Some("INTEL CORP".to_owned()) + Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), ( "12/21/22".to_string(), @@ -1340,7 +1750,8 @@ mod tests { "05/03/21".to_string(), 2046.61285, 0.0, - Some("INTEL CORP".to_owned()) + Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), ( "12/19/22".to_string(), @@ -1348,7 +1759,8 @@ mod tests { "08/19/22".to_string(), 3986.9048, 5045.6257, - Some("INTEL CORP".to_owned()) + Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), ( "12/21/22".to_string(), @@ -1356,7 +1768,8 @@ mod tests { "05/02/22".to_string(), 2285.82733, 0.0, - Some("INTEL CORP".to_owned()) + Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), ] ); @@ -1365,7 +1778,15 @@ mod tests { #[test] fn test_sold_transaction_reconstruction_no_gains_fail() { - let parsed_sold_transactions: Vec<(String, String, f32, f32, f32, Option)> = vec![ + let parsed_sold_transactions: Vec<( + String, + String, + f32, + f32, + f32, + Option, + Option, + )> = vec![ ( "06/01/21".to_string(), "06/03/21".to_string(), @@ -1373,6 +1794,7 @@ mod tests { 25.0, 24.8, Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), ( "03/01/21".to_string(), @@ -1381,6 +1803,7 @@ mod tests { 10.0, 19.8, Some("INTEL CORP".to_owned()), + Some("US".to_string()), ), ];