From fe85652e4d3e8906824baddaafd05d19105579b8 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Mon, 25 Aug 2025 16:35:45 +0000 Subject: task-072 Co-authored-by: Joe Carstairs Reviewed-on: https://git.joeac.net/joeac/schist/pulls/3 Co-authored-by: Joe Carstairs Co-committed-by: Joe Carstairs --- .../src/actualbudget_category.rs | 1 + .../src/actualbudget_category_group.rs | 13 + .../src/actualbudget_transaction.rs | 3 +- .../actualbudget_fakes/src/lib.rs | 1 + .../src/actualbudget_category.rs | 1 + .../src/actualbudget_category_group.rs | 9 + .../actualbudget_models/src/actualbudget_schema.rs | 12 +- .../src/actualbudget_transaction.rs | 11 +- .../actualbudget_models/src/lib.rs | 3 + .../src/actualbudget_category_groups.rs | 11 + .../actualbudget_queries/src/lib.rs | 1 + .../tests/resources/actualbudget_test_db.sqlite | Bin 528384 -> 573440 bytes .../resources/actualbudget_test_db_metadata.json | 10 +- .../actualbudget_to_schist_transformer/Cargo.toml | 1 + .../src/actualbudget_state.rs | 24 +- .../src/transform_state.rs | 27 ++ .../src/transform_state/is_bucket_transfer.rs | 46 +++ .../src/transform_state/is_category_transfer.rs | 18 - .../src/transform_state/mod.rs | 27 -- .../transform_state/transform_account_transfers.rs | 18 +- .../src/transform_state/transform_accounts.rs | 182 ++++++++- .../src/transform_state/transform_budget_drips.rs | 325 ---------------- .../src/transform_state/transform_categories.rs | 143 -------- .../transform_categories_to_buckets.rs | 263 +++++++++++++ .../transform_category_transfers.rs | 408 --------------------- .../src/transform_state/transform_state.rs | 49 +-- .../src/transform_state/transform_transactions.rs | 76 ++-- .../transform_transactions_to_bucket_transfers.rs | 383 +++++++++++++++++++ .../src/transform_state/transform_zero_budgets.rs | 6 - .../transform_zero_budgets_to_drips.rs | 325 ++++++++++++++++ .../transform_zero_budgets_to_pipes.rs | 211 +++++++++++ .../tests/common/account_matchers.rs | 8 +- .../tests/common/bucket_matchers.rs | 25 +- .../tests/common/bucket_names.rs | 22 +- .../tests/common/budget_drip_matchers.rs | 211 ----------- .../tests/common/drip_matchers.rs | 202 ++++++++++ .../tests/common/mod.rs | 10 +- .../tests/common/pipe_matchers.rs | 36 ++ .../tests/common/transaction_matchers.rs | 112 ++---- .../tests/resources/actualbudget_test_db.sqlite | Bin 73 -> 573440 bytes .../tests/run.rs | 26 +- 41 files changed, 1904 insertions(+), 1356 deletions(-) create mode 100644 transformers/actualbudget_transformer/actualbudget_fakes/src/actualbudget_category_group.rs create mode 100644 transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_category_group.rs create mode 100644 transformers/actualbudget_transformer/actualbudget_queries/src/actualbudget_category_groups.rs create mode 100644 transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state.rs create mode 100644 transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_bucket_transfer.rs delete mode 100644 transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_category_transfer.rs delete mode 100644 transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/mod.rs delete mode 100644 transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_budget_drips.rs delete mode 100644 transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_categories.rs create mode 100644 transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_categories_to_buckets.rs delete mode 100644 transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_category_transfers.rs create mode 100644 transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_transactions_to_bucket_transfers.rs delete mode 100644 transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets.rs create mode 100644 transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets_to_drips.rs create mode 100644 transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets_to_pipes.rs delete mode 100644 transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/budget_drip_matchers.rs create mode 100644 transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/drip_matchers.rs create mode 100644 transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/pipe_matchers.rs (limited to 'transformers') 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, 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, +} 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,10 +8,20 @@ diesel::table! { diesel::table! { categories (id) { id -> Text, + #[sql_name = "cat_group"] + group_id -> Nullable, name -> Text, } } +diesel::table! { + category_groups (id) { + id -> Text, + name -> Nullable, + } +} + + diesel::table! { v_transactions (id) { id -> Text, @@ -27,10 +37,10 @@ diesel::table! { notes -> Nullable, date -> Integer, transfer_id -> Nullable, + 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, pub date: ActualbudgetDate, pub transfer_id: Option, + 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, 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 index 4146bb0..5f89924 100644 Binary files a/transformers/actualbudget_transformer/actualbudget_queries/tests/resources/actualbudget_test_db.sqlite and b/transformers/actualbudget_transformer/actualbudget_queries/tests/resources/actualbudget_test_db.sqlite differ 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, pub actualbudget_categories: Vec, + pub actualbudget_category_groups: Vec, pub actualbudget_payees: Vec, pub actualbudget_zero_budgets: Vec, - pub actualbudget_category_transfers_account: Option, pub actualbudget_transactions: Vec, } -pub fn read_actualbudget_state(actualbudget_connection: &mut SqliteConnection) -> Result { +pub fn read_actualbudget_state( + actualbudget_connection: &mut SqliteConnection, +) -> Result { 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.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state.rs new file mode 100644 index 0000000..a116649 --- /dev/null +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state.rs @@ -0,0 +1,27 @@ +mod is_account_transfer; +mod is_bucket_transfer; +mod transform_account_transfers; +mod transform_accounts; +mod transform_categories_to_buckets; +mod transform_date; +mod transform_id; +mod transform_state; +mod transform_transactions; +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_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_to_buckets::transform_categories_to_buckets; +use transform_date::transform_date; +use transform_id::transform_id; +use transform_transactions::transform_transactions; +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 -) -> 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 -) -> bool { - !is_category_transfer(transaction, actualbudget_category_transfers_account) -} 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/mod.rs deleted file mode 100644 index 5fadc2c..0000000 --- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/mod.rs +++ /dev/null @@ -1,27 +0,0 @@ -mod is_account_transfer; -mod is_category_transfer; -mod transform_account_transfers; -mod transform_accounts; -mod transform_budget_drips; -mod transform_categories; -mod transform_category_transfers; -mod transform_date; -mod transform_id; -mod transform_state; -mod transform_transactions; -mod transform_zero_budgets; - -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 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_date::transform_date; -use transform_id::transform_id; -use transform_transactions::transform_transactions; -use transform_zero_budgets::transform_zero_budgets; - -pub use transform_state::transform_state; 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::().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, + actualbudget_transactions: &[ActualbudgetTransaction], first_activity_date: &DateUtc, ) -> Vec { 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_budget_drips.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_budget_drips.rs deleted file mode 100644 index 76509cd..0000000 --- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_budget_drips.rs +++ /dev/null @@ -1,325 +0,0 @@ -use std::hash::{DefaultHasher, Hash, Hasher}; - -use actualbudget_models::ActualbudgetZeroBudget; -use anyhow::{Context, Result}; -use itertools::Itertools; -use schist_models::{DateUtc, Drip}; -use schist_traits::Dateable; - -use super::{transform_date::transform_date, transform_id::transform_id}; - -pub fn transform_budget_drips( - actualbudget_zero_budgets: &[ActualbudgetZeroBudget], -) -> Result> { - let non_zero_actualbudget_zero_budgets: Vec = actualbudget_zero_budgets - .iter() - .filter(|zb| zb.amount != 0) - .cloned() - .collect(); - - let budget_drips = - group_actualbudget_zero_budgets_by_category_id(&non_zero_actualbudget_zero_budgets) - .iter() - .map(sort_actualbudget_zero_budgets_by_month) - .flat_map(|sorted_actualbudget_zero_budgets| { - sorted_actualbudget_zero_budgets - .iter() - .map(map_actualbudget_zero_budget_to_budget_drips) - .collect::>>>() - }) - .flatten() - .flatten() - .filter(|bd| bd.amount != 0) - .collect(); - - Ok(budget_drips) -} - -fn map_actualbudget_zero_budget_to_budget_drips( - actualbudget_zero_budget: &ActualbudgetZeroBudget, -) -> Result> { - let context = || "failed to map actualbudget zero budgets to budget drips"; - - let first_day_of_month: DateUtc = Into::::into( - transform_date(&actualbudget_zero_budget.month).with_context(context)?, - ); - let month = first_day_of_month.month(); - let days_in_month: Vec = (0..31) - .map(|i| first_day_of_month.add_days(i)) - .take_while(|d| match d { - Err(_) => true, - Ok(d) => d.month() == month, - }) - .filter(Result::is_ok) - .map(Result::unwrap) - .collect(); - - // unwrap() can panic, but days_in_month is guaranteed to have between 0 and - // 31 elements, so we should be grand - let num_days_in_month: i32 = days_in_month.len().try_into().unwrap(); - - let amount = actualbudget_zero_budget.amount / num_days_in_month; - let remainder_amount = actualbudget_zero_budget.amount - (amount * num_days_in_month); - - let budget_drips: Vec = days_in_month - .iter() - .map(|&date| Drip { - id: transform_budget_drip_id(&actualbudget_zero_budget, &date), - bucket_id: transform_id(&actualbudget_zero_budget.category_id), - date, - amount: if date == first_day_of_month { - amount + remainder_amount - } else { - amount - }, - inserted_at_unix_seconds: std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("The current time should be after the Unix epoch") - .as_secs() - .try_into() - .expect("The current time's timestamp in seconds should fit into a 32-bit signed integer"), - pipe_id: 0, - }) - .collect(); - - Ok(budget_drips) -} - -fn sort_actualbudget_zero_budgets_by_month( - actualbudget_zero_budgets_by_category_id: &Vec, -) -> Vec { - let mut actualbudget_zero_budgets = actualbudget_zero_budgets_by_category_id.clone(); - actualbudget_zero_budgets.sort_by_key(|zb| zb.month.clone()); - actualbudget_zero_budgets -} - -fn group_actualbudget_zero_budgets_by_category_id( - actualbudget_zero_budgets: &[ActualbudgetZeroBudget], -) -> Vec> { - let category_ids = actualbudget_zero_budgets - .iter() - .map(|zb| zb.category_id.clone()) - .unique(); - - category_ids - .map(|category_id| { - actualbudget_zero_budgets - .iter() - .filter(|zb| zb.category_id == category_id) - .map(ActualbudgetZeroBudget::clone) - .collect() - }) - .collect() -} - -fn transform_budget_drip_id( - actualbudget_zero_budget: &ActualbudgetZeroBudget, - date: &DateUtc, -) -> i32 { - let mut hasher = DefaultHasher::new(); - let unique_str = actualbudget_zero_budget.id.clone() + date.to_string().as_str(); - unique_str.hash(&mut hasher); - (hasher.finish() % std::convert::TryInto::::try_into(i32::MAX).unwrap()) - .try_into() - .unwrap() -} - -#[cfg(test)] -mod test { - use actualbudget_fakes::actualbudget_zero_budget::make_fake_actualbudget_zero_budget_builder; - use actualbudget_models::ActualbudgetDate; - use schist_models::Drip; - use schist_traits::Dateable; - - use crate::transform_state::transform_id::transform_id; - - use super::transform_budget_drips; - - mod map_actualbudget_zero_budget_to_budget_drips { - use actualbudget_fakes::actualbudget_zero_budget::make_fake_actualbudget_zero_budget_builder; - use actualbudget_models::ActualbudgetDate; - use schist_models::Drip; - use schist_traits::Dateable; - - use crate::transform_state::transform_budget_drips::map_actualbudget_zero_budget_to_budget_drips; - - #[test] - fn when_zero_budget_with_integer_budget_per_day_then_evenly_divided_budget_drips() { - let actualbudget_zero_budget = make_fake_actualbudget_zero_budget_builder("0") - .amount(31 * 100) - .category_id(String::from("0")) - .month(ActualbudgetDate::from_i32(202401)) - .build() - .unwrap(); - - let result = map_actualbudget_zero_budget_to_budget_drips(&actualbudget_zero_budget); - - assert!(result.is_ok()); - let mut budget_drips = result.unwrap(); - assert_eq!(31, budget_drips.len()); - assert!(budget_drips.iter().all(|bd| bd.amount == 100)); - budget_drips.sort_by_key(|bd| bd.date); - for i in 1..31 { - assert_eq!( - Ok(i), - budget_drips.get(i - 1).unwrap().date.day().try_into() - ); - } - } - - #[test] - fn when_zero_budget_with_non_integer_budget_per_day_then_remainder_on_one_day() { - let actualbudget_zero_budget = make_fake_actualbudget_zero_budget_builder("0") - .amount(31 * 100 + 30) - .category_id(String::from("0")) - .month(ActualbudgetDate::from_i32(202401)) - .build() - .unwrap(); - - let result = map_actualbudget_zero_budget_to_budget_drips(&actualbudget_zero_budget); - - assert!(result.is_ok()); - let budget_drips = result.unwrap(); - assert_eq!(31, budget_drips.len()); - - let budget_drips_amount_not_floored: Vec<&Drip> = - budget_drips.iter().filter(|bd| bd.amount != 100).collect(); - assert_eq!(1, budget_drips_amount_not_floored.len()); - - let budget_drips_floored: Vec<&Drip> = - budget_drips.iter().filter(|bd| bd.amount == 100).collect(); - assert_eq!(30, budget_drips_floored.len()); - } - } - - #[test] - fn when_no_zero_budgets_then_no_budget_drips() { - let actualbudget_zero_budgets = []; - let result = transform_budget_drips(&actualbudget_zero_budgets); - assert!(result.is_ok()); - assert_eq!(result.unwrap().len(), 0); - } - - #[test] - fn when_one_zero_budget_with_integer_budget_per_day_then_evenly_divided_budget_drips() { - let actualbudget_zero_budgets = [make_fake_actualbudget_zero_budget_builder("0") - .amount(31 * 100) - .category_id(String::from("0")) - .month(ActualbudgetDate::from_i32(202401)) - .build() - .unwrap()]; - - let result = transform_budget_drips(&actualbudget_zero_budgets); - - assert!(result.is_ok()); - let mut budget_drips = result.unwrap(); - assert_eq!(31, budget_drips.len()); - assert!(budget_drips.iter().all(|bd| bd.amount == 100)); - budget_drips.sort_by_key(|bd| bd.date); - for i in 1..31 { - assert_eq!( - Ok(i), - budget_drips.get(i - 1).unwrap().date.day().try_into() - ); - } - } - - #[test] - fn when_one_zero_budgets_with_non_integer_budget_per_day_then_remainder_on_one_day() { - let actualbudget_zero_budgets = [make_fake_actualbudget_zero_budget_builder("0") - .amount(31 * 100 + 30) - .category_id(String::from("0")) - .month(ActualbudgetDate::from_i32(202401)) - .build() - .unwrap()]; - - let result = transform_budget_drips(&actualbudget_zero_budgets); - - assert!(result.is_ok()); - let budget_drips = result.unwrap(); - assert_eq!(31, budget_drips.len()); - - let budget_drips_amount_not_floored: Vec<&Drip> = - budget_drips.iter().filter(|bd| bd.amount != 100).collect(); - assert_eq!(1, budget_drips_amount_not_floored.len()); - - let budget_drips_floored: Vec<&Drip> = - budget_drips.iter().filter(|bd| bd.amount == 100).collect(); - assert_eq!(30, budget_drips_floored.len()); - } - - #[test] - fn when_multiple_zero_budgets_with_integer_budget_per_day_across_multiple_categories_then_evenly_divided_budget_drips( - ) { - let actualbudget_zero_budgets = [ - make_fake_actualbudget_zero_budget_builder("0") - .amount(31 * 100) - .category_id(String::from("0")) - .month(ActualbudgetDate::from_i32(202401)) - .build() - .unwrap(), - make_fake_actualbudget_zero_budget_builder("1") - .amount(31 * 200) - .category_id(String::from("1")) - .month(ActualbudgetDate::from_i32(202401)) - .build() - .unwrap(), - make_fake_actualbudget_zero_budget_builder("2") - .amount(29 * 110) - .category_id(String::from("0")) - .month(ActualbudgetDate::from_i32(202402)) - .build() - .unwrap(), - make_fake_actualbudget_zero_budget_builder("3") - .amount(29 * 210) - .category_id(String::from("1")) - .month(ActualbudgetDate::from_i32(202402)) - .build() - .unwrap(), - ]; - - let result = transform_budget_drips(&actualbudget_zero_budgets); - let category_0_id = transform_id("0"); - let category_1_id = transform_id("1"); - - assert!(result.is_ok()); - let budget_drips = result.unwrap(); - assert_eq!(31 + 31 + 29 + 29, budget_drips.len()); - - let budget_drips_jan_category_0: Vec<&Drip> = budget_drips - .iter() - .filter(|bd| bd.bucket_id == category_0_id && bd.date.month() == 1) - .collect(); - assert_eq!(31, budget_drips_jan_category_0.len()); - assert!(budget_drips_jan_category_0 - .iter() - .all(|bd| bd.amount == 100)); - - let budget_drips_jan_category_1: Vec<&Drip> = budget_drips - .iter() - .filter(|bd| bd.bucket_id == category_1_id && bd.date.month() == 1) - .collect(); - assert_eq!(31, budget_drips_jan_category_1.len()); - assert!(budget_drips_jan_category_1 - .iter() - .all(|bd| bd.amount == 200)); - - let budget_drips_feb_category_0: Vec<&Drip> = budget_drips - .iter() - .filter(|bd| bd.bucket_id == category_0_id && bd.date.month() == 2) - .collect(); - assert_eq!(29, budget_drips_feb_category_0.len()); - assert!(budget_drips_feb_category_0 - .iter() - .all(|bd| bd.amount == 110)); - - let budget_drips_feb_category_1: Vec<&Drip> = budget_drips - .iter() - .filter(|bd| bd.bucket_id == category_1_id && bd.date.month() == 2) - .collect(); - assert_eq!(29, budget_drips_feb_category_1.len()); - assert!(budget_drips_feb_category_1 - .iter() - .all(|bd| bd.amount == 210)); - } -} 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 { - 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 { + 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_category_transfers.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_category_transfers.rs deleted file mode 100644 index 78b466d..0000000 --- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_category_transfers.rs +++ /dev/null @@ -1,408 +0,0 @@ -use actualbudget_models::{ActualbudgetAccount, ActualbudgetTransaction}; -use anyhow::{bail, Context, Result}; -use itertools::Itertools; -use schist_models::BucketTransfer; - -use super::{is_category_transfer, transform_id}; - -pub fn transform_category_transfers( - actualbudget_transactions: &[ActualbudgetTransaction], - actualbudget_category_transfers_account: &Option, -) -> Result> { - let actualbudget_category_transfers = actualbudget_transactions.iter().filter(|transaction| { - is_category_transfer(transaction, actualbudget_category_transfers_account) - }); - let actualbudget_parent_category_transfers: Vec = - actualbudget_category_transfers - .clone() - .filter(|t| t.is_parent) - .map(ActualbudgetTransaction::clone) - .collect(); - let actualbudget_child_category_transfers: Vec = - actualbudget_category_transfers - .filter(|t| t.is_child) - .map(ActualbudgetTransaction::clone) - .collect(); - - Ok(actualbudget_parent_category_transfers - .iter() - .map(|pct| transform_category_transfer(pct, &actualbudget_child_category_transfers)) - .filter_ok(Option::is_some) - .map_ok(Option::unwrap) - .collect::>>() - .with_context(|| "failed to transform category transfers")?) -} - -fn transform_category_transfer( - actualbudget_parent_category_transfer: &ActualbudgetTransaction, - actualbudget_child_category_transfers: &[ActualbudgetTransaction], -) -> Result> { - transform_category_transfer_without_context( - actualbudget_parent_category_transfer, - actualbudget_child_category_transfers, - ) - .with_context(|| "failed to transform category transfer") -} - -fn transform_category_transfer_without_context( - actualbudget_parent_category_transfer: &ActualbudgetTransaction, - actualbudget_child_category_transfers: &[ActualbudgetTransaction], -) -> Result> { - 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 = actualbudget_child_category_transfers - .clone() - .filter(|t| t.amount < 0) - .collect(); - if from_transfers.len() != 1 { - bail!( - "actualbudget parent category transfer {} had {} from child transfers", - actualbudget_parent_category_transfer.id, - from_transfers.len(), - ); - } - let from_transfer = from_transfers.get(0).unwrap(); - - if from_transfer.category_id.is_none() { - bail!( - "actualbudget child from transfer {} had no category ID", - from_transfer.id - ); - } - let from_category_id: i32 = transform_id(&from_transfer.category_id.clone().unwrap()); - - let to_transfers: Vec = actualbudget_child_category_transfers - .clone() - .filter(|t| t.amount > 0) - .collect(); - if to_transfers.len() != 1 { - bail!( - "actualbudget parent category transfer {} had {} to child transfers", - actualbudget_parent_category_transfer.id, - to_transfers.len(), - ); - } - let to_transfer = to_transfers.get(0).unwrap(); - - if to_transfer.category_id.is_none() { - bail!( - "actualbudget child to transfer {} had no category ID", - to_transfer.id - ); - } - let to_category_id: i32 = transform_id(&to_transfer.category_id.clone().unwrap()); - - if from_transfer.amount != -to_transfer.amount { - bail!("actualbudget category transfer amounts did not sum to zero"); - } - - Ok(Some(BucketTransfer { - id: transform_id(&actualbudget_parent_category_transfer.id), - description: actualbudget_parent_category_transfer - .notes - .clone() - .unwrap_or_else(String::new), - amount: to_transfer.amount.try_into().unwrap(), - from_bucket_id: from_category_id, - to_bucket_id: to_category_id, - })) -} - -#[cfg(test)] -mod test { - use actualbudget_fakes::{ - actualbudget_account::make_fake_actualbudget_account, - 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; - - #[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), - ); - - assert!(result.is_ok()); - let bucket_transfers = result.unwrap(); - assert_eq!(1, bucket_transfers.len()); - let bucket_transfer = bucket_transfers.get(0).unwrap(); - assert_eq!(100, bucket_transfer.amount); - assert_eq!("I was a parent transaction", bucket_transfer.description); - assert_eq!(transform_id("0"), bucket_transfer.from_bucket_id); - assert_eq!(transform_id("1"), bucket_transfer.to_bucket_id); - } - - #[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") - .category_id(Some(String::from("0"))) - .build() - .unwrap(), - make_fake_to_child_transaction_builder("2", 100, "0") - .category_id(Some(String::from("1"))) - .build() - .unwrap(), - make_fake_parent_transaction("3"), - make_fake_from_child_transaction_builder("4", 200, "3") - .category_id(Some(String::from("1"))) - .build() - .unwrap(), - make_fake_to_child_transaction_builder("5", 200, "3") - .category_id(Some(String::from("0"))) - .build() - .unwrap(), - make_fake_actualbudget_transaction_builder("6") - .amount(300) - .build() - .unwrap(), - make_fake_actualbudget_transaction_builder("7") - .amount(-400) - .build() - .unwrap(), - ]; - - let result = transform_category_transfers( - &actualbudget_transactions, - &Some(actualbudget_category_transfers_account), - ); - - assert!(result.is_ok()); - let bucket_transfers = result.unwrap(); - assert_eq!(2, bucket_transfers.len()); - - let bucket_transfer_0: Vec<&BucketTransfer> = bucket_transfers - .iter() - .filter(|ct| ct.amount == 100) - .collect(); - assert_eq!(1, bucket_transfer_0.len()); - let bucket_transfer_0 = bucket_transfer_0.get(0).unwrap(); - assert_eq!(transform_id("0"), bucket_transfer_0.from_bucket_id); - assert_eq!(transform_id("1"), bucket_transfer_0.to_bucket_id); - - let bucket_transfer_1: Vec<&BucketTransfer> = bucket_transfers - .iter() - .filter(|ct| ct.amount == 200) - .collect(); - assert_eq!(1, bucket_transfer_1.len()); - let bucket_transfer_1 = bucket_transfer_1.get(0).unwrap(); - assert_eq!(transform_id("1"), bucket_transfer_1.from_bucket_id); - assert_eq!(transform_id("0"), bucket_transfer_1.to_bucket_id); - } - - #[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") - .build() - .unwrap(), - make_fake_from_child_transaction_builder("2", 50, "0") - .build() - .unwrap(), - make_fake_to_child_transaction_builder("3", 100, "0") - .build() - .unwrap(), - ]; - - let result = transform_category_transfers( - &actualbudget_transactions, - &Some(actualbudget_category_transfers_account), - ); - - 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") - .build() - .unwrap(), - make_fake_to_child_transaction_builder("2", 50, "0") - .build() - .unwrap(), - make_fake_to_child_transaction_builder("3", 50, "0") - .build() - .unwrap(), - ]; - - let result = transform_category_transfers( - &actualbudget_transactions, - &Some(actualbudget_category_transfers_account), - ); - - 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), - ); - - 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), - ); - - 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") - .build() - .unwrap(), - make_fake_to_child_transaction_builder("2", 100, "0") - .build() - .unwrap(), - ]; - - let result = transform_category_transfers( - &actualbudget_transactions, - &Some(actualbudget_category_transfers_account), - ); - - 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") - .category_id(None) - .build() - .unwrap(), - make_fake_to_child_transaction_builder("2", 100, "0") - .build() - .unwrap(), - ]; - let actualbudget_transactions_1 = [ - make_fake_parent_transaction("0"), - make_fake_from_child_transaction_builder("1", 100, "0") - .build() - .unwrap(), - make_fake_to_child_transaction_builder("2", 100, "0") - .category_id(None) - .build() - .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), - ); - - assert!(result_0.is_err()); - assert!(result_1.is_err()); - } - - fn make_fake_parent_transaction(id: &str) -> ActualbudgetTransaction { - make_fake_parent_transaction_builder(id).build().unwrap() - } - - fn make_fake_parent_transaction_builder(id: &str) -> ActualbudgetTransactionBuilder { - let mut builder = make_fake_actualbudget_transaction_builder(id); - builder - .account_id(String::from("0")) - .notes(Some(String::from("I was a parent transaction"))) - .is_parent(true); - builder - } - - fn make_fake_from_child_transaction(id: &str, parent_id: &str) -> ActualbudgetTransaction { - make_fake_from_child_transaction_builder(id, 100, parent_id) - .build() - .unwrap() - } - fn make_fake_from_child_transaction_builder( - id: &str, - amount: i32, - parent_id: &str, - ) -> ActualbudgetTransactionBuilder { - let mut builder = make_fake_actualbudget_transaction_builder(id); - builder - .account_id(String::from("0")) - .category_id(Some(String::from("0"))) - .amount(-amount) - .is_child(true) - .notes(Some(String::from("I was a from child transaction"))) - .parent_id(Some(String::from(parent_id))); - builder - } - - fn make_fake_to_child_transaction(id: &str, parent_id: &str) -> ActualbudgetTransaction { - make_fake_to_child_transaction_builder(id, 100, parent_id) - .build() - .unwrap() - } - - fn make_fake_to_child_transaction_builder( - id: &str, - amount: i32, - parent_id: &str, - ) -> ActualbudgetTransactionBuilder { - let mut builder = make_fake_actualbudget_transaction_builder(id); - builder - .account_id(String::from("0")) - .category_id(Some(String::from("1"))) - .amount(amount) - .is_child(true) - .notes(Some(String::from("I was a to child transaction"))) - .parent_id(Some(String::from(parent_id))); - builder - } -} 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 { @@ -17,25 +17,27 @@ pub fn transform_state(actualbudget_state: ActualbudgetState) -> Result Result { let (remaining_transactions, account_transfers) = transform_account_transfers(&actualbudget_state.actualbudget_transactions)?; + let transactions: Vec = 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 = 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 = transform_categories(&actualbudget_state.actualbudget_categories); - let pipes: Vec = transform_zero_budgets(&actualbudget_state.actualbudget_zero_budgets); - let transactions: Vec = transform_transactions( - &remaining_transactions, - &actualbudget_state.actualbudget_category_transfers_account, - &actualbudget_state.actualbudget_payees, - )?; - let bucket_transfers: Vec = transform_category_transfers( - &remaining_transactions, - &actualbudget_state.actualbudget_category_transfers_account, - )?; + let buckets: Vec = transform_categories_to_buckets( + &actualbudget_state.actualbudget_categories, + &actualbudget_state.actualbudget_category_groups, + &budget_drips, + &transactions, + ); + let pipes: Vec = + transform_zero_budgets_to_pipes(&actualbudget_state.actualbudget_zero_budgets)?; + let bucket_transfers: Vec = + 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 { - 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, actualbudget_payees: &[ActualbudgetPayee], ) -> Result> { - 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, actualbudget_payees: &[ActualbudgetPayee], ) -> Result> { let mut transactions = Vec::::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,36 +88,16 @@ 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") @@ -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_transactions_to_bucket_transfers.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_transactions_to_bucket_transfers.rs new file mode 100644 index 0000000..51545c3 --- /dev/null +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_transactions_to_bucket_transfers.rs @@ -0,0 +1,383 @@ +use actualbudget_models::ActualbudgetTransaction; +use anyhow::{bail, Context, Result}; +use itertools::Itertools; +use schist_models::BucketTransfer; + +use super::transform_id; + +pub fn transform_transactions_to_bucket_transfers( + actualbudget_transactions: &[ActualbudgetTransaction], +) -> Result> { + 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(|parent| { + ( + parent, + children + .clone() + .into_iter() + .filter(|&c| c.parent_id.clone().is_some_and(|id| id == parent.id)) + .collect::>(), + ) + }) + .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(|(parent, children)| transform_category_transfer(parent, children)) + .filter_ok(Option::is_some) + .map_ok(Option::unwrap) + .collect::>>() + .with_context(|| "failed to transform category transfers")?) +} + +fn transform_category_transfer( + actualbudget_parent_category_transfer: &ActualbudgetTransaction, + actualbudget_child_category_transfers: &[&ActualbudgetTransaction], +) -> Result> { + transform_category_transfer_without_context( + actualbudget_parent_category_transfer, + actualbudget_child_category_transfers, + ) + .with_context(|| "failed to transform category transfer") +} + +fn transform_category_transfer_without_context( + actualbudget_parent_category_transfer: &ActualbudgetTransaction, + actualbudget_child_category_transfers: &[&ActualbudgetTransaction], +) -> Result> { + let from_transfers: Vec<&ActualbudgetTransaction> = actualbudget_child_category_transfers + .into_iter() + .cloned() + .filter(|&t| t.amount < 0) + .collect(); + if from_transfers.len() != 1 { + bail!( + "actualbudget parent category transfer {} had {} from child transfers", + actualbudget_parent_category_transfer.id, + from_transfers.len(), + ); + } + let from_transfer = from_transfers.get(0).unwrap(); + + if from_transfer.category_id.is_none() { + bail!( + "actualbudget child from transfer {} had no category ID", + from_transfer.id + ); + } + let from_category_id: i32 = transform_id(&from_transfer.category_id.clone().unwrap()); + + let to_transfers: Vec<&ActualbudgetTransaction> = actualbudget_child_category_transfers + .iter() + .filter(|&t| t.amount > 0) + .cloned() + .collect(); + if to_transfers.len() != 1 { + bail!( + "actualbudget parent category transfer {} had {} to child transfers", + actualbudget_parent_category_transfer.id, + to_transfers.len(), + ); + } + let to_transfer = to_transfers.get(0).unwrap(); + + if to_transfer.category_id.is_none() { + bail!( + "actualbudget child to transfer {} had no category ID", + to_transfer.id + ); + } + let to_category_id: i32 = transform_id(&to_transfer.category_id.clone().unwrap()); + + if from_transfer.amount != -to_transfer.amount { + bail!("actualbudget category transfer amounts did not sum to zero"); + } + + Ok(Some(BucketTransfer { + id: transform_id(&actualbudget_parent_category_transfer.id), + description: actualbudget_parent_category_transfer + .notes + .clone() + .unwrap_or_else(String::new), + amount: to_transfer.amount.try_into().unwrap(), + from_bucket_id: from_category_id, + to_bucket_id: to_category_id, + })) +} + +#[cfg(test)] +mod test { + 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_transactions_to_bucket_transfers; + + #[test] + fn when_one_category_transfer_then_transforms() { + 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_transactions_to_bucket_transfers(&actualbudget_transactions); + + assert!(result.is_ok()); + let bucket_transfers = result.unwrap(); + assert_eq!(1, bucket_transfers.len()); + let bucket_transfer = bucket_transfers.get(0).unwrap(); + assert_eq!(100, bucket_transfer.amount); + assert_eq!("I was a parent transaction", bucket_transfer.description); + assert_eq!(transform_id("0"), bucket_transfer.from_bucket_id); + assert_eq!(transform_id("1"), bucket_transfer.to_bucket_id); + } + + #[test] + fn when_many_transactions_then_transforms() { + let actualbudget_transactions = [ + make_fake_parent_transaction("0"), + make_fake_from_child_transaction_builder("1", 100, "0") + .category_id(Some(String::from("0"))) + .build() + .unwrap(), + make_fake_to_child_transaction_builder("2", 100, "0") + .category_id(Some(String::from("1"))) + .build() + .unwrap(), + make_fake_parent_transaction("3"), + make_fake_from_child_transaction_builder("4", 200, "3") + .category_id(Some(String::from("1"))) + .build() + .unwrap(), + make_fake_to_child_transaction_builder("5", 200, "3") + .category_id(Some(String::from("0"))) + .build() + .unwrap(), + make_fake_actualbudget_transaction_builder("6") + .amount(300) + .build() + .unwrap(), + make_fake_actualbudget_transaction_builder("7") + .amount(-400) + .build() + .unwrap(), + ]; + + let result = transform_transactions_to_bucket_transfers(&actualbudget_transactions); + + assert!(result.is_ok()); + let bucket_transfers = result.unwrap(); + assert_eq!(2, bucket_transfers.len()); + + let bucket_transfer_0: Vec<&BucketTransfer> = bucket_transfers + .iter() + .filter(|ct| ct.amount == 100) + .collect(); + assert_eq!(1, bucket_transfer_0.len()); + let bucket_transfer_0 = bucket_transfer_0.get(0).unwrap(); + assert_eq!(transform_id("0"), bucket_transfer_0.from_bucket_id); + assert_eq!(transform_id("1"), bucket_transfer_0.to_bucket_id); + + let bucket_transfer_1: Vec<&BucketTransfer> = bucket_transfers + .iter() + .filter(|ct| ct.amount == 200) + .collect(); + assert_eq!(1, bucket_transfer_1.len()); + let bucket_transfer_1 = bucket_transfer_1.get(0).unwrap(); + assert_eq!(transform_id("1"), bucket_transfer_1.from_bucket_id); + assert_eq!(transform_id("0"), bucket_transfer_1.to_bucket_id); + } + + #[test] + fn when_multiple_child_from_transactions_then_err() { + let actualbudget_transactions = [ + make_fake_parent_transaction("0"), + make_fake_from_child_transaction_builder("1", 50, "0") + .build() + .unwrap(), + make_fake_from_child_transaction_builder("2", 50, "0") + .build() + .unwrap(), + make_fake_to_child_transaction_builder("3", 100, "0") + .build() + .unwrap(), + ]; + + let result = transform_transactions_to_bucket_transfers(&actualbudget_transactions); + + assert!(result.is_err()); + } + + #[test] + fn when_multiple_child_to_transactions_then_err() { + let actualbudget_transactions = [ + make_fake_parent_transaction("0"), + make_fake_from_child_transaction_builder("1", 100, "0") + .build() + .unwrap(), + make_fake_to_child_transaction_builder("2", 50, "0") + .build() + .unwrap(), + make_fake_to_child_transaction_builder("3", 50, "0") + .build() + .unwrap(), + ]; + + let result = transform_transactions_to_bucket_transfers(&actualbudget_transactions); + + assert!(result.is_err()); + } + + #[test] + fn when_no_child_from_transactions_then_err() { + let actualbudget_transactions = [ + make_fake_parent_transaction("0"), + make_fake_to_child_transaction("1", "0"), + ]; + + let result = transform_transactions_to_bucket_transfers(&actualbudget_transactions); + + assert!(result.is_err()); + } + + #[test] + fn when_no_child_to_transactions_then_err() { + let actualbudget_transactions = [ + make_fake_parent_transaction("0"), + make_fake_from_child_transaction("1", "0"), + ]; + + 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_transactions = [ + make_fake_parent_transaction("0"), + make_fake_from_child_transaction_builder("1", 50, "0") + .build() + .unwrap(), + make_fake_to_child_transaction_builder("2", 100, "0") + .build() + .unwrap(), + ]; + + 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_transactions_0 = [ + make_fake_parent_transaction("0"), + make_fake_from_child_transaction_builder("1", 100, "0") + .category_id(None) + .build() + .unwrap(), + make_fake_to_child_transaction_builder("2", 100, "0") + .build() + .unwrap(), + ]; + let actualbudget_transactions_1 = [ + make_fake_parent_transaction("0"), + make_fake_from_child_transaction_builder("1", 100, "0") + .build() + .unwrap(), + make_fake_to_child_transaction_builder("2", 100, "0") + .category_id(None) + .build() + .unwrap(), + ]; + + 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()); + } + + fn make_fake_parent_transaction(id: &str) -> ActualbudgetTransaction { + make_fake_parent_transaction_builder(id).build().unwrap() + } + + fn make_fake_parent_transaction_builder(id: &str) -> ActualbudgetTransactionBuilder { + let mut builder = make_fake_actualbudget_transaction_builder(id); + builder + .account_id(String::from("0")) + .notes(Some(String::from("I was a parent transaction"))) + .is_parent(true); + builder + } + + fn make_fake_from_child_transaction(id: &str, parent_id: &str) -> ActualbudgetTransaction { + make_fake_from_child_transaction_builder(id, 100, parent_id) + .build() + .unwrap() + } + fn make_fake_from_child_transaction_builder( + id: &str, + amount: i32, + parent_id: &str, + ) -> ActualbudgetTransactionBuilder { + let mut builder = make_fake_actualbudget_transaction_builder(id); + builder + .account_id(String::from("0")) + .category_id(Some(String::from("0"))) + .amount(-amount) + .is_child(true) + .notes(Some(String::from("I was a from child transaction"))) + .parent_id(Some(String::from(parent_id))); + builder + } + + fn make_fake_to_child_transaction(id: &str, parent_id: &str) -> ActualbudgetTransaction { + make_fake_to_child_transaction_builder(id, 100, parent_id) + .build() + .unwrap() + } + + fn make_fake_to_child_transaction_builder( + id: &str, + amount: i32, + parent_id: &str, + ) -> ActualbudgetTransactionBuilder { + let mut builder = make_fake_actualbudget_transaction_builder(id); + builder + .account_id(String::from("0")) + .category_id(Some(String::from("1"))) + .amount(amount) + .is_child(true) + .notes(Some(String::from("I was a to child transaction"))) + .parent_id(Some(String::from(parent_id))); + builder + } +} 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 { - todo!() -} diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets_to_drips.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets_to_drips.rs new file mode 100644 index 0000000..bfaef31 --- /dev/null +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets_to_drips.rs @@ -0,0 +1,325 @@ +use std::hash::{DefaultHasher, Hash, Hasher}; + +use actualbudget_models::ActualbudgetZeroBudget; +use anyhow::{Context, Result}; +use itertools::Itertools; +use schist_models::{DateUtc, Drip}; +use schist_traits::Dateable; + +use super::{transform_date::transform_date, transform_id::transform_id}; + +pub fn transform_budget_drips( + actualbudget_zero_budgets: &[ActualbudgetZeroBudget], +) -> Result> { + let non_zero_actualbudget_zero_budgets: Vec = actualbudget_zero_budgets + .iter() + .filter(|zb| zb.amount != 0) + .cloned() + .collect(); + + let budget_drips = + group_actualbudget_zero_budgets_by_category_id(&non_zero_actualbudget_zero_budgets) + .iter() + .map(sort_actualbudget_zero_budgets_by_month) + .flat_map(|sorted_actualbudget_zero_budgets| { + sorted_actualbudget_zero_budgets + .iter() + .map(map_actualbudget_zero_budget_to_budget_drips) + .collect::>>>() + }) + .flatten() + .flatten() + .filter(|bd| bd.amount != 0) + .collect(); + + Ok(budget_drips) +} + +fn map_actualbudget_zero_budget_to_budget_drips( + actualbudget_zero_budget: &ActualbudgetZeroBudget, +) -> Result> { + let context = || "failed to map actualbudget zero budgets to budget drips"; + + let first_day_of_month: DateUtc = Into::::into( + transform_date(&actualbudget_zero_budget.month).with_context(context)?, + ); + let month = first_day_of_month.month(); + let days_in_month: Vec = (0..31) + .map(|i| first_day_of_month.add_days(i)) + .take_while(|d| match d { + Err(_) => true, + Ok(d) => d.month() == month, + }) + .filter(Result::is_ok) + .map(Result::unwrap) + .collect(); + + // unwrap() can panic, but days_in_month is guaranteed to have between 0 and + // 31 elements, so we should be grand + let num_days_in_month: i32 = days_in_month.len().try_into().unwrap(); + + let amount = actualbudget_zero_budget.amount / num_days_in_month; + let remainder_amount = actualbudget_zero_budget.amount - (amount * num_days_in_month); + + let budget_drips: Vec = days_in_month + .iter() + .map(|&date| Drip { + id: transform_budget_drip_id(&actualbudget_zero_budget, &date), + bucket_id: transform_id(&actualbudget_zero_budget.category_id), + date, + amount: if date == first_day_of_month { + amount + remainder_amount + } else { + amount + }, + inserted_at_unix_seconds: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("The current time should be after the Unix epoch") + .as_secs() + .try_into() + .expect("The current time's timestamp in seconds should fit into a 32-bit signed integer"), + pipe_id: 0, + }) + .collect(); + + Ok(budget_drips) +} + +fn sort_actualbudget_zero_budgets_by_month( + actualbudget_zero_budgets_by_category_id: &Vec, +) -> Vec { + let mut actualbudget_zero_budgets = actualbudget_zero_budgets_by_category_id.clone(); + actualbudget_zero_budgets.sort_by_key(|zb| zb.month.clone()); + actualbudget_zero_budgets +} + +fn group_actualbudget_zero_budgets_by_category_id( + actualbudget_zero_budgets: &[ActualbudgetZeroBudget], +) -> Vec> { + let category_ids = actualbudget_zero_budgets + .iter() + .map(|zb| zb.category_id.clone()) + .unique(); + + category_ids + .map(|category_id| { + actualbudget_zero_budgets + .iter() + .filter(|zb| zb.category_id == category_id) + .map(ActualbudgetZeroBudget::clone) + .collect() + }) + .collect() +} + +fn transform_budget_drip_id( + actualbudget_zero_budget: &ActualbudgetZeroBudget, + date: &DateUtc, +) -> i32 { + let mut hasher = DefaultHasher::new(); + let unique_str = actualbudget_zero_budget.id.clone() + date.to_string().as_str(); + unique_str.hash(&mut hasher); + (hasher.finish() % std::convert::TryInto::::try_into(i32::MAX).unwrap()) + .try_into() + .unwrap() +} + +#[cfg(test)] +mod test { + use actualbudget_fakes::actualbudget_zero_budget::make_fake_actualbudget_zero_budget_builder; + use actualbudget_models::ActualbudgetDate; + use schist_models::Drip; + use schist_traits::Dateable; + + use crate::transform_state::transform_id::transform_id; + + use super::transform_budget_drips; + + mod map_actualbudget_zero_budget_to_budget_drips { + use actualbudget_fakes::actualbudget_zero_budget::make_fake_actualbudget_zero_budget_builder; + use actualbudget_models::ActualbudgetDate; + use schist_models::Drip; + use schist_traits::Dateable; + + 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() { + let actualbudget_zero_budget = make_fake_actualbudget_zero_budget_builder("0") + .amount(31 * 100) + .category_id(String::from("0")) + .month(ActualbudgetDate::from_i32(202401)) + .build() + .unwrap(); + + let result = map_actualbudget_zero_budget_to_budget_drips(&actualbudget_zero_budget); + + assert!(result.is_ok()); + let mut budget_drips = result.unwrap(); + assert_eq!(31, budget_drips.len()); + assert!(budget_drips.iter().all(|bd| bd.amount == 100)); + budget_drips.sort_by_key(|bd| bd.date); + for i in 1..31 { + assert_eq!( + Ok(i), + budget_drips.get(i - 1).unwrap().date.day().try_into() + ); + } + } + + #[test] + fn when_zero_budget_with_non_integer_budget_per_day_then_remainder_on_one_day() { + let actualbudget_zero_budget = make_fake_actualbudget_zero_budget_builder("0") + .amount(31 * 100 + 30) + .category_id(String::from("0")) + .month(ActualbudgetDate::from_i32(202401)) + .build() + .unwrap(); + + let result = map_actualbudget_zero_budget_to_budget_drips(&actualbudget_zero_budget); + + assert!(result.is_ok()); + let budget_drips = result.unwrap(); + assert_eq!(31, budget_drips.len()); + + let budget_drips_amount_not_floored: Vec<&Drip> = + budget_drips.iter().filter(|bd| bd.amount != 100).collect(); + assert_eq!(1, budget_drips_amount_not_floored.len()); + + let budget_drips_floored: Vec<&Drip> = + budget_drips.iter().filter(|bd| bd.amount == 100).collect(); + assert_eq!(30, budget_drips_floored.len()); + } + } + + #[test] + fn when_no_zero_budgets_then_no_budget_drips() { + let actualbudget_zero_budgets = []; + let result = transform_budget_drips(&actualbudget_zero_budgets); + assert!(result.is_ok()); + assert_eq!(result.unwrap().len(), 0); + } + + #[test] + fn when_one_zero_budget_with_integer_budget_per_day_then_evenly_divided_budget_drips() { + let actualbudget_zero_budgets = [make_fake_actualbudget_zero_budget_builder("0") + .amount(31 * 100) + .category_id(String::from("0")) + .month(ActualbudgetDate::from_i32(202401)) + .build() + .unwrap()]; + + let result = transform_budget_drips(&actualbudget_zero_budgets); + + assert!(result.is_ok()); + let mut budget_drips = result.unwrap(); + assert_eq!(31, budget_drips.len()); + assert!(budget_drips.iter().all(|bd| bd.amount == 100)); + budget_drips.sort_by_key(|bd| bd.date); + for i in 1..31 { + assert_eq!( + Ok(i), + budget_drips.get(i - 1).unwrap().date.day().try_into() + ); + } + } + + #[test] + fn when_one_zero_budgets_with_non_integer_budget_per_day_then_remainder_on_one_day() { + let actualbudget_zero_budgets = [make_fake_actualbudget_zero_budget_builder("0") + .amount(31 * 100 + 30) + .category_id(String::from("0")) + .month(ActualbudgetDate::from_i32(202401)) + .build() + .unwrap()]; + + let result = transform_budget_drips(&actualbudget_zero_budgets); + + assert!(result.is_ok()); + let budget_drips = result.unwrap(); + assert_eq!(31, budget_drips.len()); + + let budget_drips_amount_not_floored: Vec<&Drip> = + budget_drips.iter().filter(|bd| bd.amount != 100).collect(); + assert_eq!(1, budget_drips_amount_not_floored.len()); + + let budget_drips_floored: Vec<&Drip> = + budget_drips.iter().filter(|bd| bd.amount == 100).collect(); + assert_eq!(30, budget_drips_floored.len()); + } + + #[test] + fn when_multiple_zero_budgets_with_integer_budget_per_day_across_multiple_categories_then_evenly_divided_budget_drips( + ) { + let actualbudget_zero_budgets = [ + make_fake_actualbudget_zero_budget_builder("0") + .amount(31 * 100) + .category_id(String::from("0")) + .month(ActualbudgetDate::from_i32(202401)) + .build() + .unwrap(), + make_fake_actualbudget_zero_budget_builder("1") + .amount(31 * 200) + .category_id(String::from("1")) + .month(ActualbudgetDate::from_i32(202401)) + .build() + .unwrap(), + make_fake_actualbudget_zero_budget_builder("2") + .amount(29 * 110) + .category_id(String::from("0")) + .month(ActualbudgetDate::from_i32(202402)) + .build() + .unwrap(), + make_fake_actualbudget_zero_budget_builder("3") + .amount(29 * 210) + .category_id(String::from("1")) + .month(ActualbudgetDate::from_i32(202402)) + .build() + .unwrap(), + ]; + + let result = transform_budget_drips(&actualbudget_zero_budgets); + let category_0_id = transform_id("0"); + let category_1_id = transform_id("1"); + + assert!(result.is_ok()); + let budget_drips = result.unwrap(); + assert_eq!(31 + 31 + 29 + 29, budget_drips.len()); + + let budget_drips_jan_category_0: Vec<&Drip> = budget_drips + .iter() + .filter(|bd| bd.bucket_id == category_0_id && bd.date.month() == 1) + .collect(); + assert_eq!(31, budget_drips_jan_category_0.len()); + assert!(budget_drips_jan_category_0 + .iter() + .all(|bd| bd.amount == 100)); + + let budget_drips_jan_category_1: Vec<&Drip> = budget_drips + .iter() + .filter(|bd| bd.bucket_id == category_1_id && bd.date.month() == 1) + .collect(); + assert_eq!(31, budget_drips_jan_category_1.len()); + assert!(budget_drips_jan_category_1 + .iter() + .all(|bd| bd.amount == 200)); + + let budget_drips_feb_category_0: Vec<&Drip> = budget_drips + .iter() + .filter(|bd| bd.bucket_id == category_0_id && bd.date.month() == 2) + .collect(); + assert_eq!(29, budget_drips_feb_category_0.len()); + assert!(budget_drips_feb_category_0 + .iter() + .all(|bd| bd.amount == 110)); + + let budget_drips_feb_category_1: Vec<&Drip> = budget_drips + .iter() + .filter(|bd| bd.bucket_id == category_1_id && bd.date.month() == 2) + .collect(); + assert_eq!(29, budget_drips_feb_category_1.len()); + assert!(budget_drips_feb_category_1 + .iter() + .all(|bd| bd.amount == 210)); + } +} 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> { + let pipes_with_consecutive_duplicates = actualbudget_zero_budgets + .iter() + .filter(|zb| zb.amount != 0) + .map(transform_zero_budget_to_pipe) + .collect::>>() + .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 { + 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) -> Vec { + 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 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 bool>> { - BUCKET_NAMES + BUCKET_NAMES_AND_GROUP_NAMES .iter() - .map(|bucket_name| -> Box bool> { - Box::new(|c| matches_bucket(c, bucket_name)) - }) + .map( + |bucket_name_and_group_name| -> Box 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/budget_drip_matchers.rs deleted file mode 100644 index 85ad045..0000000 --- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/budget_drip_matchers.rs +++ /dev/null @@ -1,211 +0,0 @@ -use std::sync::LazyLock; - -use schist_models::{Bucket, DateUtc, Drip}; -use schist_traits::Dateable; - -use crate::common::bucket_names::{ - BUCKET_NAME_BILLS, BUCKET_NAME_BILLS_FLEXIBLE, BUCKET_NAME_FOOD, BUCKET_NAME_GENERAL, - BUCKET_NAME_SAVINGS, -}; - -pub fn get_num_expected_budget_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]) { - assert!( - DATES - .iter() - .any(|date| *LazyLock::force(date) == budget_drip.date), - "budget drip had unexpected date: {:?}", - budget_drip, - ); - - let bucket_name = buckets - .iter() - .find(|c| c.id == budget_drip.bucket_id) - .map(|c| c.name.clone()) - .expect( - format!( - "budget drip {:?} had unexpected bucket ID. buckets were: {:?}", - budget_drip, buckets, - ) - .as_str(), - ); - - let expected_quantity = match ( - bucket_name.as_str(), - budget_drip.date.month(), - budget_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_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_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_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) - } - - (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), - - (_, _, _) => panic!( - "budget drip {:?} had unexpected bucket name {}", - budget_drip, bucket_name, - ), - }; - - assert_eq!( - expected_quantity, budget_drip.amount, - "budget drip {:?} had unexpected quantity", - budget_drip, - ); -} - -static DATES: [LazyLock; 122] = [ - LazyLock::::new(|| "2024-08-01".parse().unwrap()), - LazyLock::::new(|| "2024-08-02".parse().unwrap()), - LazyLock::::new(|| "2024-08-03".parse().unwrap()), - LazyLock::::new(|| "2024-08-04".parse().unwrap()), - LazyLock::::new(|| "2024-08-05".parse().unwrap()), - LazyLock::::new(|| "2024-08-06".parse().unwrap()), - LazyLock::::new(|| "2024-08-07".parse().unwrap()), - LazyLock::::new(|| "2024-08-08".parse().unwrap()), - LazyLock::::new(|| "2024-08-09".parse().unwrap()), - LazyLock::::new(|| "2024-08-10".parse().unwrap()), - LazyLock::::new(|| "2024-08-11".parse().unwrap()), - LazyLock::::new(|| "2024-08-12".parse().unwrap()), - LazyLock::::new(|| "2024-08-13".parse().unwrap()), - LazyLock::::new(|| "2024-08-14".parse().unwrap()), - LazyLock::::new(|| "2024-08-15".parse().unwrap()), - LazyLock::::new(|| "2024-08-16".parse().unwrap()), - LazyLock::::new(|| "2024-08-17".parse().unwrap()), - LazyLock::::new(|| "2024-08-18".parse().unwrap()), - LazyLock::::new(|| "2024-08-19".parse().unwrap()), - LazyLock::::new(|| "2024-08-20".parse().unwrap()), - LazyLock::::new(|| "2024-08-21".parse().unwrap()), - LazyLock::::new(|| "2024-08-22".parse().unwrap()), - LazyLock::::new(|| "2024-08-23".parse().unwrap()), - LazyLock::::new(|| "2024-08-24".parse().unwrap()), - LazyLock::::new(|| "2024-08-25".parse().unwrap()), - LazyLock::::new(|| "2024-08-26".parse().unwrap()), - LazyLock::::new(|| "2024-08-27".parse().unwrap()), - LazyLock::::new(|| "2024-08-28".parse().unwrap()), - LazyLock::::new(|| "2024-08-29".parse().unwrap()), - LazyLock::::new(|| "2024-08-30".parse().unwrap()), - LazyLock::::new(|| "2024-08-31".parse().unwrap()), - LazyLock::::new(|| "2024-09-01".parse().unwrap()), - LazyLock::::new(|| "2024-09-02".parse().unwrap()), - LazyLock::::new(|| "2024-09-03".parse().unwrap()), - LazyLock::::new(|| "2024-09-04".parse().unwrap()), - LazyLock::::new(|| "2024-09-05".parse().unwrap()), - LazyLock::::new(|| "2024-09-06".parse().unwrap()), - LazyLock::::new(|| "2024-09-07".parse().unwrap()), - LazyLock::::new(|| "2024-09-08".parse().unwrap()), - LazyLock::::new(|| "2024-09-09".parse().unwrap()), - LazyLock::::new(|| "2024-09-10".parse().unwrap()), - LazyLock::::new(|| "2024-09-11".parse().unwrap()), - LazyLock::::new(|| "2024-09-12".parse().unwrap()), - LazyLock::::new(|| "2024-09-13".parse().unwrap()), - LazyLock::::new(|| "2024-09-14".parse().unwrap()), - LazyLock::::new(|| "2024-09-15".parse().unwrap()), - LazyLock::::new(|| "2024-09-16".parse().unwrap()), - LazyLock::::new(|| "2024-09-17".parse().unwrap()), - LazyLock::::new(|| "2024-09-18".parse().unwrap()), - LazyLock::::new(|| "2024-09-19".parse().unwrap()), - LazyLock::::new(|| "2024-09-20".parse().unwrap()), - LazyLock::::new(|| "2024-09-21".parse().unwrap()), - LazyLock::::new(|| "2024-09-22".parse().unwrap()), - LazyLock::::new(|| "2024-09-23".parse().unwrap()), - LazyLock::::new(|| "2024-09-24".parse().unwrap()), - LazyLock::::new(|| "2024-09-25".parse().unwrap()), - LazyLock::::new(|| "2024-09-26".parse().unwrap()), - LazyLock::::new(|| "2024-09-27".parse().unwrap()), - LazyLock::::new(|| "2024-09-28".parse().unwrap()), - LazyLock::::new(|| "2024-09-29".parse().unwrap()), - LazyLock::::new(|| "2024-09-30".parse().unwrap()), - LazyLock::::new(|| "2024-10-01".parse().unwrap()), - LazyLock::::new(|| "2024-10-02".parse().unwrap()), - LazyLock::::new(|| "2024-10-03".parse().unwrap()), - LazyLock::::new(|| "2024-10-04".parse().unwrap()), - LazyLock::::new(|| "2024-10-05".parse().unwrap()), - LazyLock::::new(|| "2024-10-06".parse().unwrap()), - LazyLock::::new(|| "2024-10-07".parse().unwrap()), - LazyLock::::new(|| "2024-10-08".parse().unwrap()), - LazyLock::::new(|| "2024-10-09".parse().unwrap()), - LazyLock::::new(|| "2024-10-10".parse().unwrap()), - LazyLock::::new(|| "2024-10-11".parse().unwrap()), - LazyLock::::new(|| "2024-10-12".parse().unwrap()), - LazyLock::::new(|| "2024-10-13".parse().unwrap()), - LazyLock::::new(|| "2024-10-14".parse().unwrap()), - LazyLock::::new(|| "2024-10-15".parse().unwrap()), - LazyLock::::new(|| "2024-10-16".parse().unwrap()), - LazyLock::::new(|| "2024-10-17".parse().unwrap()), - LazyLock::::new(|| "2024-10-18".parse().unwrap()), - LazyLock::::new(|| "2024-10-19".parse().unwrap()), - LazyLock::::new(|| "2024-10-20".parse().unwrap()), - LazyLock::::new(|| "2024-10-21".parse().unwrap()), - LazyLock::::new(|| "2024-10-22".parse().unwrap()), - LazyLock::::new(|| "2024-10-23".parse().unwrap()), - LazyLock::::new(|| "2024-10-24".parse().unwrap()), - LazyLock::::new(|| "2024-10-25".parse().unwrap()), - LazyLock::::new(|| "2024-10-26".parse().unwrap()), - LazyLock::::new(|| "2024-10-27".parse().unwrap()), - LazyLock::::new(|| "2024-10-28".parse().unwrap()), - LazyLock::::new(|| "2024-10-29".parse().unwrap()), - LazyLock::::new(|| "2024-10-30".parse().unwrap()), - LazyLock::::new(|| "2024-10-31".parse().unwrap()), - LazyLock::::new(|| "2024-11-01".parse().unwrap()), - LazyLock::::new(|| "2024-11-02".parse().unwrap()), - LazyLock::::new(|| "2024-11-03".parse().unwrap()), - LazyLock::::new(|| "2024-11-04".parse().unwrap()), - LazyLock::::new(|| "2024-11-05".parse().unwrap()), - LazyLock::::new(|| "2024-11-06".parse().unwrap()), - LazyLock::::new(|| "2024-11-07".parse().unwrap()), - LazyLock::::new(|| "2024-11-08".parse().unwrap()), - LazyLock::::new(|| "2024-11-09".parse().unwrap()), - LazyLock::::new(|| "2024-11-10".parse().unwrap()), - LazyLock::::new(|| "2024-11-11".parse().unwrap()), - LazyLock::::new(|| "2024-11-12".parse().unwrap()), - LazyLock::::new(|| "2024-11-13".parse().unwrap()), - LazyLock::::new(|| "2024-11-14".parse().unwrap()), - LazyLock::::new(|| "2024-11-15".parse().unwrap()), - LazyLock::::new(|| "2024-11-16".parse().unwrap()), - LazyLock::::new(|| "2024-11-17".parse().unwrap()), - LazyLock::::new(|| "2024-11-18".parse().unwrap()), - LazyLock::::new(|| "2024-11-19".parse().unwrap()), - LazyLock::::new(|| "2024-11-20".parse().unwrap()), - LazyLock::::new(|| "2024-11-21".parse().unwrap()), - LazyLock::::new(|| "2024-11-22".parse().unwrap()), - LazyLock::::new(|| "2024-11-23".parse().unwrap()), - LazyLock::::new(|| "2024-11-24".parse().unwrap()), - LazyLock::::new(|| "2024-11-25".parse().unwrap()), - LazyLock::::new(|| "2024-11-26".parse().unwrap()), - LazyLock::::new(|| "2024-11-27".parse().unwrap()), - LazyLock::::new(|| "2024-11-28".parse().unwrap()), - LazyLock::::new(|| "2024-11-29".parse().unwrap()), - LazyLock::::new(|| "2024-11-30".parse().unwrap()), -]; diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/drip_matchers.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/drip_matchers.rs new file mode 100644 index 0000000..629a04b --- /dev/null +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/drip_matchers.rs @@ -0,0 +1,202 @@ +use std::sync::LazyLock; + +use schist_models::{Bucket, DateUtc, Drip}; +use schist_traits::Dateable; + +use crate::common::bucket_names::{ + BUCKET_NAME_BILLS, BUCKET_NAME_BILLS_FLEXIBLE, BUCKET_NAME_FOOD, BUCKET_NAME_GENERAL, + BUCKET_NAME_SAVINGS, +}; + +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_drip_matches(drip: &Drip, buckets: &[Bucket]) { + assert!( + DATES.iter().any(|date| *LazyLock::force(date) == drip.date), + "drip had unexpected date: {:?}", + drip, + ); + + let bucket_name = buckets + .iter() + .find(|c| c.id == drip.bucket_id) + .map(|c| c.name.clone()) + .expect( + format!( + "drip {:?} had unexpected bucket ID. buckets were: {:?}", + drip, buckets, + ) + .as_str(), + ); + + 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!("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!("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!("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!("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!("drip {:?} had unexpected date", drip), + + (_, _, _) => panic!("drip {:?} had unexpected bucket name {}", drip, bucket_name,), + }; + + assert_eq!( + expected_quantity, drip.amount, + "drip {:?} had unexpected quantity", + drip, + ); +} + +static DATES: [LazyLock; 122] = [ + LazyLock::::new(|| "2024-08-01".parse().unwrap()), + LazyLock::::new(|| "2024-08-02".parse().unwrap()), + LazyLock::::new(|| "2024-08-03".parse().unwrap()), + LazyLock::::new(|| "2024-08-04".parse().unwrap()), + LazyLock::::new(|| "2024-08-05".parse().unwrap()), + LazyLock::::new(|| "2024-08-06".parse().unwrap()), + LazyLock::::new(|| "2024-08-07".parse().unwrap()), + LazyLock::::new(|| "2024-08-08".parse().unwrap()), + LazyLock::::new(|| "2024-08-09".parse().unwrap()), + LazyLock::::new(|| "2024-08-10".parse().unwrap()), + LazyLock::::new(|| "2024-08-11".parse().unwrap()), + LazyLock::::new(|| "2024-08-12".parse().unwrap()), + LazyLock::::new(|| "2024-08-13".parse().unwrap()), + LazyLock::::new(|| "2024-08-14".parse().unwrap()), + LazyLock::::new(|| "2024-08-15".parse().unwrap()), + LazyLock::::new(|| "2024-08-16".parse().unwrap()), + LazyLock::::new(|| "2024-08-17".parse().unwrap()), + LazyLock::::new(|| "2024-08-18".parse().unwrap()), + LazyLock::::new(|| "2024-08-19".parse().unwrap()), + LazyLock::::new(|| "2024-08-20".parse().unwrap()), + LazyLock::::new(|| "2024-08-21".parse().unwrap()), + LazyLock::::new(|| "2024-08-22".parse().unwrap()), + LazyLock::::new(|| "2024-08-23".parse().unwrap()), + LazyLock::::new(|| "2024-08-24".parse().unwrap()), + LazyLock::::new(|| "2024-08-25".parse().unwrap()), + LazyLock::::new(|| "2024-08-26".parse().unwrap()), + LazyLock::::new(|| "2024-08-27".parse().unwrap()), + LazyLock::::new(|| "2024-08-28".parse().unwrap()), + LazyLock::::new(|| "2024-08-29".parse().unwrap()), + LazyLock::::new(|| "2024-08-30".parse().unwrap()), + LazyLock::::new(|| "2024-08-31".parse().unwrap()), + LazyLock::::new(|| "2024-09-01".parse().unwrap()), + LazyLock::::new(|| "2024-09-02".parse().unwrap()), + LazyLock::::new(|| "2024-09-03".parse().unwrap()), + LazyLock::::new(|| "2024-09-04".parse().unwrap()), + LazyLock::::new(|| "2024-09-05".parse().unwrap()), + LazyLock::::new(|| "2024-09-06".parse().unwrap()), + LazyLock::::new(|| "2024-09-07".parse().unwrap()), + LazyLock::::new(|| "2024-09-08".parse().unwrap()), + LazyLock::::new(|| "2024-09-09".parse().unwrap()), + LazyLock::::new(|| "2024-09-10".parse().unwrap()), + LazyLock::::new(|| "2024-09-11".parse().unwrap()), + LazyLock::::new(|| "2024-09-12".parse().unwrap()), + LazyLock::::new(|| "2024-09-13".parse().unwrap()), + LazyLock::::new(|| "2024-09-14".parse().unwrap()), + LazyLock::::new(|| "2024-09-15".parse().unwrap()), + LazyLock::::new(|| "2024-09-16".parse().unwrap()), + LazyLock::::new(|| "2024-09-17".parse().unwrap()), + LazyLock::::new(|| "2024-09-18".parse().unwrap()), + LazyLock::::new(|| "2024-09-19".parse().unwrap()), + LazyLock::::new(|| "2024-09-20".parse().unwrap()), + LazyLock::::new(|| "2024-09-21".parse().unwrap()), + LazyLock::::new(|| "2024-09-22".parse().unwrap()), + LazyLock::::new(|| "2024-09-23".parse().unwrap()), + LazyLock::::new(|| "2024-09-24".parse().unwrap()), + LazyLock::::new(|| "2024-09-25".parse().unwrap()), + LazyLock::::new(|| "2024-09-26".parse().unwrap()), + LazyLock::::new(|| "2024-09-27".parse().unwrap()), + LazyLock::::new(|| "2024-09-28".parse().unwrap()), + LazyLock::::new(|| "2024-09-29".parse().unwrap()), + LazyLock::::new(|| "2024-09-30".parse().unwrap()), + LazyLock::::new(|| "2024-10-01".parse().unwrap()), + LazyLock::::new(|| "2024-10-02".parse().unwrap()), + LazyLock::::new(|| "2024-10-03".parse().unwrap()), + LazyLock::::new(|| "2024-10-04".parse().unwrap()), + LazyLock::::new(|| "2024-10-05".parse().unwrap()), + LazyLock::::new(|| "2024-10-06".parse().unwrap()), + LazyLock::::new(|| "2024-10-07".parse().unwrap()), + LazyLock::::new(|| "2024-10-08".parse().unwrap()), + LazyLock::::new(|| "2024-10-09".parse().unwrap()), + LazyLock::::new(|| "2024-10-10".parse().unwrap()), + LazyLock::::new(|| "2024-10-11".parse().unwrap()), + LazyLock::::new(|| "2024-10-12".parse().unwrap()), + LazyLock::::new(|| "2024-10-13".parse().unwrap()), + LazyLock::::new(|| "2024-10-14".parse().unwrap()), + LazyLock::::new(|| "2024-10-15".parse().unwrap()), + LazyLock::::new(|| "2024-10-16".parse().unwrap()), + LazyLock::::new(|| "2024-10-17".parse().unwrap()), + LazyLock::::new(|| "2024-10-18".parse().unwrap()), + LazyLock::::new(|| "2024-10-19".parse().unwrap()), + LazyLock::::new(|| "2024-10-20".parse().unwrap()), + LazyLock::::new(|| "2024-10-21".parse().unwrap()), + LazyLock::::new(|| "2024-10-22".parse().unwrap()), + LazyLock::::new(|| "2024-10-23".parse().unwrap()), + LazyLock::::new(|| "2024-10-24".parse().unwrap()), + LazyLock::::new(|| "2024-10-25".parse().unwrap()), + LazyLock::::new(|| "2024-10-26".parse().unwrap()), + LazyLock::::new(|| "2024-10-27".parse().unwrap()), + LazyLock::::new(|| "2024-10-28".parse().unwrap()), + LazyLock::::new(|| "2024-10-29".parse().unwrap()), + LazyLock::::new(|| "2024-10-30".parse().unwrap()), + LazyLock::::new(|| "2024-10-31".parse().unwrap()), + LazyLock::::new(|| "2024-11-01".parse().unwrap()), + LazyLock::::new(|| "2024-11-02".parse().unwrap()), + LazyLock::::new(|| "2024-11-03".parse().unwrap()), + LazyLock::::new(|| "2024-11-04".parse().unwrap()), + LazyLock::::new(|| "2024-11-05".parse().unwrap()), + LazyLock::::new(|| "2024-11-06".parse().unwrap()), + LazyLock::::new(|| "2024-11-07".parse().unwrap()), + LazyLock::::new(|| "2024-11-08".parse().unwrap()), + LazyLock::::new(|| "2024-11-09".parse().unwrap()), + LazyLock::::new(|| "2024-11-10".parse().unwrap()), + LazyLock::::new(|| "2024-11-11".parse().unwrap()), + LazyLock::::new(|| "2024-11-12".parse().unwrap()), + LazyLock::::new(|| "2024-11-13".parse().unwrap()), + LazyLock::::new(|| "2024-11-14".parse().unwrap()), + LazyLock::::new(|| "2024-11-15".parse().unwrap()), + LazyLock::::new(|| "2024-11-16".parse().unwrap()), + LazyLock::::new(|| "2024-11-17".parse().unwrap()), + LazyLock::::new(|| "2024-11-18".parse().unwrap()), + LazyLock::::new(|| "2024-11-19".parse().unwrap()), + LazyLock::::new(|| "2024-11-20".parse().unwrap()), + LazyLock::::new(|| "2024-11-21".parse().unwrap()), + LazyLock::::new(|| "2024-11-22".parse().unwrap()), + LazyLock::::new(|| "2024-11-23".parse().unwrap()), + LazyLock::::new(|| "2024-11-24".parse().unwrap()), + LazyLock::::new(|| "2024-11-25".parse().unwrap()), + LazyLock::::new(|| "2024-11-26".parse().unwrap()), + LazyLock::::new(|| "2024-11-27".parse().unwrap()), + LazyLock::::new(|| "2024-11-28".parse().unwrap()), + LazyLock::::new(|| "2024-11-29".parse().unwrap()), + LazyLock::::new(|| "2024-11-30".parse().unwrap()), +]; 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 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 bool>> { +pub fn get_transaction_matchers() -> Vec 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 { - 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 index f8d3f5d..5f89924 100644 Binary files a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/resources/actualbudget_test_db.sqlite and b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/resources/actualbudget_test_db.sqlite differ 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::::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 + ); + } } -- cgit v1.2.3