diff options
Diffstat (limited to 'transformers')
37 files changed, 1057 insertions, 509 deletions
diff --git a/transformers/actualbudget_transformer/actualbudget_fakes/src/actualbudget_category.rs b/transformers/actualbudget_transformer/actualbudget_fakes/src/actualbudget_category.rs index 84fb32a..a02e672 100644 --- a/transformers/actualbudget_transformer/actualbudget_fakes/src/actualbudget_category.rs +++ b/transformers/actualbudget_transformer/actualbudget_fakes/src/actualbudget_category.rs @@ -8,6 +8,7 @@ pub fn make_fake_actualbudget_category_builder(id: &str) -> ActualbudgetCategory let mut builder = ActualbudgetCategoryBuilder::default(); builder .id(String::from(id)) + .group_id(None) .name(String::new()); return builder } diff --git a/transformers/actualbudget_transformer/actualbudget_fakes/src/actualbudget_category_group.rs b/transformers/actualbudget_transformer/actualbudget_fakes/src/actualbudget_category_group.rs new file mode 100644 index 0000000..8695e04 --- /dev/null +++ b/transformers/actualbudget_transformer/actualbudget_fakes/src/actualbudget_category_group.rs @@ -0,0 +1,13 @@ +use actualbudget_models::{ActualbudgetCategoryGroup, ActualbudgetCategoryGroupBuilder}; + +pub fn make_fake_actualbudget_category_group(id: &str) -> ActualbudgetCategoryGroup { + make_fake_actualbudget_category_group_builder(id).build().unwrap() +} + +pub fn make_fake_actualbudget_category_group_builder(id: &str) -> ActualbudgetCategoryGroupBuilder { + let mut builder = ActualbudgetCategoryGroupBuilder::default(); + builder + .id(String::from(id)) + .name(None); + return builder +} diff --git a/transformers/actualbudget_transformer/actualbudget_fakes/src/actualbudget_transaction.rs b/transformers/actualbudget_transformer/actualbudget_fakes/src/actualbudget_transaction.rs index 548424d..f897444 100644 --- a/transformers/actualbudget_transformer/actualbudget_fakes/src/actualbudget_transaction.rs +++ b/transformers/actualbudget_transformer/actualbudget_fakes/src/actualbudget_transaction.rs @@ -17,6 +17,7 @@ pub fn make_fake_actualbudget_transaction_builder(id: &str) -> ActualbudgetTrans .payee(None) .notes(None) .date(ActualbudgetDate::from_i32(19700101)) - .transfer_id(None); + .transfer_id(None) + .starting_balance_flag(0); return builder } diff --git a/transformers/actualbudget_transformer/actualbudget_fakes/src/lib.rs b/transformers/actualbudget_transformer/actualbudget_fakes/src/lib.rs index 815c962..dbd0849 100644 --- a/transformers/actualbudget_transformer/actualbudget_fakes/src/lib.rs +++ b/transformers/actualbudget_transformer/actualbudget_fakes/src/lib.rs @@ -1,5 +1,6 @@ pub mod actualbudget_account; pub mod actualbudget_category; +pub mod actualbudget_category_group; pub mod actualbudget_payee; pub mod actualbudget_transaction; pub mod actualbudget_zero_budget; diff --git a/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_category.rs b/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_category.rs index bf35450..84ad4b0 100644 --- a/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_category.rs +++ b/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_category.rs @@ -5,5 +5,6 @@ use diesel::{prelude::Queryable, Selectable}; #[diesel(table_name = crate::actualbudget_schema::categories)] pub struct ActualbudgetCategory { pub id: String, + pub group_id: Option<String>, pub name: String, } diff --git a/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_category_group.rs b/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_category_group.rs new file mode 100644 index 0000000..8ec21f4 --- /dev/null +++ b/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_category_group.rs @@ -0,0 +1,9 @@ +use derive_builder::Builder; +use diesel::prelude::*; + +#[derive(Builder, Clone, Queryable, Selectable)] +#[diesel(table_name = crate::actualbudget_schema::category_groups)] +pub struct ActualbudgetCategoryGroup { + pub id: String, + pub name: Option<String>, +} diff --git a/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_schema.rs b/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_schema.rs index 4e1ba24..3a26344 100644 --- a/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_schema.rs +++ b/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_schema.rs @@ -8,11 +8,21 @@ diesel::table! { diesel::table! { categories (id) { id -> Text, + #[sql_name = "cat_group"] + group_id -> Nullable<Text>, name -> Text, } } diesel::table! { + category_groups (id) { + id -> Text, + name -> Nullable<Text>, + } +} + + +diesel::table! { v_transactions (id) { id -> Text, is_parent -> Bool, @@ -27,10 +37,10 @@ diesel::table! { notes -> Nullable<Text>, date -> Integer, transfer_id -> Nullable<Text>, + starting_balance_flag -> Integer, // imported_id -> Text, // error -> Text, // imported_payee -> Text, - // starting_balance_flag -> Integer, // sort_order -> Real, // cleared -> Integer, // reconciled -> Integer, diff --git a/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_transaction.rs b/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_transaction.rs index c8ac522..d6270da 100644 --- a/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_transaction.rs +++ b/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_transaction.rs @@ -3,7 +3,9 @@ use diesel::prelude::{Identifiable, Queryable, QueryableByName, Selectable}; use crate::actualbudget_date::ActualbudgetDate; -#[derive(Builder, Debug, PartialEq, Eq, Identifiable, QueryableByName, Queryable, Selectable, Clone)] +#[derive( + Builder, Debug, PartialEq, Eq, Identifiable, QueryableByName, Queryable, Selectable, Clone, +)] #[diesel(table_name = crate::actualbudget_schema::v_transactions)] pub struct ActualbudgetTransaction { pub id: String, @@ -17,4 +19,11 @@ pub struct ActualbudgetTransaction { pub notes: Option<String>, pub date: ActualbudgetDate, pub transfer_id: Option<String>, + pub starting_balance_flag: i32, +} + +impl ActualbudgetTransaction { + pub fn is_starting_balance(&self) -> bool { + self.starting_balance_flag != 0 + } } diff --git a/transformers/actualbudget_transformer/actualbudget_models/src/lib.rs b/transformers/actualbudget_transformer/actualbudget_models/src/lib.rs index 2d7d46c..b46b287 100644 --- a/transformers/actualbudget_transformer/actualbudget_models/src/lib.rs +++ b/transformers/actualbudget_transformer/actualbudget_models/src/lib.rs @@ -1,5 +1,6 @@ mod actualbudget_account; mod actualbudget_category; +mod actualbudget_category_group; mod actualbudget_date; mod actualbudget_payee; mod actualbudget_transaction; @@ -11,6 +12,8 @@ pub use actualbudget_account::ActualbudgetAccount; pub use actualbudget_account::ActualbudgetAccountBuilder; pub use actualbudget_category::ActualbudgetCategory; pub use actualbudget_category::ActualbudgetCategoryBuilder; +pub use actualbudget_category_group::ActualbudgetCategoryGroup; +pub use actualbudget_category_group::ActualbudgetCategoryGroupBuilder; pub use actualbudget_date::ActualbudgetDate; pub use actualbudget_payee::ActualbudgetPayee; pub use actualbudget_payee::ActualbudgetPayeeBuilder; diff --git a/transformers/actualbudget_transformer/actualbudget_queries/src/actualbudget_category_groups.rs b/transformers/actualbudget_transformer/actualbudget_queries/src/actualbudget_category_groups.rs new file mode 100644 index 0000000..8d90765 --- /dev/null +++ b/transformers/actualbudget_transformer/actualbudget_queries/src/actualbudget_category_groups.rs @@ -0,0 +1,11 @@ +use actualbudget_models::ActualbudgetCategoryGroup; +use actualbudget_schema::actualbudget_schema::category_groups::dsl::category_groups; +use diesel::{result::Error, QueryDsl, RunQueryDsl, SelectableHelper, SqliteConnection}; + +pub fn get_all_actualbudget_category_groups( + connection: &mut SqliteConnection +) -> Result<Vec<ActualbudgetCategoryGroup>, Error> { + category_groups + .select(ActualbudgetCategoryGroup::as_select()) + .load(connection) +} diff --git a/transformers/actualbudget_transformer/actualbudget_queries/src/lib.rs b/transformers/actualbudget_transformer/actualbudget_queries/src/lib.rs index ca3f6ea..0fffcbb 100644 --- a/transformers/actualbudget_transformer/actualbudget_queries/src/lib.rs +++ b/transformers/actualbudget_transformer/actualbudget_queries/src/lib.rs @@ -1,5 +1,6 @@ pub mod actualbudget_accounts; pub mod actualbudget_categories; +pub mod actualbudget_category_groups; pub mod actualbudget_payees; pub mod actualbudget_transactions; pub mod actualbudget_zero_budgets; diff --git a/transformers/actualbudget_transformer/actualbudget_queries/tests/resources/actualbudget_test_db.sqlite b/transformers/actualbudget_transformer/actualbudget_queries/tests/resources/actualbudget_test_db.sqlite Binary files differindex 4146bb0..5f89924 100644 --- a/transformers/actualbudget_transformer/actualbudget_queries/tests/resources/actualbudget_test_db.sqlite +++ b/transformers/actualbudget_transformer/actualbudget_queries/tests/resources/actualbudget_test_db.sqlite diff --git a/transformers/actualbudget_transformer/actualbudget_queries/tests/resources/actualbudget_test_db_metadata.json b/transformers/actualbudget_transformer/actualbudget_queries/tests/resources/actualbudget_test_db_metadata.json index 006b23d..c88313a 100644 --- a/transformers/actualbudget_transformer/actualbudget_queries/tests/resources/actualbudget_test_db_metadata.json +++ b/transformers/actualbudget_transformer/actualbudget_queries/tests/resources/actualbudget_test_db_metadata.json @@ -1,7 +1,7 @@ { - "id": "My-Finances-1-36ec11a", - "budgetName": "My Finances 1", - "userId": "fec9ba17-c260-4f55-8d44-676ef0ec5da9", - "lastScheduleRun": "2024-11-21", - "resetClock": true + "id": "My-Finances-1-36ec11a", + "budgetName": "My Finances 1", + "userId": "fec9ba17-c260-4f55-8d44-676ef0ec5da9", + "lastScheduleRun": "2025-08-16", + "resetClock": true } diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/Cargo.toml b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/Cargo.toml index 51febe1..202d041 100644 --- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/Cargo.toml +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/Cargo.toml @@ -12,6 +12,7 @@ diesel = { workspace = true, features = ["sqlite"] } diesel_migrations = { workspace = true } itertools = { workspace = true } libsqlite3-sys = { workspace = true, features = ["bundled"] } +schist_fakes = { workspace = true } schist_models = { workspace = true } schist_queries = { workspace = true } schist_traits = { workspace = true } diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/actualbudget_state.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/actualbudget_state.rs index 5379b62..cda63b1 100644 --- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/actualbudget_state.rs +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/actualbudget_state.rs @@ -1,7 +1,13 @@ use std::cmp::min; -use actualbudget_models::{ActualbudgetAccount, ActualbudgetCategory, ActualbudgetDate, ActualbudgetPayee, ActualbudgetTransaction, ActualbudgetZeroBudget}; -use actualbudget_queries::{actualbudget_accounts::{get_all_actualbudget_accounts, get_category_transfers_account}, actualbudget_categories::get_all_actualbudget_categories, actualbudget_payees::get_all_actualbudget_payees, actualbudget_transactions::{get_all_actualbudget_transactions, get_first_actualbudget_transaction_date}, actualbudget_zero_budgets::{get_all_actualbudget_zero_budgets, get_first_zero_budget_date}}; +use actualbudget_models::{ + ActualbudgetAccount, ActualbudgetCategory, ActualbudgetCategoryGroup, ActualbudgetDate, ActualbudgetPayee, ActualbudgetTransaction, ActualbudgetZeroBudget +}; +use actualbudget_queries::{ + actualbudget_accounts::get_all_actualbudget_accounts, actualbudget_categories::get_all_actualbudget_categories, actualbudget_category_groups::get_all_actualbudget_category_groups, actualbudget_payees::get_all_actualbudget_payees, actualbudget_transactions::{ + get_all_actualbudget_transactions, get_first_actualbudget_transaction_date, + }, actualbudget_zero_budgets::{get_all_actualbudget_zero_budgets, get_first_zero_budget_date} +}; use anyhow::Result; use diesel::SqliteConnection; @@ -9,23 +15,25 @@ pub struct ActualbudgetState { pub actualbudget_first_activity_date: ActualbudgetDate, pub actualbudget_accounts: Vec<ActualbudgetAccount>, pub actualbudget_categories: Vec<ActualbudgetCategory>, + pub actualbudget_category_groups: Vec<ActualbudgetCategoryGroup>, pub actualbudget_payees: Vec<ActualbudgetPayee>, pub actualbudget_zero_budgets: Vec<ActualbudgetZeroBudget>, - pub actualbudget_category_transfers_account: Option<ActualbudgetAccount>, pub actualbudget_transactions: Vec<ActualbudgetTransaction>, } -pub fn read_actualbudget_state(actualbudget_connection: &mut SqliteConnection) -> Result<ActualbudgetState> { +pub fn read_actualbudget_state( + actualbudget_connection: &mut SqliteConnection, +) -> Result<ActualbudgetState> { Ok(ActualbudgetState { actualbudget_first_activity_date: min( - get_first_actualbudget_transaction_date(actualbudget_connection)?, - get_first_zero_budget_date(actualbudget_connection)?, - ), + get_first_actualbudget_transaction_date(actualbudget_connection)?, + get_first_zero_budget_date(actualbudget_connection)?, + ), actualbudget_accounts: get_all_actualbudget_accounts(actualbudget_connection)?, actualbudget_categories: get_all_actualbudget_categories(actualbudget_connection)?, + actualbudget_category_groups: get_all_actualbudget_category_groups(actualbudget_connection)?, actualbudget_payees: get_all_actualbudget_payees(actualbudget_connection)?, actualbudget_zero_budgets: get_all_actualbudget_zero_budgets(actualbudget_connection)?, - actualbudget_category_transfers_account: get_category_transfers_account(actualbudget_connection)?, actualbudget_transactions: get_all_actualbudget_transactions(actualbudget_connection)?, }) } diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/mod.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state.rs index 5fadc2c..a116649 100644 --- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/mod.rs +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state.rs @@ -1,27 +1,27 @@ mod is_account_transfer; -mod is_category_transfer; +mod is_bucket_transfer; mod transform_account_transfers; mod transform_accounts; -mod transform_budget_drips; -mod transform_categories; -mod transform_category_transfers; +mod transform_categories_to_buckets; mod transform_date; mod transform_id; mod transform_state; mod transform_transactions; -mod transform_zero_budgets; +mod transform_transactions_to_bucket_transfers; +mod transform_zero_budgets_to_drips; +mod transform_zero_budgets_to_pipes; use is_account_transfer::is_account_transfer; use is_account_transfer::is_not_account_transfer; -use is_category_transfer::is_category_transfer; -use is_category_transfer::is_not_category_transfer; +use is_bucket_transfer::is_bucket_transfer; +use is_bucket_transfer::is_not_bucket_transfer; use transform_account_transfers::transform_account_transfers; use transform_accounts::transform_accounts; -use transform_categories::transform_categories; -use transform_category_transfers::transform_category_transfers; +use transform_categories_to_buckets::transform_categories_to_buckets; use transform_date::transform_date; use transform_id::transform_id; use transform_transactions::transform_transactions; -use transform_zero_budgets::transform_zero_budgets; +use transform_transactions_to_bucket_transfers::transform_transactions_to_bucket_transfers; +use transform_zero_budgets_to_pipes::transform_zero_budgets_to_pipes; pub use transform_state::transform_state; diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_bucket_transfer.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_bucket_transfer.rs new file mode 100644 index 0000000..188697e --- /dev/null +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_bucket_transfer.rs @@ -0,0 +1,46 @@ +use actualbudget_models::ActualbudgetTransaction; + +pub fn is_bucket_transfer( + transaction: &ActualbudgetTransaction, + transactions: &[ActualbudgetTransaction], +) -> bool { + is_parent_bucket_transfer(transaction, transactions) + || is_child_bucket_transfer(transaction, transactions) +} + +fn is_parent_bucket_transfer( + transaction: &ActualbudgetTransaction, + transactions: &[ActualbudgetTransaction], +) -> bool { + if !transaction.is_parent { + return false; + } + + let sum_of_children_amounts: i32 = transactions + .iter() + .filter(|t| is_parent_and_child(transaction, t)) + .map(|t| t.amount) + .sum(); + sum_of_children_amounts == 0 +} + +fn is_child_bucket_transfer( + transaction: &ActualbudgetTransaction, + transactions: &[ActualbudgetTransaction], +) -> bool { + transaction.is_child + && transactions + .iter() + .any(|t| is_parent_and_child(t, transaction)) +} + +fn is_parent_and_child(parent: &ActualbudgetTransaction, child: &ActualbudgetTransaction) -> bool { + parent.is_parent && child.is_child && child.parent_id.clone().is_some_and(|id| id == parent.id) +} + +pub fn is_not_bucket_transfer( + transaction: &ActualbudgetTransaction, + transactions: &[ActualbudgetTransaction], +) -> bool { + !is_bucket_transfer(transaction, transactions) +} diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_category_transfer.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_category_transfer.rs deleted file mode 100644 index c82387c..0000000 --- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_category_transfer.rs +++ /dev/null @@ -1,18 +0,0 @@ -use actualbudget_models::{ActualbudgetAccount, ActualbudgetTransaction}; - -pub fn is_category_transfer( - transaction: &ActualbudgetTransaction, - actualbudget_category_transfers_account: &Option<ActualbudgetAccount> -) -> bool { - match actualbudget_category_transfers_account { - None => false, - Some(acc) => transaction.account_id.clone() == acc.id.clone(), - } -} - -pub fn is_not_category_transfer( - transaction: &ActualbudgetTransaction, - actualbudget_category_transfers_account: &Option<ActualbudgetAccount> -) -> bool { - !is_category_transfer(transaction, actualbudget_category_transfers_account) -} diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_account_transfers.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_account_transfers.rs index c41b812..a116086 100644 --- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_account_transfers.rs +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_account_transfers.rs @@ -112,7 +112,7 @@ fn transform_account_transfer( mod test { use actualbudget_fakes::actualbudget_transaction::make_fake_actualbudget_transaction_builder; use actualbudget_models::ActualbudgetDate; - use schist_models::DateUtc; + use schist_models::{AccountTransfer, DateUtc}; use crate::transform_state::transform_id::transform_id; @@ -147,14 +147,14 @@ mod test { assert_eq!(0, remaining_transactions.len()); assert_eq!(1, account_transfers.len()); let account_transfer = account_transfers.get(0).unwrap(); - assert_eq!( - "2024-08-01".parse::<DateUtc>().unwrap(), - account_transfer.date - ); - assert_eq!("My notes", account_transfer.description); - assert_eq!(100, account_transfer.amount); - assert_eq!(transform_id("0"), account_transfer.from_account_id); - assert_eq!(transform_id("1"), account_transfer.to_account_id); + assert_eq!(AccountTransfer { + id: account_transfer.id, + amount: 100, + date: "2024-08-01".parse().unwrap(), + description: String::from("My notes"), + from_account_id: transform_id("0"), + to_account_id: transform_id("1"), + }, *account_transfer); } #[test] diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_accounts.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_accounts.rs index c900da7..76eaf0c 100644 --- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_accounts.rs +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_accounts.rs @@ -1,32 +1,194 @@ -use actualbudget_models::ActualbudgetAccount; +use actualbudget_models::{ActualbudgetAccount, ActualbudgetTransaction}; use schist_models::{Account, DateUtc}; -use super::transform_id; +use super::{is_bucket_transfer, transform_id}; pub fn transform_accounts( actualbudget_accounts: &[ActualbudgetAccount], - actualbudget_category_transfers_account: &Option<ActualbudgetAccount>, + actualbudget_transactions: &[ActualbudgetTransaction], first_activity_date: &DateUtc, ) -> Vec<Account> { actualbudget_accounts .iter() - .filter(|a| { - actualbudget_category_transfers_account - .clone() - .is_some_and(|cta| cta.id != a.id) - }) - .map(|a| transform_account(a, first_activity_date)) + .filter(|a| !is_empty_account(a, actualbudget_transactions)) + .map(|a| transform_account(a, actualbudget_transactions, first_activity_date)) .collect() } +fn is_empty_account( + actualbudget_account: &ActualbudgetAccount, + actualbudget_transactions: &[ActualbudgetTransaction], +) -> bool { + actualbudget_transactions.iter().all(|t| { + t.is_starting_balance() && t.amount == 0 + || t.account_id != actualbudget_account.id + || is_bucket_transfer(t, actualbudget_transactions) + }) +} + fn transform_account( actualbudget_account: &ActualbudgetAccount, + actualbudget_transactions: &[ActualbudgetTransaction], opening_date: &DateUtc, ) -> Account { + let opening_balance = actualbudget_transactions + .iter() + .filter(|t| t.is_starting_balance()) + .filter(|t| t.account_id == actualbudget_account.id) + .map(|t| t.amount) + .sum(); + Account { id: transform_id(&actualbudget_account.id.clone()), name: actualbudget_account.name.clone(), - opening_balance: 0, + opening_balance, opening_date: opening_date.clone(), } } + +#[cfg(test)] +mod test { + use actualbudget_fakes::{ + actualbudget_account::make_fake_actualbudget_account_builder, + actualbudget_transaction::make_fake_actualbudget_transaction_builder, + }; + use schist_models::{Account, DateUtc}; + + use super::{transform_accounts, transform_id}; + + #[test] + fn given_starting_balances_then_sets_opening_balances() { + let actualbudget_accounts = vec![ + make_fake_actualbudget_account_builder("0") + .name(String::from("Cash account")) + .build() + .unwrap(), + make_fake_actualbudget_account_builder("1") + .name(String::from("Current account")) + .build() + .unwrap(), + ]; + + let actualbudget_transactions = vec![ + make_fake_actualbudget_transaction_builder("0") + .account_id(String::from("0")) + .amount(100) + .starting_balance_flag(1) + .build() + .unwrap(), + make_fake_actualbudget_transaction_builder("1") + .account_id(String::from("1")) + .amount(200) + .starting_balance_flag(1) + .build() + .unwrap(), + make_fake_actualbudget_transaction_builder("2") + .account_id(String::from("1")) + .amount(50) + .starting_balance_flag(1) + .build() + .unwrap(), + make_fake_actualbudget_transaction_builder("3") + .account_id(String::from("0")) + .amount(300) + .starting_balance_flag(0) + .build() + .unwrap(), + make_fake_actualbudget_transaction_builder("4") + .account_id(String::from("1")) + .amount(600) + .starting_balance_flag(0) + .build() + .unwrap(), + ]; + let first_activity_date: DateUtc = "2024-08-01".parse().unwrap(); + + let result = transform_accounts( + &actualbudget_accounts, + &actualbudget_transactions, + &first_activity_date, + ); + + assert_eq!(2, result.len()); + assert_eq!( + Some(Account { + id: transform_id("0"), + name: String::from("Cash account"), + opening_balance: 100, + opening_date: "2024-08-01".parse().unwrap(), + }), + result.iter().find(|a| a.name.eq("Cash account")).cloned() + ); + assert_eq!( + Some(Account { + id: transform_id("1"), + name: String::from("Current account"), + opening_balance: 250, + opening_date: "2024-08-01".parse().unwrap(), + }), + result + .iter() + .find(|a| a.name.eq("Current account")) + .cloned() + ); + } + + #[test] + fn filters_out_empty_account() { + let actualbudget_accounts = vec![ + make_fake_actualbudget_account_builder("0") + .name(String::from("Cash account")) + .build() + .unwrap(), + make_fake_actualbudget_account_builder("1") + .name(String::from("Current account")) + .build() + .unwrap(), + ]; + + let actualbudget_transactions = vec![ + make_fake_actualbudget_transaction_builder("0") + .account_id(String::from("0")) + .amount(0) + .starting_balance_flag(1) + .build() + .unwrap(), + make_fake_actualbudget_transaction_builder("1") + .account_id(String::from("1")) + .amount(200) + .starting_balance_flag(1) + .build() + .unwrap(), + make_fake_actualbudget_transaction_builder("2") + .account_id(String::from("1")) + .amount(50) + .starting_balance_flag(1) + .build() + .unwrap(), + make_fake_actualbudget_transaction_builder("3") + .account_id(String::from("1")) + .amount(300) + .starting_balance_flag(0) + .build() + .unwrap(), + ]; + let first_activity_date: DateUtc = "2024-08-01".parse().unwrap(); + + let result = transform_accounts( + &actualbudget_accounts, + &actualbudget_transactions, + &first_activity_date, + ); + + assert_eq!(1, result.len()); + assert_eq!( + Some(Account { + id: transform_id("1"), + name: String::from("Current account"), + opening_balance: 250, + opening_date: "2024-08-01".parse().unwrap(), + }), + result.first().cloned(), + ); + } +} diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_categories.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_categories.rs deleted file mode 100644 index 5bea17c..0000000 --- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_categories.rs +++ /dev/null @@ -1,143 +0,0 @@ -use actualbudget_models::{ActualbudgetCategory, ActualbudgetZeroBudget}; -use schist_models::Bucket; - -use super::transform_id; - -pub fn transform_categories(actualbudget_categories: &[ActualbudgetCategory]) -> Vec<Bucket> { - actualbudget_categories - .iter() - .map(|actualbudget_category| Bucket { - id: transform_id(&actualbudget_category.id.clone()), - balance: None, - balance_cache_key: None, - group: String::from("un-named group"), - name: actualbudget_category.name.clone(), - }) - .collect() -} - -fn get_budget_quantity( - actualbudget_category: &ActualbudgetCategory, - actualbudget_zero_budgets: &[ActualbudgetZeroBudget], -) -> i32 { - let latest_zero_budget_for_category = actualbudget_zero_budgets - .iter() - .filter(|zb| zb.category_id == actualbudget_category.id) - .max_by_key(|zb| zb.month.clone()); - - match latest_zero_budget_for_category { - Some(latest_zero_budget_for_category) => latest_zero_budget_for_category.amount, - None => 0, - } -} - -#[cfg(test)] -mod test { - use actualbudget_fakes::actualbudget_category::make_fake_actualbudget_category_builder; - - use crate::transform_state::transform_id::transform_id; - - use super::transform_categories; - - #[test] - fn when_one_category_one_zero_budget_then_transforms_category() { - let actualbudget_categories = [make_fake_actualbudget_category_builder("0") - .name(String::from("Food")) - .build() - .unwrap()]; - - let buckets = transform_categories(&actualbudget_categories); - - assert_eq!(1, buckets.len()); - let bucket = buckets.get(0).unwrap(); - assert_eq!(None, bucket.balance); - assert_eq!(None, bucket.balance_cache_key); - assert_eq!("Food", bucket.name); - } - - #[test] - fn when_one_category_no_zero_budget_then_transforms_category() { - let actualbudget_categories = [make_fake_actualbudget_category_builder("0") - .name(String::from("Food")) - .build() - .unwrap()]; - - let buckets = transform_categories(&actualbudget_categories); - - assert_eq!(1, buckets.len()); - let bucket = buckets.get(0).unwrap(); - assert_eq!("Food", bucket.name); - assert_eq!(None, bucket.balance); - assert_eq!(None, bucket.balance_cache_key); - assert_eq!("un-named group", bucket.group); - } - - #[test] - fn when_one_category_multiple_zero_budgets_then_transforms_category() { - let actualbudget_categories = [make_fake_actualbudget_category_builder("0") - .name(String::from("Food")) - .build() - .unwrap()]; - - let buckets = transform_categories(&actualbudget_categories); - - assert_eq!(1, buckets.len()); - let bucket = buckets.get(0).unwrap(); - assert_eq!("Food", bucket.name); - assert_eq!(None, bucket.balance); - assert_eq!(None, bucket.balance_cache_key); - assert_eq!("un-named group", bucket.group); - } - - #[test] - fn when_multiple_categories_multiple_zero_budgets_then_transforms_category() { - let actualbudget_categories = [ - make_fake_actualbudget_category_builder("0") - .name(String::from("Food")) - .build() - .unwrap(), - make_fake_actualbudget_category_builder("1") - .name(String::from("Clothes")) - .build() - .unwrap(), - make_fake_actualbudget_category_builder("2") - .name(String::from("Rent")) - .build() - .unwrap(), - ]; - - let buckets = transform_categories(&actualbudget_categories); - - assert_eq!(3, buckets.len()); - - let bucket_0 = buckets - .iter() - .filter(|c| c.id == transform_id("0")) - .last() - .unwrap(); - assert_eq!("Food", bucket_0.name); - assert_eq!(None, bucket_0.balance); - assert_eq!(None, bucket_0.balance_cache_key); - assert_eq!("un-named group", bucket_0.group); - - let bucket_1 = buckets - .iter() - .filter(|c| c.id == transform_id("1")) - .last() - .unwrap(); - assert_eq!("Clothes", bucket_1.name); - assert_eq!(None, bucket_1.balance); - assert_eq!(None, bucket_1.balance_cache_key); - assert_eq!("un-named group", bucket_1.group); - - let bucket_2 = buckets - .iter() - .filter(|c| c.id == transform_id("2")) - .last() - .unwrap(); - assert_eq!("Rent", bucket_2.name); - assert_eq!(None, bucket_2.balance); - assert_eq!(None, bucket_2.balance_cache_key); - assert_eq!("un-named group", bucket_2.group); - } -} diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_categories_to_buckets.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_categories_to_buckets.rs new file mode 100644 index 0000000..5653c26 --- /dev/null +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_categories_to_buckets.rs @@ -0,0 +1,263 @@ +use actualbudget_models::{ActualbudgetCategory, ActualbudgetCategoryGroup}; +use schist_models::{Bucket, Drip, Transaction}; + +use super::transform_id; + +pub fn transform_categories_to_buckets( + actualbudget_categories: &[ActualbudgetCategory], + actualbudget_category_groups: &[ActualbudgetCategoryGroup], + drips: &[Drip], + transactions: &[Transaction], +) -> Vec<Bucket> { + actualbudget_categories + .iter() + .map(|actualbudget_category| Bucket { + id: transform_id(&actualbudget_category.id.clone()), + balance: None, + balance_cache_key: None, + group: actualbudget_category.group_id.clone() + .map_or_else( + default_group, + |group_id| actualbudget_category_groups + .iter() + .find(|&group| group.id == group_id) + .map_or_else(default_group, |group| group.name.clone().unwrap_or_else(default_group)), + ), + name: actualbudget_category.name.clone(), + }) + .filter(|bucket| !bucket.is_empty(drips, transactions)) + .collect() +} + +fn default_group() -> String { + String::from("un-named group") +} + +#[cfg(test)] +mod test { + use actualbudget_fakes::actualbudget_category::make_fake_actualbudget_category_builder; + use actualbudget_models::ActualbudgetCategoryGroup; + use schist_fakes::{drip::make_fake_drip_builder, transaction::make_fake_transaction_builder}; + use schist_models::Bucket; + + use crate::transform_state::transform_id::transform_id; + + use super::transform_categories_to_buckets; + + #[test] + fn when_one_category_one_drip_then_transforms_category() { + let actualbudget_categories = [make_fake_actualbudget_category_builder("0") + .name(String::from("Food")) + .build() + .unwrap()]; + let actualbudget_category_groups = vec![]; + let drips = [make_fake_drip_builder(0) + .bucket_id(transform_id("0")) + .build() + .unwrap()]; + let transactions = [make_fake_transaction_builder(0) + .bucket_id(Some(transform_id("1"))) + .build() + .unwrap()]; + + let buckets = transform_categories_to_buckets( + &actualbudget_categories, + &actualbudget_category_groups, + &drips, + &transactions); + + assert_eq!(1, buckets.len()); + let bucket = buckets.get(0).unwrap(); + assert_eq!(None, bucket.balance); + assert_eq!(None, bucket.balance_cache_key); + assert_eq!("Food", bucket.name); + } + + #[test] + fn when_one_category_one_transaction_then_transforms_category() { + let actualbudget_categories = [make_fake_actualbudget_category_builder("0") + .name(String::from("Food")) + .build() + .unwrap()]; + let actualbudget_category_groups = vec![]; + let drips = [make_fake_drip_builder(0) + .bucket_id(transform_id("2")) + .build() + .unwrap()]; + let transactions = [make_fake_transaction_builder(0) + .bucket_id(Some(transform_id("0"))) + .build() + .unwrap()]; + + let buckets = transform_categories_to_buckets( + &actualbudget_categories, + &actualbudget_category_groups, + &drips, + &transactions); + + assert_eq!(1, buckets.len()); + let bucket = buckets.get(0).unwrap(); + assert_eq!("Food", bucket.name); + assert_eq!(None, bucket.balance); + assert_eq!(None, bucket.balance_cache_key); + assert_eq!("un-named group", bucket.group); + } + + #[test] + fn when_one_category_no_drip_no_transaction_then_ignores_category() { + let actualbudget_categories = [make_fake_actualbudget_category_builder("0") + .name(String::from("Food")) + .build() + .unwrap()]; + let actualbudget_category_groups = vec![]; + let drips = [make_fake_drip_builder(0) + .bucket_id(transform_id("2")) + .build() + .unwrap()]; + let transactions = [make_fake_transaction_builder(0) + .bucket_id(Some(transform_id("1"))) + .build() + .unwrap()]; + + let buckets = transform_categories_to_buckets( + &actualbudget_categories, + &actualbudget_category_groups, + &drips, + &transactions); + + assert_eq!(0, buckets.len()); + } + + #[test] + fn when_multiple_categories_then_transforms_categories() { + let actualbudget_categories = [ + make_fake_actualbudget_category_builder("0") + .name(String::from("Food")) + .build() + .unwrap(), + make_fake_actualbudget_category_builder("1") + .name(String::from("Clothes")) + .build() + .unwrap(), + make_fake_actualbudget_category_builder("2") + .name(String::from("Rent")) + .build() + .unwrap(), + make_fake_actualbudget_category_builder("3") + .name(String::from("Fragrance")) + .build() + .unwrap(), + ]; + let actualbudget_category_groups = vec![]; + let drips = [ + make_fake_drip_builder(0) + .bucket_id(transform_id("0")) + .build() + .unwrap(), + make_fake_drip_builder(0) + .bucket_id(transform_id("1")) + .build() + .unwrap(), + make_fake_drip_builder(0) + .bucket_id(transform_id("1")) + .build() + .unwrap(), + ]; + let transactions = [ + make_fake_transaction_builder(0) + .bucket_id(Some(transform_id("0"))) + .build() + .unwrap(), + make_fake_transaction_builder(0) + .bucket_id(Some(transform_id("0"))) + .build() + .unwrap(), + make_fake_transaction_builder(0) + .bucket_id(Some(transform_id("2"))) + .build() + .unwrap(), + ]; + + let buckets = transform_categories_to_buckets( + &actualbudget_categories, + &actualbudget_category_groups, + &drips, + &transactions); + + assert_eq!(3, buckets.len()); + + let bucket_0 = buckets + .iter() + .filter(|c| c.id == transform_id("0")) + .last() + .unwrap(); + assert_eq!("Food", bucket_0.name); + assert_eq!(None, bucket_0.balance); + assert_eq!(None, bucket_0.balance_cache_key); + assert_eq!("un-named group", bucket_0.group); + + let bucket_1 = buckets + .iter() + .filter(|c| c.id == transform_id("1")) + .last() + .unwrap(); + assert_eq!("Clothes", bucket_1.name); + assert_eq!(None, bucket_1.balance); + assert_eq!(None, bucket_1.balance_cache_key); + assert_eq!("un-named group", bucket_1.group); + + let bucket_2 = buckets + .iter() + .filter(|c| c.id == transform_id("2")) + .last() + .unwrap(); + assert_eq!("Rent", bucket_2.name); + assert_eq!(None, bucket_2.balance); + assert_eq!(None, bucket_2.balance_cache_key); + assert_eq!("un-named group", bucket_2.group); + } + + #[test] + fn when_groups_include_match_then_includes_group_name() { + let actualbudget_categories = [make_fake_actualbudget_category_builder("0") + .name(String::from("Food")) + .group_id(Some(String::from("group-1"))) + .build() + .unwrap()]; + let actualbudget_category_groups = vec![ + ActualbudgetCategoryGroup { + id: String::from("group-0"), + name: Some(String::from("Income")), + }, + ActualbudgetCategoryGroup { + id: String::from("group-1"), + name: Some(String::from("Essentials")), + } + ]; + let drips = [make_fake_drip_builder(0) + .bucket_id(transform_id("0")) + .build() + .unwrap()]; + let transactions = [make_fake_transaction_builder(0) + .bucket_id(Some(transform_id("1"))) + .build() + .unwrap()]; + + let buckets = transform_categories_to_buckets( + &actualbudget_categories, + &actualbudget_category_groups, + &drips, + &transactions); + + assert_eq!(1, buckets.len()); + let bucket = buckets.get(0).unwrap(); + assert_eq!(Bucket { + id: transform_id("0"), + balance: None, + balance_cache_key: None, + group: String::from("Essentials"), + name: String::from("Food"), + }, *bucket); + + } +} diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs index 7816df6..835ceff 100644 --- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs @@ -1,12 +1,12 @@ -use anyhow::{bail, Context, Result}; -use schist_models::{Account, Bucket, BucketTransfer, DateUtc, Pipe, Transaction}; +use anyhow::{Context, Result}; +use schist_models::{Account, Bucket, BucketTransfer, Pipe, Transaction}; use crate::{actualbudget_state::ActualbudgetState, schist_state::SchistState}; use super::{ - transform_account_transfers, transform_accounts, - transform_budget_drips::transform_budget_drips, transform_categories, - transform_category_transfers, transform_date, transform_transactions, transform_zero_budgets, + transform_account_transfers, transform_accounts, transform_categories_to_buckets, + transform_date, transform_transactions, transform_transactions_to_bucket_transfers, + transform_zero_budgets_to_drips::transform_budget_drips, transform_zero_budgets_to_pipes, }; pub fn transform_state(actualbudget_state: ActualbudgetState) -> Result<SchistState> { @@ -17,25 +17,27 @@ pub fn transform_state(actualbudget_state: ActualbudgetState) -> Result<SchistSt fn transform_state_without_context(actualbudget_state: ActualbudgetState) -> Result<SchistState> { let (remaining_transactions, account_transfers) = transform_account_transfers(&actualbudget_state.actualbudget_transactions)?; + let transactions: Vec<Transaction> = transform_transactions( + &remaining_transactions, + &actualbudget_state.actualbudget_payees, + )?; let first_activity_date = transform_date(&actualbudget_state.actualbudget_first_activity_date)?; - let before_first_activity_date = find_date_before(&first_activity_date)?; let accounts: Vec<Account> = transform_accounts( &actualbudget_state.actualbudget_accounts, - &actualbudget_state.actualbudget_category_transfers_account, + &actualbudget_state.actualbudget_transactions, &first_activity_date, ); let budget_drips = transform_budget_drips(&actualbudget_state.actualbudget_zero_budgets)?; - let buckets: Vec<Bucket> = transform_categories(&actualbudget_state.actualbudget_categories); - let pipes: Vec<Pipe> = transform_zero_budgets(&actualbudget_state.actualbudget_zero_budgets); - let transactions: Vec<Transaction> = transform_transactions( - &remaining_transactions, - &actualbudget_state.actualbudget_category_transfers_account, - &actualbudget_state.actualbudget_payees, - )?; - let bucket_transfers: Vec<BucketTransfer> = transform_category_transfers( - &remaining_transactions, - &actualbudget_state.actualbudget_category_transfers_account, - )?; + let buckets: Vec<Bucket> = transform_categories_to_buckets( + &actualbudget_state.actualbudget_categories, + &actualbudget_state.actualbudget_category_groups, + &budget_drips, + &transactions, + ); + let pipes: Vec<Pipe> = + transform_zero_budgets_to_pipes(&actualbudget_state.actualbudget_zero_budgets)?; + let bucket_transfers: Vec<BucketTransfer> = + transform_transactions_to_bucket_transfers(&remaining_transactions)?; Ok(SchistState { first_transaction_date: first_activity_date, @@ -48,14 +50,3 @@ fn transform_state_without_context(actualbudget_state: ActualbudgetState) -> Res bucket_transfers, }) } - -fn find_date_before(date: &DateUtc) -> Result<DateUtc> { - for days_before in 1..31 { - let result = date.sub_days(days_before); - if result.is_ok() { - return result; - } - } - - bail!("failed to find a date before {}", date) -} diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_transactions.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_transactions.rs index 7c4ece5..11765be 100644 --- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_transactions.rs +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_transactions.rs @@ -1,32 +1,27 @@ -use actualbudget_models::{ActualbudgetAccount, ActualbudgetPayee, ActualbudgetTransaction}; +use actualbudget_models::{ActualbudgetPayee, ActualbudgetTransaction}; use anyhow::{Context, Result}; use schist_models::Transaction; -use super::{is_not_category_transfer, transform_date, transform_id}; +use super::{is_not_bucket_transfer, transform_date, transform_id}; pub fn transform_transactions( actualbudget_transactions: &[ActualbudgetTransaction], - actualbudget_category_transfers_account: &Option<ActualbudgetAccount>, actualbudget_payees: &[ActualbudgetPayee], ) -> Result<Vec<Transaction>> { - transform_transactions_without_context( - actualbudget_transactions, - actualbudget_category_transfers_account, - actualbudget_payees, - ) - .with_context(|| "failed to transform actualbudget transactions to schist transactions") + transform_transactions_without_context(actualbudget_transactions, actualbudget_payees) + .with_context(|| "failed to transform actualbudget transactions to schist transactions") } fn transform_transactions_without_context( actualbudget_transactions: &[ActualbudgetTransaction], - actualbudget_category_transfers_account: &Option<ActualbudgetAccount>, actualbudget_payees: &[ActualbudgetPayee], ) -> Result<Vec<Transaction>> { let mut transactions = Vec::<Transaction>::new(); let actualbudget_transactions = actualbudget_transactions .iter() - .filter(|t| is_not_category_transfer(t, actualbudget_category_transfers_account)); + .filter(|t| !t.is_starting_balance()) + .filter(|t| is_not_bucket_transfer(t, actualbudget_transactions)); let parent_actualbudget_transactions: Vec<&ActualbudgetTransaction> = actualbudget_transactions .clone() .filter(|t| t.is_parent) @@ -60,7 +55,10 @@ fn transform_transactions_without_context( amount: atomic_actualbudget_transaction.amount.try_into().unwrap(), date: transform_date(&atomic_actualbudget_transaction.date)?, account_id: transform_id(&atomic_actualbudget_transaction.account_id), - bucket_id: None, + bucket_id: atomic_actualbudget_transaction + .category_id + .clone() + .map(|id| transform_id(id.as_str())), }); } @@ -90,37 +88,17 @@ fn transform_transactions_without_context( #[cfg(test)] mod test { use actualbudget_fakes::{ - actualbudget_account::make_fake_actualbudget_account, - actualbudget_payee::{make_fake_actualbudget_payee, make_fake_actualbudget_payee_builder}, + actualbudget_payee::make_fake_actualbudget_payee_builder, actualbudget_transaction::make_fake_actualbudget_transaction_builder, }; use actualbudget_models::ActualbudgetDate; - use schist_models::DateUtc; + use schist_models::{DateUtc, Transaction}; use crate::transform_state::transform_id::transform_id; use super::transform_transactions; #[test] - fn when_one_category_transfer_transaction_then_returns_empty() { - let actualbudget_transactions = [make_fake_actualbudget_transaction_builder("0") - .category_id(Some(String::from("0"))) - .build() - .unwrap()]; - let actualbudget_category_transfers_account = make_fake_actualbudget_account("0"); - let actualbudget_payees = [make_fake_actualbudget_payee("0")]; - - let transactions = transform_transactions( - &actualbudget_transactions, - &Some(actualbudget_category_transfers_account), - &actualbudget_payees, - ) - .unwrap(); - - assert_eq!(0, transactions.len()); - } - - #[test] fn when_one_atomic_transaction_then_returns_transformed_transaction() { let actualbudget_transactions = [make_fake_actualbudget_transaction_builder("0") .account_id(String::from("0")) @@ -137,8 +115,7 @@ mod test { .unwrap()]; let transactions = - transform_transactions(&actualbudget_transactions, &None, &actualbudget_payees) - .unwrap(); + transform_transactions(&actualbudget_transactions, &actualbudget_payees).unwrap(); assert_eq!(1, transactions.len()); let transaction = transactions.get(0).unwrap(); @@ -153,11 +130,14 @@ mod test { fn when_one_split_transaction_then_returns_transformed_transaction() { let actualbudget_transactions = [ make_fake_actualbudget_transaction_builder("0") + .account_id(String::from("0")) + .date(ActualbudgetDate::from_i32(20240801)) .is_parent(true) - .amount(100) + .amount(300) .build() .unwrap(), make_fake_actualbudget_transaction_builder("1") + .account_id(String::from("0")) .amount(200) .category_id(Some(String::from("0"))) .is_child(true) @@ -166,8 +146,9 @@ mod test { .build() .unwrap(), make_fake_actualbudget_transaction_builder("2") - .amount(300) - .category_id(Some(String::from("0"))) + .account_id(String::from("0")) + .amount(100) + .category_id(Some(String::from("1"))) .is_child(true) .parent_id(Some(String::from("0"))) .notes(Some(String::from("notes B"))) @@ -175,11 +156,20 @@ mod test { .unwrap(), ]; - let transactions = transform_transactions(&actualbudget_transactions, &None, &[]).unwrap(); + let transactions = transform_transactions(&actualbudget_transactions, &[]).unwrap(); assert_eq!(1, transactions.len()); - - let transaction = transactions.get(0).unwrap(); - assert_eq!(100, transaction.amount); + assert_eq!( + Some(Transaction { + id: transform_id("0"), + account_id: transform_id("0"), + amount: 300, + bucket_id: None, + counterparty: String::from(""), + date: "2024-08-01".parse().unwrap(), + description: String::from(""), + }), + transactions.get(0).cloned(), + ); } } diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_category_transfers.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_transactions_to_bucket_transfers.rs index 78b466d..51545c3 100644 --- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_category_transfers.rs +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_transactions_to_bucket_transfers.rs @@ -1,32 +1,52 @@ -use actualbudget_models::{ActualbudgetAccount, ActualbudgetTransaction}; +use actualbudget_models::ActualbudgetTransaction; use anyhow::{bail, Context, Result}; use itertools::Itertools; use schist_models::BucketTransfer; -use super::{is_category_transfer, transform_id}; +use super::transform_id; -pub fn transform_category_transfers( +pub fn transform_transactions_to_bucket_transfers( actualbudget_transactions: &[ActualbudgetTransaction], - actualbudget_category_transfers_account: &Option<ActualbudgetAccount>, ) -> Result<Vec<BucketTransfer>> { - let actualbudget_category_transfers = actualbudget_transactions.iter().filter(|transaction| { - is_category_transfer(transaction, actualbudget_category_transfers_account) - }); - let actualbudget_parent_category_transfers: Vec<ActualbudgetTransaction> = - actualbudget_category_transfers - .clone() + let children: Vec<&ActualbudgetTransaction> = actualbudget_transactions + .into_iter() + .filter(|&t| t.is_child) + .collect(); + let parent_children_pairs: Vec<(&ActualbudgetTransaction, Vec<&ActualbudgetTransaction>)> = + actualbudget_transactions + .iter() .filter(|t| t.is_parent) - .map(ActualbudgetTransaction::clone) - .collect(); - let actualbudget_child_category_transfers: Vec<ActualbudgetTransaction> = - actualbudget_category_transfers - .filter(|t| t.is_child) - .map(ActualbudgetTransaction::clone) - .collect(); - - Ok(actualbudget_parent_category_transfers + .map(|parent| { + ( + parent, + children + .clone() + .into_iter() + .filter(|&c| c.parent_id.clone().is_some_and(|id| id == parent.id)) + .collect::<Vec<&ActualbudgetTransaction>>(), + ) + }) + .map(|(parent, children)| { + let children_total_amount: i32 = children.iter().map(|child| child.amount).sum(); + if parent.amount != children_total_amount { + bail!("parent transaction {} has amount: {}, but its {} children add up to a total amount of: {}", + parent.id, + parent.amount, + children.len(), + children_total_amount); + } + Ok((parent, children)) + }) + .filter(|result| + match result { + Ok((parent, _children)) => parent.amount == 0, + Err(_) => true, + }) + .try_collect()?; + + Ok(parent_children_pairs .iter() - .map(|pct| transform_category_transfer(pct, &actualbudget_child_category_transfers)) + .map(|(parent, children)| transform_category_transfer(parent, children)) .filter_ok(Option::is_some) .map_ok(Option::unwrap) .collect::<Result<Vec<BucketTransfer>>>() @@ -35,7 +55,7 @@ pub fn transform_category_transfers( fn transform_category_transfer( actualbudget_parent_category_transfer: &ActualbudgetTransaction, - actualbudget_child_category_transfers: &[ActualbudgetTransaction], + actualbudget_child_category_transfers: &[&ActualbudgetTransaction], ) -> Result<Option<BucketTransfer>> { transform_category_transfer_without_context( actualbudget_parent_category_transfer, @@ -46,20 +66,12 @@ fn transform_category_transfer( fn transform_category_transfer_without_context( actualbudget_parent_category_transfer: &ActualbudgetTransaction, - actualbudget_child_category_transfers: &[ActualbudgetTransaction], + actualbudget_child_category_transfers: &[&ActualbudgetTransaction], ) -> Result<Option<BucketTransfer>> { - let actualbudget_child_category_transfers = actualbudget_child_category_transfers - .iter() - .filter(|t| { - t.parent_id - .clone() - .is_some_and(|parent_id| parent_id == actualbudget_parent_category_transfer.id) - }) - .map(ActualbudgetTransaction::clone); - - let from_transfers: Vec<ActualbudgetTransaction> = actualbudget_child_category_transfers - .clone() - .filter(|t| t.amount < 0) + let from_transfers: Vec<&ActualbudgetTransaction> = actualbudget_child_category_transfers + .into_iter() + .cloned() + .filter(|&t| t.amount < 0) .collect(); if from_transfers.len() != 1 { bail!( @@ -78,9 +90,10 @@ fn transform_category_transfer_without_context( } let from_category_id: i32 = transform_id(&from_transfer.category_id.clone().unwrap()); - let to_transfers: Vec<ActualbudgetTransaction> = actualbudget_child_category_transfers - .clone() - .filter(|t| t.amount > 0) + let to_transfers: Vec<&ActualbudgetTransaction> = actualbudget_child_category_transfers + .iter() + .filter(|&t| t.amount > 0) + .cloned() .collect(); if to_transfers.len() != 1 { bail!( @@ -117,30 +130,23 @@ fn transform_category_transfer_without_context( #[cfg(test)] mod test { - use actualbudget_fakes::{ - actualbudget_account::make_fake_actualbudget_account, - actualbudget_transaction::make_fake_actualbudget_transaction_builder, - }; + use actualbudget_fakes::actualbudget_transaction::make_fake_actualbudget_transaction_builder; use actualbudget_models::{ActualbudgetTransaction, ActualbudgetTransactionBuilder}; use schist_models::BucketTransfer; use crate::transform_state::transform_id::transform_id; - use super::transform_category_transfers; + use super::transform_transactions_to_bucket_transfers; #[test] fn when_one_category_transfer_then_transforms() { - let actualbudget_category_transfers_account = make_fake_actualbudget_account("0"); let actualbudget_transactions = [ make_fake_parent_transaction("0"), make_fake_from_child_transaction("1", "0"), make_fake_to_child_transaction("2", "0"), ]; - let result = transform_category_transfers( - &actualbudget_transactions, - &Some(actualbudget_category_transfers_account), - ); + let result = transform_transactions_to_bucket_transfers(&actualbudget_transactions); assert!(result.is_ok()); let bucket_transfers = result.unwrap(); @@ -154,7 +160,6 @@ mod test { #[test] fn when_many_transactions_then_transforms() { - let actualbudget_category_transfers_account = make_fake_actualbudget_account("0"); let actualbudget_transactions = [ make_fake_parent_transaction("0"), make_fake_from_child_transaction_builder("1", 100, "0") @@ -184,10 +189,7 @@ mod test { .unwrap(), ]; - let result = transform_category_transfers( - &actualbudget_transactions, - &Some(actualbudget_category_transfers_account), - ); + let result = transform_transactions_to_bucket_transfers(&actualbudget_transactions); assert!(result.is_ok()); let bucket_transfers = result.unwrap(); @@ -214,7 +216,6 @@ mod test { #[test] fn when_multiple_child_from_transactions_then_err() { - let actualbudget_category_transfers_account = make_fake_actualbudget_account("0"); let actualbudget_transactions = [ make_fake_parent_transaction("0"), make_fake_from_child_transaction_builder("1", 50, "0") @@ -228,17 +229,13 @@ mod test { .unwrap(), ]; - let result = transform_category_transfers( - &actualbudget_transactions, - &Some(actualbudget_category_transfers_account), - ); + let result = transform_transactions_to_bucket_transfers(&actualbudget_transactions); assert!(result.is_err()); } #[test] fn when_multiple_child_to_transactions_then_err() { - let actualbudget_category_transfers_account = make_fake_actualbudget_account("0"); let actualbudget_transactions = [ make_fake_parent_transaction("0"), make_fake_from_child_transaction_builder("1", 100, "0") @@ -252,49 +249,37 @@ mod test { .unwrap(), ]; - let result = transform_category_transfers( - &actualbudget_transactions, - &Some(actualbudget_category_transfers_account), - ); + let result = transform_transactions_to_bucket_transfers(&actualbudget_transactions); assert!(result.is_err()); } #[test] fn when_no_child_from_transactions_then_err() { - let actualbudget_category_transfers_account = make_fake_actualbudget_account("0"); let actualbudget_transactions = [ make_fake_parent_transaction("0"), make_fake_to_child_transaction("1", "0"), ]; - let result = transform_category_transfers( - &actualbudget_transactions, - &Some(actualbudget_category_transfers_account), - ); + let result = transform_transactions_to_bucket_transfers(&actualbudget_transactions); assert!(result.is_err()); } #[test] fn when_no_child_to_transactions_then_err() { - let actualbudget_category_transfers_account = make_fake_actualbudget_account("0"); let actualbudget_transactions = [ make_fake_parent_transaction("0"), make_fake_from_child_transaction("1", "0"), ]; - let result = transform_category_transfers( - &actualbudget_transactions, - &Some(actualbudget_category_transfers_account), - ); + let result = transform_transactions_to_bucket_transfers(&actualbudget_transactions); assert!(result.is_err()); } #[test] fn when_child_amounts_dont_add_up_to_zero_then_err() { - let actualbudget_category_transfers_account = make_fake_actualbudget_account("0"); let actualbudget_transactions = [ make_fake_parent_transaction("0"), make_fake_from_child_transaction_builder("1", 50, "0") @@ -305,17 +290,13 @@ mod test { .unwrap(), ]; - let result = transform_category_transfers( - &actualbudget_transactions, - &Some(actualbudget_category_transfers_account), - ); + let result = transform_transactions_to_bucket_transfers(&actualbudget_transactions); assert!(result.is_err()); } #[test] fn when_child_category_transfer_has_no_category_then_err() { - let actualbudget_category_transfers_account = make_fake_actualbudget_account("0"); let actualbudget_transactions_0 = [ make_fake_parent_transaction("0"), make_fake_from_child_transaction_builder("1", 100, "0") @@ -337,14 +318,8 @@ mod test { .unwrap(), ]; - let result_0 = transform_category_transfers( - &actualbudget_transactions_0, - &Some(actualbudget_category_transfers_account.clone()), - ); - let result_1 = transform_category_transfers( - &actualbudget_transactions_1, - &Some(actualbudget_category_transfers_account), - ); + let result_0 = transform_transactions_to_bucket_transfers(&actualbudget_transactions_0); + let result_1 = transform_transactions_to_bucket_transfers(&actualbudget_transactions_1); assert!(result_0.is_err()); assert!(result_1.is_err()); diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets.rs deleted file mode 100644 index 999c3e8..0000000 --- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets.rs +++ /dev/null @@ -1,6 +0,0 @@ -use actualbudget_models::ActualbudgetZeroBudget; -use schist_models::Pipe; - -pub fn transform_zero_budgets(_actualbudget_zero_budgets: &[ActualbudgetZeroBudget]) -> Vec<Pipe> { - todo!() -} diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_budget_drips.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets_to_drips.rs index 76509cd..bfaef31 100644 --- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_budget_drips.rs +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets_to_drips.rs @@ -141,7 +141,7 @@ mod test { use schist_models::Drip; use schist_traits::Dateable; - use crate::transform_state::transform_budget_drips::map_actualbudget_zero_budget_to_budget_drips; + use crate::transform_state::transform_zero_budgets_to_drips::map_actualbudget_zero_budget_to_budget_drips; #[test] fn when_zero_budget_with_integer_budget_per_day_then_evenly_divided_budget_drips() { diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets_to_pipes.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets_to_pipes.rs new file mode 100644 index 0000000..2469ba9 --- /dev/null +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets_to_pipes.rs @@ -0,0 +1,211 @@ +use actualbudget_models::ActualbudgetZeroBudget; +use anyhow::Context; +use itertools::Itertools; +use schist_models::{BudgetPeriodUnit, Pipe}; + +use super::{transform_date, transform_id}; + +pub fn transform_zero_budgets_to_pipes( + actualbudget_zero_budgets: &[ActualbudgetZeroBudget], +) -> anyhow::Result<Vec<Pipe>> { + let pipes_with_consecutive_duplicates = actualbudget_zero_budgets + .iter() + .filter(|zb| zb.amount != 0) + .map(transform_zero_budget_to_pipe) + .collect::<anyhow::Result<Vec<Pipe>>>() + .context("Failed to transform Actualbudget zero budgets to Schist pipes")?; + + Ok(pipes_with_consecutive_duplicates + .into_iter() + .sorted_by_key(|pipe| pipe.start_date) + .sorted_by_key(|pipe| pipe.bucket_id) + .fold(Vec::new(), fold_pipes)) +} + +fn transform_zero_budget_to_pipe( + actualbudget_zero_budget: &ActualbudgetZeroBudget, +) -> anyhow::Result<Pipe> { + Ok(Pipe { + id: transform_id(&actualbudget_zero_budget.id), + amount: actualbudget_zero_budget.amount, + bucket_id: transform_id(&actualbudget_zero_budget.category_id), + period_length: 1, + period_unit: BudgetPeriodUnit::Month, + start_date: transform_date(&actualbudget_zero_budget.month)?, + }) +} + +fn fold_pipes(mut pipes: Vec<Pipe>, pipe: Pipe) -> Vec<Pipe> { + if let Some(index) = pipes.iter().position(|other_pipe| { + other_pipe.bucket_id == pipe.bucket_id && other_pipe.amount == pipe.amount + }) { + if pipe.start_date < pipes[index].start_date { + pipes.remove(index); + pipes.push(pipe); + } + } else { + pipes.push(pipe); + } + pipes +} + +#[cfg(test)] +mod test { + use actualbudget_models::{ActualbudgetDate, ActualbudgetZeroBudget}; + use schist_models::{BudgetPeriodUnit, Pipe}; + + use crate::transform_state::transform_id::transform_id; + + #[test] + fn when_zero_budget_is_zero_then_does_not_transform() { + let actualbudget_zero_budgets = vec![ActualbudgetZeroBudget { + id: String::from("zb-0"), + month: ActualbudgetDate::from_i32(202408), + category_id: String::from("category-0"), + amount: 0, + do_carry_over: true, + }]; + + let pipes = super::transform_zero_budgets_to_pipes(&actualbudget_zero_budgets); + + assert!(pipes.is_ok()); + assert_eq!(0, pipes.unwrap().len()); + } + + #[test] + fn when_zero_budget_is_not_zero_then_does_transform() { + let actualbudget_zero_budgets = vec![ + ActualbudgetZeroBudget { + id: String::from("zb-0"), + month: ActualbudgetDate::from_i32(202408), + category_id: String::from("category-0"), + amount: 100, + do_carry_over: true, + }, + ActualbudgetZeroBudget { + id: String::from("zb-1"), + month: ActualbudgetDate::from_i32(202408), + category_id: String::from("category-1"), + amount: -200, + do_carry_over: false, + }, + ]; + + let pipes = super::transform_zero_budgets_to_pipes(&actualbudget_zero_budgets); + + assert!(pipes.is_ok()); + let mut pipes = pipes.unwrap(); + pipes.sort_by_key(|pipe| -pipe.amount); + assert_eq!( + vec![ + Pipe { + id: transform_id("zb-0"), + amount: 100, + bucket_id: transform_id("category-0"), + period_length: 1, + period_unit: BudgetPeriodUnit::Month, + start_date: "2024-08-01".parse().unwrap(), + }, + Pipe { + id: transform_id("zb-1"), + amount: -200, + bucket_id: transform_id("category-1"), + period_length: 1, + period_unit: BudgetPeriodUnit::Month, + start_date: "2024-08-01".parse().unwrap() + }, + ], + pipes, + ); + } + + #[test] + fn when_zero_budgets_contain_adjacent_duplicates_then_merge() { + let actualbudget_zero_budgets = vec![ + ActualbudgetZeroBudget { + id: String::from("zb-0"), + month: ActualbudgetDate::from_i32(202408), + category_id: String::from("category-0"), + amount: 100, + do_carry_over: true, + }, + ActualbudgetZeroBudget { + id: String::from("zb-1"), + month: ActualbudgetDate::from_i32(202409), + category_id: String::from("category-1"), + amount: -200, + do_carry_over: false, + }, + ActualbudgetZeroBudget { + id: String::from("zb-0"), + month: ActualbudgetDate::from_i32(202410), + category_id: String::from("category-0"), + amount: 100, + do_carry_over: true, + }, + ActualbudgetZeroBudget { + id: String::from("zb-0"), + month: ActualbudgetDate::from_i32(202411), + category_id: String::from("category-0"), + amount: 200, + do_carry_over: false, + }, + ActualbudgetZeroBudget { + id: String::from("zb-1"), + month: ActualbudgetDate::from_i32(202410), + category_id: String::from("category-1"), + amount: -200, + do_carry_over: true, + }, + ActualbudgetZeroBudget { + id: String::from("zb-0"), + month: ActualbudgetDate::from_i32(202409), + category_id: String::from("category-0"), + amount: 100, + do_carry_over: false, + }, + ActualbudgetZeroBudget { + id: String::from("zb-1"), + month: ActualbudgetDate::from_i32(202408), + category_id: String::from("category-1"), + amount: -200, + do_carry_over: true, + }, + ]; + + let pipes = super::transform_zero_budgets_to_pipes(&actualbudget_zero_budgets); + + assert!(pipes.is_ok()); + let mut pipes = pipes.unwrap(); + pipes.sort_by_key(|pipe| -pipe.amount); + assert_eq!( + vec![ + Pipe { + id: transform_id("zb-0"), + amount: 200, + bucket_id: transform_id("category-0"), + period_length: 1, + period_unit: BudgetPeriodUnit::Month, + start_date: "2024-11-01".parse().unwrap(), + }, + Pipe { + id: transform_id("zb-0"), + amount: 100, + bucket_id: transform_id("category-0"), + period_length: 1, + period_unit: BudgetPeriodUnit::Month, + start_date: "2024-08-01".parse().unwrap(), + }, + Pipe { + id: transform_id("zb-1"), + amount: -200, + bucket_id: transform_id("category-1"), + period_length: 1, + period_unit: BudgetPeriodUnit::Month, + start_date: "2024-08-01".parse().unwrap() + }, + ], + pipes, + ); + } +} diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/account_matchers.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/account_matchers.rs index eaa1805..358d894 100644 --- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/account_matchers.rs +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/account_matchers.rs @@ -10,10 +10,10 @@ pub fn get_account_matchers() -> Vec<Box<dyn Fn(&Account) -> bool>> { fn matches_bank_account(account: &Account) -> bool { match account { Account { + id: _id, name, - opening_balance: 0, + opening_balance: 1200_00, opening_date, - .. } => name == "My bank account" && *opening_date == "2024-08-01".parse().unwrap(), _ => false, } @@ -22,10 +22,10 @@ fn matches_bank_account(account: &Account) -> bool { fn matches_cash_account(account: &Account) -> bool { match account { Account { + id: _id, name, - opening_balance: 0, + opening_balance: 45_10, opening_date, - .. } => name == "My cash account" && *opening_date == "2024-08-01".parse().unwrap(), _ => false, } diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_matchers.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_matchers.rs index 3821806..5753847 100644 --- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_matchers.rs +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_matchers.rs @@ -1,16 +1,27 @@ use schist_models::Bucket; -use super::bucket_names::BUCKET_NAMES; +use super::bucket_names::BUCKET_NAMES_AND_GROUP_NAMES; pub fn get_bucket_matchers() -> Vec<Box<dyn Fn(&Bucket) -> bool>> { - BUCKET_NAMES + BUCKET_NAMES_AND_GROUP_NAMES .iter() - .map(|bucket_name| -> Box<dyn Fn(&Bucket) -> bool> { - Box::new(|c| matches_bucket(c, bucket_name)) - }) + .map( + |bucket_name_and_group_name| -> Box<dyn Fn(&Bucket) -> bool> { + Box::new(|c| matches_bucket(c, bucket_name_and_group_name)) + }, + ) .collect() } -fn matches_bucket(bucket: &Bucket, bucket_name: &str) -> bool { - bucket.name == bucket_name && bucket.balance == None && bucket.balance_cache_key == None +fn matches_bucket(bucket: &Bucket, (bucket_name, group_name): &(&str, &str)) -> bool { + match bucket { + Bucket { + id: _id, + balance: None, + balance_cache_key: None, + group, + name, + } => *name == *bucket_name && *group == *group_name, + _ => false, + } } diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_names.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_names.rs index 5f232a8..d8b5d3f 100644 --- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_names.rs +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_names.rs @@ -3,17 +3,19 @@ pub const BUCKET_NAME_GENERAL: &str = "General"; pub const BUCKET_NAME_BILLS: &str = "Bills"; pub const BUCKET_NAME_BILLS_FLEXIBLE: &str = "Bills (Flexible)"; pub const BUCKET_NAME_SAVINGS: &str = "Savings"; -pub const BUCKET_NAME_STARTING_BALANCES: &str = "Starting Balances"; pub const BUCKET_NAME_INCOME: &str = "Income"; -pub const BUCKET_NAMES: &[&'static str] = &[ - BUCKET_NAME_FOOD, - BUCKET_NAME_GENERAL, - BUCKET_NAME_BILLS, - BUCKET_NAME_BILLS_FLEXIBLE, - BUCKET_NAME_SAVINGS, - BUCKET_NAME_STARTING_BALANCES, - BUCKET_NAME_INCOME, +pub const GROUP_NAME_USUAL_EXPENSES: &str = "Usual Expenses"; +pub const GROUP_NAME_SAVINGS: &str = "Investments and Savings"; +pub const GROUP_NAME_INCOME: &str = "Income"; + +pub const BUCKET_NAMES_AND_GROUP_NAMES: &[(&'static str, &'static str)] = &[ + (BUCKET_NAME_FOOD, GROUP_NAME_USUAL_EXPENSES), + (BUCKET_NAME_GENERAL, GROUP_NAME_USUAL_EXPENSES), + (BUCKET_NAME_BILLS, GROUP_NAME_USUAL_EXPENSES), + (BUCKET_NAME_BILLS_FLEXIBLE, GROUP_NAME_USUAL_EXPENSES), + (BUCKET_NAME_SAVINGS, GROUP_NAME_SAVINGS), + (BUCKET_NAME_INCOME, GROUP_NAME_INCOME), ]; pub struct BucketNamesDict { @@ -22,7 +24,6 @@ pub struct BucketNamesDict { pub bills: &'static str, pub bills_flexible: &'static str, pub savings: &'static str, - pub starting_balances: &'static str, pub income: &'static str, } @@ -32,6 +33,5 @@ pub const BUCKET_NAMES_DICT: BucketNamesDict = BucketNamesDict { bills: BUCKET_NAME_BILLS, bills_flexible: BUCKET_NAME_BILLS_FLEXIBLE, savings: BUCKET_NAME_SAVINGS, - starting_balances: BUCKET_NAME_STARTING_BALANCES, income: BUCKET_NAME_INCOME, }; diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/budget_drip_matchers.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/drip_matchers.rs index 85ad045..629a04b 100644 --- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/budget_drip_matchers.rs +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/drip_matchers.rs @@ -8,80 +8,71 @@ use crate::common::bucket_names::{ BUCKET_NAME_SAVINGS, }; -pub fn get_num_expected_budget_drips() -> usize { +pub fn get_num_expected_drips() -> usize { let num_expected_dates = DATES.len(); let num_expected_buckets = 5; num_expected_dates * num_expected_buckets } -pub fn assert_budget_drip_matches(budget_drip: &Drip, buckets: &[Bucket]) { +pub fn assert_drip_matches(drip: &Drip, buckets: &[Bucket]) { assert!( - DATES - .iter() - .any(|date| *LazyLock::force(date) == budget_drip.date), - "budget drip had unexpected date: {:?}", - budget_drip, + DATES.iter().any(|date| *LazyLock::force(date) == drip.date), + "drip had unexpected date: {:?}", + drip, ); let bucket_name = buckets .iter() - .find(|c| c.id == budget_drip.bucket_id) + .find(|c| c.id == drip.bucket_id) .map(|c| c.name.clone()) .expect( format!( - "budget drip {:?} had unexpected bucket ID. buckets were: {:?}", - budget_drip, buckets, + "drip {:?} had unexpected bucket ID. buckets were: {:?}", + drip, buckets, ) .as_str(), ); - let expected_quantity = match ( - bucket_name.as_str(), - budget_drip.date.month(), - budget_drip.date.day(), - ) { + let expected_quantity = match (bucket_name.as_str(), drip.date.month(), drip.date.day()) { (BUCKET_NAME_FOOD, 8 | 10, 1) => 6_50, (BUCKET_NAME_FOOD, 8 | 10, _) => 6_45, (BUCKET_NAME_FOOD, 9 | 11, 1) => 6_86, (BUCKET_NAME_FOOD, 9 | 11, _) => 6_66, - (BUCKET_NAME_FOOD, _, _) => panic!("budget drip {:?} had unexpected date", budget_drip), + (BUCKET_NAME_FOOD, _, _) => panic!("drip {:?} had unexpected date", drip), (BUCKET_NAME_GENERAL, 8 | 10, 1) => 3_40, (BUCKET_NAME_GENERAL, 8 | 10, _) => 3_22, (BUCKET_NAME_GENERAL, 9 | 11, 1) => 3_43, (BUCKET_NAME_GENERAL, 9 | 11, _) => 3_33, - (BUCKET_NAME_GENERAL, _, _) => panic!("budget drip {:?} had unexpected date", budget_drip), + (BUCKET_NAME_GENERAL, _, _) => panic!("drip {:?} had unexpected date", drip), (BUCKET_NAME_BILLS, 8 | 10, 1) => 4_10, (BUCKET_NAME_BILLS, 8 | 10, _) => 4_03, (BUCKET_NAME_BILLS, 9 | 11, 1) => 4_36, (BUCKET_NAME_BILLS, 9 | 11, _) => 4_16, - (BUCKET_NAME_BILLS, _, _) => panic!("budget drip {:?} had unexpected date", budget_drip), + (BUCKET_NAME_BILLS, _, _) => panic!("drip {:?} had unexpected date", drip), (BUCKET_NAME_BILLS_FLEXIBLE, 8 | 10, 1) => 1_00, (BUCKET_NAME_BILLS_FLEXIBLE, 8 | 10, _) => 80, (BUCKET_NAME_BILLS_FLEXIBLE, 9 | 11, 1) => 93, (BUCKET_NAME_BILLS_FLEXIBLE, 9 | 11, _) => 83, (BUCKET_NAME_BILLS_FLEXIBLE, _, _) => { - panic!("budget drip {:?} had unexpected date", budget_drip) + panic!("drip {:?} had unexpected date", drip) } (BUCKET_NAME_SAVINGS, 8 | 10, 1) => 13_00, (BUCKET_NAME_SAVINGS, 8 | 10, _) => 12_90, (BUCKET_NAME_SAVINGS, 9 | 11, 1) => 13_43, (BUCKET_NAME_SAVINGS, 9 | 11, _) => 13_33, - (BUCKET_NAME_SAVINGS, _, _) => panic!("budget drip {:?} had unexpected date", budget_drip), + (BUCKET_NAME_SAVINGS, _, _) => panic!("drip {:?} had unexpected date", drip), - (_, _, _) => panic!( - "budget drip {:?} had unexpected bucket name {}", - budget_drip, bucket_name, - ), + (_, _, _) => panic!("drip {:?} had unexpected bucket name {}", drip, bucket_name,), }; assert_eq!( - expected_quantity, budget_drip.amount, - "budget drip {:?} had unexpected quantity", - budget_drip, + expected_quantity, drip.amount, + "drip {:?} had unexpected quantity", + drip, ); } diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/mod.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/mod.rs index f067d0d..82d7870 100644 --- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/mod.rs +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/mod.rs @@ -4,8 +4,9 @@ mod account_transfer_matchers; mod bucket_matchers; mod bucket_names; mod bucket_transfer_matchers; -mod budget_drip_matchers; mod db_url; +mod drip_matchers; +mod pipe_matchers; mod test_context; mod transaction_matchers; @@ -15,13 +16,12 @@ pub use account_transfer_matchers::get_account_transfer_matchers; pub use bucket_matchers::get_bucket_matchers; pub use bucket_names::BUCKET_NAMES_DICT; pub use bucket_transfer_matchers::get_bucket_transfer_matchers; -pub use budget_drip_matchers::assert_budget_drip_matches; -pub use budget_drip_matchers::get_num_expected_budget_drips; pub use db_url::TEST_IN_DB_URL; pub use db_url::TEST_OUT_DB_URL; +pub use drip_matchers::assert_drip_matches; +pub use drip_matchers::get_num_expected_drips; +pub use pipe_matchers::get_pipe_matchers; pub use test_context::TestContext; pub use transaction_matchers::get_transaction_matchers; -pub use transaction_matchers::matches_bank_account_initial_transaction; -pub use transaction_matchers::matches_cash_account_initial_transaction; pub use transaction_matchers::matches_food_transaction; pub use transaction_matchers::matches_income_transaction; diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/pipe_matchers.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/pipe_matchers.rs new file mode 100644 index 0000000..3ca92c8 --- /dev/null +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/pipe_matchers.rs @@ -0,0 +1,36 @@ +use schist_models::{Bucket, BudgetPeriodUnit, Pipe}; + +use super::bucket_names::BUCKET_NAMES_DICT; + +macro_rules! matches_pipe { + ($pipe:ident, $amount:expr, $start_date:expr) => { + Box::new(|pipe: &Pipe, buckets: &[Bucket]| -> bool { + let bucket = buckets + .iter() + .find(|c| c.name == BUCKET_NAMES_DICT.$pipe) + .expect("failed to find $pipe bucket"); + + match pipe { + Pipe { + id: _id, + amount: $amount, + bucket_id, + period_length: 1, + period_unit: BudgetPeriodUnit::Month, + start_date, + } => *bucket_id == bucket.id && *start_date == $start_date.parse().unwrap(), + _ => false, + } + }) + }; +} + +pub fn get_pipe_matchers() -> Vec<Box<dyn Fn(&Pipe, &[Bucket]) -> bool>> { + vec![ + matches_pipe!(food, 200_00, "2024-08-01"), + matches_pipe!(general, 100_00, "2024-08-01"), + matches_pipe!(bills, 125_00, "2024-08-01"), + matches_pipe!(bills_flexible, 25_00, "2024-08-01"), + matches_pipe!(savings, 400_00, "2024-08-01"), + ] +} diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/transaction_matchers.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/transaction_matchers.rs index e6c3f27..836a8a1 100644 --- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/transaction_matchers.rs +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/transaction_matchers.rs @@ -1,23 +1,28 @@ -use schist_models::{Account, Transaction}; +use schist_models::{Account, Bucket, Transaction}; + +use crate::common::bucket_names::{BUCKET_NAME_FOOD, BUCKET_NAME_INCOME}; use super::ACCOUNT_NAMES_DICT; -pub fn get_transaction_matchers() -> Vec<Box<dyn Fn(&Transaction, &[Account]) -> bool>> { +pub fn get_transaction_matchers() -> Vec<Box<dyn Fn(&Transaction, &[Account], &[Bucket]) -> bool>> { vec![ - Box::new(|t, a| matches_bank_account_initial_transaction(t, a)), - Box::new(|t, a| matches_cash_account_initial_transaction(t, a)), - Box::new(|t, a| matches_income_transaction(t, a, "08")), - Box::new(|t, a| matches_income_transaction(t, a, "09")), - Box::new(|t, a| matches_income_transaction(t, a, "10")), - Box::new(|t, a| matches_income_transaction(t, a, "11")), - Box::new(|t, a| matches_food_transaction(t, a, "08")), - Box::new(|t, a| matches_food_transaction(t, a, "09")), - Box::new(|t, a| matches_food_transaction(t, a, "10")), - Box::new(|t, a| matches_food_transaction(t, a, "11")), + Box::new(|t, a, b| matches_income_transaction(t, a, b, "08")), + Box::new(|t, a, b| matches_income_transaction(t, a, b, "09")), + Box::new(|t, a, b| matches_income_transaction(t, a, b, "10")), + Box::new(|t, a, b| matches_income_transaction(t, a, b, "11")), + Box::new(|t, a, b| matches_food_transaction(t, a, b, "08")), + Box::new(|t, a, b| matches_food_transaction(t, a, b, "09")), + Box::new(|t, a, b| matches_food_transaction(t, a, b, "10")), + Box::new(|t, a, b| matches_food_transaction(t, a, b, "11")), ] } -pub fn matches_bank_account_initial_transaction(t: &Transaction, accounts: &[Account]) -> bool { +pub fn matches_income_transaction( + t: &Transaction, + accounts: &[Account], + buckets: &[Bucket], + month: &str, +) -> bool { let bank_account_id = accounts .iter() .find(|a| a.name == ACCOUNT_NAMES_DICT.bank_account) @@ -25,66 +30,15 @@ pub fn matches_bank_account_initial_transaction(t: &Transaction, accounts: &[Acc .id .clone(); - match t { - Transaction { - account_id, - bucket_id, - date, - id: _id, - description, - counterparty: payee, - amount: 1200_00, - } => { - *account_id == bank_account_id - && *bucket_id == get_bucket_id() - && *date == "2024-08-01".parse().unwrap() - && description == "" - && payee == "Starting Balance" - } - _ => false, - } -} - -pub fn matches_cash_account_initial_transaction(t: &Transaction, accounts: &[Account]) -> bool { - let bank_account_id = accounts + let income_bucket = buckets .iter() - .find(|a| a.name == ACCOUNT_NAMES_DICT.cash_account) - .expect("failed to find cash account") - .id - .clone(); + .find(|c| c.name == BUCKET_NAME_INCOME) + .expect("failed to find income bucket"); match t { Transaction { account_id, - bucket_id, - date, - id: _id, - description, - counterparty: payee, - amount: 45_10, - } => { - *account_id == bank_account_id - && *bucket_id == get_bucket_id() - && *date == "2024-08-01".parse().unwrap() - && description == "" - && payee == "Starting Balance" - } - _ => false, - } -} - -pub fn matches_income_transaction(t: &Transaction, accounts: &[Account], month: &str) -> bool { - let bank_account_id = accounts - .iter() - .find(|a| a.name == ACCOUNT_NAMES_DICT.bank_account) - .expect("failed to find bank account") - .id - .clone(); - - match t { - Transaction { - account_id, - bucket_id, + bucket_id: Some(bucket_id), date, description, id: _id, @@ -92,7 +46,7 @@ pub fn matches_income_transaction(t: &Transaction, accounts: &[Account], month: amount: 846_46, } => { *account_id == bank_account_id - && *bucket_id == get_bucket_id() + && *bucket_id == income_bucket.id && *date == format!("2024-{}-21", month).parse().unwrap() && description == "Salary" && payee == "Employer Ltd" @@ -101,7 +55,12 @@ pub fn matches_income_transaction(t: &Transaction, accounts: &[Account], month: } } -pub fn matches_food_transaction(t: &Transaction, accounts: &[Account], month: &str) -> bool { +pub fn matches_food_transaction( + t: &Transaction, + accounts: &[Account], + buckets: &[Bucket], + month: &str, +) -> bool { let bank_account_id = accounts .iter() .find(|a| a.name == ACCOUNT_NAMES_DICT.bank_account) @@ -109,10 +68,15 @@ pub fn matches_food_transaction(t: &Transaction, accounts: &[Account], month: &s .id .clone(); + let food_bucket = buckets + .iter() + .find(|c| c.name == BUCKET_NAME_FOOD) + .expect("failed to find food bucket"); + match t { Transaction { account_id, - bucket_id, + bucket_id: Some(bucket_id), date, description, id: _id, @@ -120,7 +84,7 @@ pub fn matches_food_transaction(t: &Transaction, accounts: &[Account], month: &s amount: -200_00, } => { *account_id == bank_account_id - && *bucket_id == get_bucket_id() + && *bucket_id == food_bucket.id && *date == format!("2024-{}-08", month).parse().unwrap() && description == "" && payee == "Tesco" @@ -128,7 +92,3 @@ pub fn matches_food_transaction(t: &Transaction, accounts: &[Account], month: &s _ => false, } } - -fn get_bucket_id() -> Option<i32> { - todo!() -} diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/resources/actualbudget_test_db.sqlite b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/resources/actualbudget_test_db.sqlite Binary files differindex f8d3f5d..5f89924 100644 --- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/resources/actualbudget_test_db.sqlite +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/resources/actualbudget_test_db.sqlite diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/run.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/run.rs index aff034f..04c21ca 100644 --- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/run.rs +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/run.rs @@ -2,18 +2,20 @@ pub mod common; use actualbudget_to_schist_transformer::run::run; use common::{ - assert_budget_drip_matches, get_account_matchers, get_account_transfer_matchers, - get_bucket_matchers, get_bucket_transfer_matchers, get_num_expected_budget_drips, - get_transaction_matchers, TestContext, TEST_IN_DB_URL, TEST_OUT_DB_URL, + assert_drip_matches, get_account_matchers, get_account_transfer_matchers, get_bucket_matchers, + get_bucket_transfer_matchers, get_num_expected_drips, get_transaction_matchers, TestContext, + TEST_IN_DB_URL, TEST_OUT_DB_URL, }; use diesel::{Connection, SqliteConnection}; -use schist_models::Pipe; + use schist_queries::{ account_transfers::get_all_account_transfers, accounts::get_all_accounts, bucket_transfers::get_all_bucket_transfers, buckets::get_all_buckets, drips::get_all_drips, pipes::get_all_pipes, transactions::get_all_transactions, }; +use crate::common::get_pipe_matchers; + #[test] fn given_test_database_when_run_then_exports_expected_schist_state() { let _context = TestContext::new(); @@ -49,9 +51,9 @@ fn given_test_database_when_run_then_exports_expected_schist_state() { ); } - assert_eq!(get_num_expected_budget_drips(), drips.len()); + assert_eq!(get_num_expected_drips(), drips.len()); for budget_drip in drips { - assert_budget_drip_matches(&budget_drip, &buckets); + assert_drip_matches(&budget_drip, &buckets); } let bucket_matchers = get_bucket_matchers(); @@ -82,11 +84,19 @@ fn given_test_database_when_run_then_exports_expected_schist_state() { assert!( transaction_matchers .iter() - .any(|m| m(&transaction, &accounts)), + .any(|m| m(&transaction, &accounts, &buckets)), "no match found for transaction {:?}", transaction, ); } - assert_eq!(Vec::<Pipe>::new(), pipes); + let pipe_matchers = get_pipe_matchers(); + assert_eq!(pipe_matchers.len(), pipes.len()); + for pipe in pipes.clone() { + assert!( + pipe_matchers.iter().any(|p| p(&pipe, &buckets)), + "no match found for pipe: {:?}", + pipe + ); + } } |
