diff options
| author | Joe Carstairs <jcarstairs@scottlogic.com> | 2025-07-28 13:22:53 +0100 |
|---|---|---|
| committer | Joe Carstairs <jcarstairs@scottlogic.com> | 2025-07-28 13:30:27 +0100 |
| commit | 2361adeb0ca77904138e3466d51c2f44cf6df613 (patch) | |
| tree | f6e7192908f99fab62061913d8a1e9d2b918414b /actualbudget_to_schist_transformer/src/transform_state | |
| parent | c7ed1e61ce2218be0b9e60577454124c4e43a366 (diff) | |
Tidier folders
Diffstat (limited to 'actualbudget_to_schist_transformer/src/transform_state')
12 files changed, 0 insertions, 1622 deletions
diff --git a/actualbudget_to_schist_transformer/src/transform_state/is_account_transfer.rs b/actualbudget_to_schist_transformer/src/transform_state/is_account_transfer.rs deleted file mode 100644 index c9540ed..0000000 --- a/actualbudget_to_schist_transformer/src/transform_state/is_account_transfer.rs +++ /dev/null @@ -1,13 +0,0 @@ -use actualbudget_models::ActualbudgetTransaction; - -pub fn is_account_transfer( - transaction: &ActualbudgetTransaction, -) -> bool { - transaction.transfer_id.is_some() -} - -pub fn is_not_account_transfer( - transaction: &ActualbudgetTransaction, -) -> bool { - transaction.transfer_id.is_none() -} diff --git a/actualbudget_to_schist_transformer/src/transform_state/is_category_transfer.rs b/actualbudget_to_schist_transformer/src/transform_state/is_category_transfer.rs deleted file mode 100644 index c82387c..0000000 --- a/actualbudget_to_schist_transformer/src/transform_state/is_category_transfer.rs +++ /dev/null @@ -1,18 +0,0 @@ -use actualbudget_models::{ActualbudgetAccount, ActualbudgetTransaction}; - -pub fn is_category_transfer( - transaction: &ActualbudgetTransaction, - actualbudget_category_transfers_account: &Option<ActualbudgetAccount> -) -> bool { - match actualbudget_category_transfers_account { - None => false, - Some(acc) => transaction.account_id.clone() == acc.id.clone(), - } -} - -pub fn is_not_category_transfer( - transaction: &ActualbudgetTransaction, - actualbudget_category_transfers_account: &Option<ActualbudgetAccount> -) -> bool { - !is_category_transfer(transaction, actualbudget_category_transfers_account) -} diff --git a/actualbudget_to_schist_transformer/src/transform_state/mod.rs b/actualbudget_to_schist_transformer/src/transform_state/mod.rs deleted file mode 100644 index 8787699..0000000 --- a/actualbudget_to_schist_transformer/src/transform_state/mod.rs +++ /dev/null @@ -1,25 +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; - -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_accounts::transform_accounts; -use transform_account_transfers::transform_account_transfers; -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; - -pub use transform_state::transform_state; diff --git a/actualbudget_to_schist_transformer/src/transform_state/transform_account_transfers.rs b/actualbudget_to_schist_transformer/src/transform_state/transform_account_transfers.rs deleted file mode 100644 index a0c426d..0000000 --- a/actualbudget_to_schist_transformer/src/transform_state/transform_account_transfers.rs +++ /dev/null @@ -1,306 +0,0 @@ -use actualbudget_models::ActualbudgetTransaction; -use anyhow::{Context, Result}; -use itertools::Itertools; -use schist_models::account_transfer::AccountTransfer; - -use super::{is_account_transfer, is_not_account_transfer, transform_date::transform_date, transform_id::transform_id}; - -pub fn transform_account_transfers( - actualbudget_transactions: &[ActualbudgetTransaction], -) -> Result<(Vec<ActualbudgetTransaction>, Vec<AccountTransfer>)> { - transform_account_transfers_without_context(actualbudget_transactions) - .with_context(|| "failed to transform account transfers") -} - -pub fn transform_account_transfers_without_context( - actualbudget_transactions: &[ActualbudgetTransaction], -) -> Result<(Vec<ActualbudgetTransaction>, Vec<AccountTransfer>)> { - let mut account_transfer_transaction_pairs: Vec<(ActualbudgetTransaction, ActualbudgetTransaction)> = Vec::new(); - let mut transactions_not_transformed: Vec<ActualbudgetTransaction> = Vec::new(); - let account_transfer_transactions = actualbudget_transactions - .iter() - .cloned() - .filter(is_account_transfer); - - for actualbudget_transaction in actualbudget_transactions { - if is_not_account_transfer(actualbudget_transaction) { - transactions_not_transformed.push(actualbudget_transaction.clone()); - continue; - } - - let is_already_transformed = |t: &ActualbudgetTransaction| - account_transfer_transaction_pairs - .iter().any(|(t1, t2)| *t1 == *t || *t2 == *t); - - if is_already_transformed(actualbudget_transaction) { - continue; - } - - let companion = account_transfer_transactions - .clone() - .filter(|t| !is_already_transformed(t) - && t.date == actualbudget_transaction.date - && t.amount == -actualbudget_transaction.amount - ).last(); - - if companion.is_none() { - transactions_not_transformed.push(actualbudget_transaction.clone()); - continue; - } - let companion = companion.unwrap(); - - let (from, to) = if actualbudget_transaction.amount >= 0 { - (companion.clone(), actualbudget_transaction.clone()) - } else { - (actualbudget_transaction.clone(), companion.clone()) - }; - - account_transfer_transaction_pairs.push((from, to)); - } - - Ok(( - transactions_not_transformed, - account_transfer_transaction_pairs - .iter() - .map(transform_account_transfer) - .try_collect()?, - )) -} - -fn transform_account_transfer( - (actualbudget_transaction_from, actualbudget_transaction_to): - &(ActualbudgetTransaction, ActualbudgetTransaction), -) -> Result<AccountTransfer> { - let context = || format!( - "failed to transform account transfer from transactions ({:?}, {:?})", - actualbudget_transaction_from, - actualbudget_transaction_to, - ); - - Ok(AccountTransfer { - id: transform_id(&actualbudget_transaction_from.id), - date: transform_date(&actualbudget_transaction_from.date) - .with_context(context)?, - description: actualbudget_transaction_from.notes.clone().unwrap_or_else(|| - actualbudget_transaction_to.notes.clone().unwrap_or_else(String::new) - ), - quantity: actualbudget_transaction_to.amount, - from_account_id: transform_id(actualbudget_transaction_from.account_id.as_str()), - to_account_id: transform_id(actualbudget_transaction_to.account_id.as_str()), - }) -} - -#[cfg(test)] -mod test { - use actualbudget_fakes::actualbudget_transaction::make_fake_actualbudget_transaction_builder; - use actualbudget_models::ActualbudgetDate; - use schist_models::date_utc::DateUtc; - - use crate::transform_state::transform_id::transform_id; - - use super::transform_account_transfers; - - #[test] - fn when_one_account_transfer_then_transforms() { - let transactions = [ - make_fake_actualbudget_transaction_builder("0") - .amount(-100) - .account_id(String::from("0")) - .payee(Some(String::from("0"))) - .transfer_id(Some(String::from("0"))) - .date(ActualbudgetDate::from_i32(20240801)) - .notes(Some(String::from("My notes"))) - .build() - .unwrap(), - make_fake_actualbudget_transaction_builder("1") - .amount(100) - .account_id(String::from("1")) - .payee(Some(String::from("1"))) - .transfer_id(Some(String::from("0"))) - .date(ActualbudgetDate::from_i32(20240801)) - .notes(Some(String::from("My notes"))) - .build() - .unwrap(), - ]; - - let (remaining_transactions, account_transfers) = - transform_account_transfers(&transactions).unwrap(); - - assert_eq!(0, remaining_transactions.len()); - assert_eq!(1, account_transfers.len()); - let account_transfer = account_transfers.get(0).unwrap(); - assert_eq!("2024-08-01".parse::<DateUtc>().unwrap(), account_transfer.date); - assert_eq!("My notes", account_transfer.description); - assert_eq!(100, account_transfer.quantity); - assert_eq!(transform_id("0"), account_transfer.from_account_id); - assert_eq!(transform_id("1"), account_transfer.to_account_id); - } - - #[test] - fn when_one_account_transfer_in_reverse_order_then_transforms() { - let transactions = [ - make_fake_actualbudget_transaction_builder("0") - .amount(100) - .account_id(String::from("1")) - .payee(Some(String::from("1"))) - .transfer_id(Some(String::from("0"))) - .date(ActualbudgetDate::from_i32(20240801)) - .notes(Some(String::from("My notes"))) - .build() - .unwrap(), - make_fake_actualbudget_transaction_builder("1") - .amount(-100) - .account_id(String::from("0")) - .payee(Some(String::from("0"))) - .transfer_id(Some(String::from("0"))) - .date(ActualbudgetDate::from_i32(20240801)) - .notes(Some(String::from("My notes"))) - .build() - .unwrap(), - ]; - - let (remaining_transactions, account_transfers) = - transform_account_transfers(&transactions).unwrap(); - - assert_eq!(0, remaining_transactions.len()); - assert_eq!(1, account_transfers.len()); - let account_transfer = account_transfers.get(0).unwrap(); - assert_eq!("2024-08-01".parse::<DateUtc>().unwrap(), account_transfer.date); - assert_eq!("My notes", account_transfer.description); - assert_eq!(100, account_transfer.quantity); - assert_eq!(transform_id("0"), account_transfer.from_account_id); - assert_eq!(transform_id("1"), account_transfer.to_account_id); - } - - #[test] - fn when_two_account_transfers_different_amounts_then_transforms() { - let transactions = [ - make_fake_actualbudget_transaction_builder("0") - .amount(-100) - .account_id(String::from("0")) - .payee(Some(String::from("0"))) - .transfer_id(Some(String::from("0"))) - .date(ActualbudgetDate::from_i32(20240801)) - .notes(Some(String::from("My notes 1"))) - .build() - .unwrap(), - make_fake_actualbudget_transaction_builder("1") - .amount(-200) - .account_id(String::from("0")) - .payee(Some(String::from("0"))) - .transfer_id(Some(String::from("0"))) - .date(ActualbudgetDate::from_i32(20240801)) - .notes(Some(String::from("My notes 2"))) - .build() - .unwrap(), - make_fake_actualbudget_transaction_builder("2") - .amount(100) - .account_id(String::from("1")) - .payee(Some(String::from("1"))) - .transfer_id(Some(String::from("0"))) - .date(ActualbudgetDate::from_i32(20240801)) - .build() - .unwrap(), - make_fake_actualbudget_transaction_builder("2") - .amount(200) - .account_id(String::from("1")) - .payee(Some(String::from("1"))) - .transfer_id(Some(String::from("0"))) - .date(ActualbudgetDate::from_i32(20240801)) - .build() - .unwrap(), - ]; - - let (remaining_transactions, account_transfers) = - transform_account_transfers(&transactions).unwrap(); - - assert_eq!(0, remaining_transactions.len()); - assert_eq!(2, account_transfers.len()); - - let account_transfer_0 = account_transfers - .iter() - .filter(|at| at.quantity == 100) - .last() - .unwrap(); - assert_eq!("2024-08-01".parse::<DateUtc>().unwrap(), account_transfer_0.date); - assert_eq!(transform_id("0"), account_transfer_0.from_account_id); - assert_eq!(transform_id("1"), account_transfer_0.to_account_id); - assert_eq!("My notes 1", account_transfer_0.description); - - let account_transfer_1 = account_transfers - .iter() - .filter(|at| at.quantity == 200) - .last() - .unwrap(); - assert_eq!("2024-08-01".parse::<DateUtc>().unwrap(), account_transfer_1.date); - assert_eq!(transform_id("0"), account_transfer_1.from_account_id); - assert_eq!(transform_id("1"), account_transfer_1.to_account_id); - assert_eq!("My notes 2", account_transfer_1.description); - } - - #[test] - fn when_two_account_transfers_different_dates_then_transforms() { - let transactions = [ - make_fake_actualbudget_transaction_builder("0") - .amount(-100) - .account_id(String::from("0")) - .payee(Some(String::from("0"))) - .transfer_id(Some(String::from("0"))) - .date(ActualbudgetDate::from_i32(20240801)) - .notes(Some(String::from("My notes 1"))) - .build() - .unwrap(), - make_fake_actualbudget_transaction_builder("1") - .amount(-100) - .account_id(String::from("0")) - .payee(Some(String::from("0"))) - .transfer_id(Some(String::from("0"))) - .date(ActualbudgetDate::from_i32(20240802)) - .notes(Some(String::from("My notes 2"))) - .build() - .unwrap(), - make_fake_actualbudget_transaction_builder("2") - .amount(100) - .account_id(String::from("1")) - .payee(Some(String::from("1"))) - .transfer_id(Some(String::from("0"))) - .date(ActualbudgetDate::from_i32(20240801)) - .build() - .unwrap(), - make_fake_actualbudget_transaction_builder("3") - .amount(100) - .account_id(String::from("1")) - .payee(Some(String::from("1"))) - .transfer_id(Some(String::from("0"))) - .date(ActualbudgetDate::from_i32(20240802)) - .build() - .unwrap(), - ]; - - let (remaining_transactions, account_transfers) = - transform_account_transfers(&transactions).unwrap(); - - assert_eq!(0, remaining_transactions.len()); - assert_eq!(2, account_transfers.len()); - - let account_transfer_0 = account_transfers - .iter() - .filter(|at| at.date == "2024-08-01".parse().unwrap()) - .last() - .unwrap(); - assert_eq!(100, account_transfer_0.quantity); - assert_eq!(transform_id("0"), account_transfer_0.from_account_id); - assert_eq!(transform_id("1"), account_transfer_0.to_account_id); - assert_eq!("My notes 1", account_transfer_0.description); - - let account_transfer_1 = account_transfers - .iter() - .filter(|at| at.date == "2024-08-02".parse().unwrap()) - .last() - .unwrap(); - assert_eq!(100, account_transfer_1.quantity); - assert_eq!(transform_id("0"), account_transfer_1.from_account_id); - assert_eq!(transform_id("1"), account_transfer_1.to_account_id); - assert_eq!("My notes 2", account_transfer_1.description); - } -} diff --git a/actualbudget_to_schist_transformer/src/transform_state/transform_accounts.rs b/actualbudget_to_schist_transformer/src/transform_state/transform_accounts.rs deleted file mode 100644 index 28d8162..0000000 --- a/actualbudget_to_schist_transformer/src/transform_state/transform_accounts.rs +++ /dev/null @@ -1,30 +0,0 @@ -use actualbudget_models::ActualbudgetAccount; -use schist_models::{account::Account, date_utc::DateUtc}; - -use super::transform_id; - -pub fn transform_accounts( - actualbudget_accounts: &[ActualbudgetAccount], - actualbudget_category_transfers_account: &Option<ActualbudgetAccount>, - first_activity_date: &DateUtc, -) -> Vec<Account> { - actualbudget_accounts - .iter() - .filter(|a| actualbudget_category_transfers_account - .clone() - .is_some_and(|cta| cta.id != a.id) - ).map(|a| transform_account(a, first_activity_date)) - .collect() -} - -fn transform_account( - actualbudget_account: &ActualbudgetAccount, - opening_date: &DateUtc, -) -> Account { - Account { - id: transform_id(&actualbudget_account.id.clone()), - name: actualbudget_account.name.clone(), - opening_balance: 0, - opening_date: opening_date.clone(), - } -} diff --git a/actualbudget_to_schist_transformer/src/transform_state/transform_budget_drips.rs b/actualbudget_to_schist_transformer/src/transform_state/transform_budget_drips.rs deleted file mode 100644 index 1163e56..0000000 --- a/actualbudget_to_schist_transformer/src/transform_state/transform_budget_drips.rs +++ /dev/null @@ -1,306 +0,0 @@ -use std::hash::{DefaultHasher, Hash, Hasher}; - -use actualbudget_models::ActualbudgetZeroBudget; -use anyhow::{Context, Result}; -use itertools::Itertools; -use schist_models::{budget_drip::BudgetDrip, date_utc::DateUtc}; -use schist_traits::dateable::Dateable; - -use super::{transform_date::transform_date, transform_id::transform_id}; - -pub fn transform_budget_drips(actualbudget_zero_budgets: &[ActualbudgetZeroBudget]) -> Result<Vec<BudgetDrip>> { - let non_zero_actualbudget_zero_budgets: Vec<ActualbudgetZeroBudget> = 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::<Result<Vec<Vec<BudgetDrip>>>>() - ).flatten() - .flatten() - .filter(|bd| bd.quantity != 0) - .collect(); - - Ok(budget_drips) -} - -fn map_actualbudget_zero_budget_to_budget_drips( - actualbudget_zero_budget: &ActualbudgetZeroBudget, -) -> Result<Vec<BudgetDrip>> { - let context = || "failed to map actualbudget zero budgets to budget drips"; - - let first_day_of_month: DateUtc = Into::<DateUtc>::into(transform_date(&actualbudget_zero_budget.month).with_context(context)?); - let month = first_day_of_month.month(); - let days_in_month: Vec<DateUtc> = (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 quantity = actualbudget_zero_budget.amount / num_days_in_month; - let remainder_quantity = actualbudget_zero_budget.amount - (quantity * num_days_in_month); - - let budget_drips: Vec<BudgetDrip> = days_in_month.iter().map(|&date| - BudgetDrip { - id: transform_budget_drip_id(&actualbudget_zero_budget, &date), - category_id: transform_id(&actualbudget_zero_budget.category_id), - date, - quantity: if date == first_day_of_month { - quantity + remainder_quantity - } else { - quantity - } - } - ).collect(); - - Ok(budget_drips) -} - -fn sort_actualbudget_zero_budgets_by_month( - actualbudget_zero_budgets_by_category_id: &Vec<ActualbudgetZeroBudget>, -) -> Vec<ActualbudgetZeroBudget> { - 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<Vec<ActualbudgetZeroBudget>> { - 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::<u64>::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::budget_drip::BudgetDrip; - use schist_traits::dateable::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::budget_drip::BudgetDrip; - use schist_traits::dateable::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.quantity == 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_quantity_not_floored: Vec<&BudgetDrip> = budget_drips - .iter() - .filter(|bd| bd.quantity != 100) - .collect(); - assert_eq!(1, budget_drips_quantity_not_floored.len()); - - let budget_drips_floored: Vec<&BudgetDrip> = budget_drips - .iter() - .filter(|bd| bd.quantity == 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.quantity == 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_quantity_not_floored: Vec<&BudgetDrip> = budget_drips - .iter() - .filter(|bd| bd.quantity != 100) - .collect(); - assert_eq!(1, budget_drips_quantity_not_floored.len()); - - let budget_drips_floored: Vec<&BudgetDrip> = budget_drips - .iter() - .filter(|bd| bd.quantity == 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<&BudgetDrip> = budget_drips - .iter() - .filter(|bd| bd.category_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.quantity == 100)); - - let budget_drips_jan_category_1: Vec<&BudgetDrip> = budget_drips - .iter() - .filter(|bd| bd.category_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.quantity == 200)); - - let budget_drips_feb_category_0: Vec<&BudgetDrip> = budget_drips - .iter() - .filter(|bd| bd.category_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.quantity == 110)); - - let budget_drips_feb_category_1: Vec<&BudgetDrip> = budget_drips - .iter() - .filter(|bd| bd.category_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.quantity == 210)); - } -} diff --git a/actualbudget_to_schist_transformer/src/transform_state/transform_categories.rs b/actualbudget_to_schist_transformer/src/transform_state/transform_categories.rs deleted file mode 100644 index 17ce194..0000000 --- a/actualbudget_to_schist_transformer/src/transform_state/transform_categories.rs +++ /dev/null @@ -1,204 +0,0 @@ -use actualbudget_models::{ActualbudgetCategory, ActualbudgetZeroBudget}; -use itertools::Itertools; -use schist_models::{budget_period_unit::BudgetPeriodUnit, category::Category, date_utc::DateUtc}; - -use super::transform_id; - -pub fn transform_categories( - actualbudget_categories: &[ActualbudgetCategory], - actualbudget_zero_budgets: &[ActualbudgetZeroBudget], - before_first_activity: &DateUtc, -) -> Vec<Category> { - let last_non_zero_month = actualbudget_zero_budgets - .iter() - .map(|zb| zb.month.clone()) - .unique() - .filter(|month| actualbudget_zero_budgets - .iter() - .any(|zb| zb.month == *month && zb.amount != 0) - ).max(); - - let actualbudget_zero_budgets = if let Some(last_non_zero_month) = last_non_zero_month { - &actualbudget_zero_budgets - .iter() - .filter(|zb| zb.month == last_non_zero_month) - .cloned() - .collect::<Vec<_>>() - } else { - actualbudget_zero_budgets - }; - - actualbudget_categories.iter().map(|actualbudget_category| Category { - id: transform_id(&actualbudget_category.id.clone()), - name: actualbudget_category.name.clone(), - balance: 0, - balance_date: before_first_activity.clone(), - budget_period: 1, - budget_period_unit: BudgetPeriodUnit::Month, - budget_quantity: get_budget_quantity(actualbudget_category, actualbudget_zero_budgets), - }).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, actualbudget_zero_budget::make_fake_actualbudget_zero_budget_builder}; - use actualbudget_models::ActualbudgetDate; - use schist_models::{budget_period_unit::BudgetPeriodUnit, date_utc::DateUtc}; - use schist_traits::nowlike::Nowlike; - - 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 actualbudget_zero_budgets = [ - make_fake_actualbudget_zero_budget_builder("0").amount(100).build().unwrap(), - ]; - let before_first_transaction = DateUtc::now(); - - let categories = transform_categories( - &actualbudget_categories, - &actualbudget_zero_budgets, - &before_first_transaction, - ); - - assert_eq!(1, categories.len()); - let category = categories.get(0).unwrap(); - assert_eq!("Food", category.name); - assert_eq!(0, category.balance); - assert_eq!(before_first_transaction, category.balance_date); - assert_eq!(1, category.budget_period); - assert_eq!(BudgetPeriodUnit::Month, category.budget_period_unit); - assert_eq!(100, category.budget_quantity); - } - - #[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 actualbudget_zero_budgets = []; - let before_first_transaction = DateUtc::now(); - - let categories = transform_categories( - &actualbudget_categories, - &actualbudget_zero_budgets, - &before_first_transaction, - ); - - assert_eq!(1, categories.len()); - let category = categories.get(0).unwrap(); - assert_eq!("Food", category.name); - assert_eq!(0, category.balance); - assert_eq!(before_first_transaction, category.balance_date); - assert_eq!(1, category.budget_period); - assert_eq!(BudgetPeriodUnit::Month, category.budget_period_unit); - assert_eq!(0, category.budget_quantity); - } - - #[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 jan = ActualbudgetDate::from_i32(202401); - let feb = ActualbudgetDate::from_i32(202402); - let mar = ActualbudgetDate::from_i32(202403); - let actualbudget_zero_budgets = [ - make_fake_actualbudget_zero_budget_builder("0").amount(100).month(jan).build().unwrap(), - make_fake_actualbudget_zero_budget_builder("1").amount(200).month(feb).build().unwrap(), - make_fake_actualbudget_zero_budget_builder("2").amount(300).month(mar).build().unwrap(), - ]; - let before_first_transaction = DateUtc::now(); - - let categories = transform_categories( - &actualbudget_categories, - &actualbudget_zero_budgets, - &before_first_transaction, - ); - - assert_eq!(1, categories.len()); - let category = categories.get(0).unwrap(); - assert_eq!("Food", category.name); - assert_eq!(0, category.balance); - assert_eq!(before_first_transaction, category.balance_date); - assert_eq!(1, category.budget_period); - assert_eq!(BudgetPeriodUnit::Month, category.budget_period_unit); - assert_eq!(300, category.budget_quantity); - } - - #[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 jan = ActualbudgetDate::from_i32(202401); - let feb = ActualbudgetDate::from_i32(202402); - let mar = ActualbudgetDate::from_i32(202403); - let actualbudget_zero_budgets = [ - make_fake_actualbudget_zero_budget_builder("0").amount(100).month(jan.clone()).category_id(String::from("0")).build().unwrap(), - make_fake_actualbudget_zero_budget_builder("1").amount(200).month(feb.clone()).category_id(String::from("0")).build().unwrap(), - make_fake_actualbudget_zero_budget_builder("2").amount(300).month(mar.clone()).category_id(String::from("0")).build().unwrap(), - make_fake_actualbudget_zero_budget_builder("3").amount(110).month(jan.clone()).category_id(String::from("1")).build().unwrap(), - make_fake_actualbudget_zero_budget_builder("4").amount(210).month(feb.clone()).category_id(String::from("1")).build().unwrap(), - make_fake_actualbudget_zero_budget_builder("5").amount(310).month(mar.clone()).category_id(String::from("1")).build().unwrap(), - make_fake_actualbudget_zero_budget_builder("6").amount(120).month(jan.clone()).category_id(String::from("2")).build().unwrap(), - make_fake_actualbudget_zero_budget_builder("7").amount(220).month(feb.clone()).category_id(String::from("2")).build().unwrap(), - make_fake_actualbudget_zero_budget_builder("8").amount(320).month(mar.clone()).category_id(String::from("2")).build().unwrap(), - ]; - let before_first_transaction = DateUtc::now(); - - let categories = transform_categories( - &actualbudget_categories, - &actualbudget_zero_budgets, - &before_first_transaction, - ); - - assert_eq!(3, categories.len()); - - let category_0 = categories.iter().filter(|c| c.id == transform_id("0")).last().unwrap(); - assert_eq!("Food", category_0.name); - assert_eq!(0, category_0.balance); - assert_eq!(before_first_transaction, category_0.balance_date); - assert_eq!(1, category_0.budget_period); - assert_eq!(BudgetPeriodUnit::Month, category_0.budget_period_unit); - assert_eq!(300, category_0.budget_quantity); - - let category_1 = categories.iter().filter(|c| c.id == transform_id("1")).last().unwrap(); - assert_eq!("Clothes", category_1.name); - assert_eq!(0, category_1.balance); - assert_eq!(before_first_transaction, category_1.balance_date); - assert_eq!(1, category_1.budget_period); - assert_eq!(BudgetPeriodUnit::Month, category_1.budget_period_unit); - assert_eq!(310, category_1.budget_quantity); - - let category_2 = categories.iter().filter(|c| c.id == transform_id("2")).last().unwrap(); - assert_eq!("Rent", category_2.name); - assert_eq!(0, category_2.balance); - assert_eq!(before_first_transaction, category_2.balance_date); - assert_eq!(1, category_2.budget_period); - assert_eq!(BudgetPeriodUnit::Month, category_2.budget_period_unit); - assert_eq!(320, category_2.budget_quantity); - } -} diff --git a/actualbudget_to_schist_transformer/src/transform_state/transform_category_transfers.rs b/actualbudget_to_schist_transformer/src/transform_state/transform_category_transfers.rs deleted file mode 100644 index 318e88c..0000000 --- a/actualbudget_to_schist_transformer/src/transform_state/transform_category_transfers.rs +++ /dev/null @@ -1,346 +0,0 @@ -use actualbudget_models::{ - ActualbudgetAccount, - ActualbudgetTransaction, -}; -use anyhow::{bail, Context, Result}; -use itertools::Itertools; -use schist_models::category_transfer::CategoryTransfer; - -use super::{is_category_transfer, transform_id}; - -pub fn transform_category_transfers( - actualbudget_transactions: &[ActualbudgetTransaction], - actualbudget_category_transfers_account: &Option<ActualbudgetAccount>, -) -> Result<Vec<CategoryTransfer>> { - let actualbudget_category_transfers = actualbudget_transactions - .iter() - .filter(|transaction| - is_category_transfer(transaction, actualbudget_category_transfers_account) - ); - let actualbudget_parent_category_transfers: Vec<ActualbudgetTransaction> = actualbudget_category_transfers - .clone() - .filter(|t| t.is_parent) - .map(ActualbudgetTransaction::clone) - .collect(); - let actualbudget_child_category_transfers: Vec<ActualbudgetTransaction> = actualbudget_category_transfers - .filter(|t| t.is_child) - .map(ActualbudgetTransaction::clone) - .collect(); - - Ok(actualbudget_parent_category_transfers - .iter() - .map(|pct| transform_category_transfer(pct, &actualbudget_child_category_transfers)) - .filter_ok(Option::is_some) - .map_ok(Option::unwrap) - .collect::<Result<Vec<CategoryTransfer>>>() - .with_context(|| "failed to transform category transfers")? - ) -} - -fn transform_category_transfer( - actualbudget_parent_category_transfer: &ActualbudgetTransaction, - actualbudget_child_category_transfers: &[ActualbudgetTransaction], -) -> Result<Option<CategoryTransfer>> { - 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<Option<CategoryTransfer>> { - let actualbudget_child_category_transfers = actualbudget_child_category_transfers - .iter() - .filter(|t| - t.parent_id - .clone() - .is_some_and(|parent_id| parent_id == actualbudget_parent_category_transfer.id) - ).map(ActualbudgetTransaction::clone); - - let from_transfers: Vec<ActualbudgetTransaction> = actualbudget_child_category_transfers - .clone() - .filter(|t| t.amount < 0) - .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 - .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(CategoryTransfer { - id: transform_id(&actualbudget_parent_category_transfer.id), - description: actualbudget_parent_category_transfer - .notes - .clone() - .unwrap_or_else(String::new), - quantity: to_transfer.amount.try_into().unwrap(), - from_category_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::category_transfer::CategoryTransfer; - - 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 category_transfers = result.unwrap(); - assert_eq!(1, category_transfers.len()); - let category_transfer = category_transfers.get(0).unwrap(); - assert_eq!(100, category_transfer.quantity); - assert_eq!("I was a parent transaction", category_transfer.description); - assert_eq!(transform_id("0"), category_transfer.from_category_id); - assert_eq!(transform_id("1"), category_transfer.to_category_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 category_transfers = result.unwrap(); - assert_eq!(2, category_transfers.len()); - - let category_transfer_0: Vec<&CategoryTransfer> = category_transfers - .iter() - .filter(|ct| ct.quantity == 100) - .collect(); - assert_eq!(1, category_transfer_0.len()); - let category_transfer_0 = category_transfer_0.get(0).unwrap(); - assert_eq!(transform_id("0"), category_transfer_0.from_category_id); - assert_eq!(transform_id("1"), category_transfer_0.to_category_id); - - let category_transfer_1: Vec<&CategoryTransfer> = category_transfers - .iter() - .filter(|ct| ct.quantity == 200) - .collect(); - assert_eq!(1, category_transfer_1.len()); - let category_transfer_1 = category_transfer_1.get(0).unwrap(); - assert_eq!(transform_id("1"), category_transfer_1.from_category_id); - assert_eq!(transform_id("0"), category_transfer_1.to_category_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/actualbudget_to_schist_transformer/src/transform_state/transform_date.rs b/actualbudget_to_schist_transformer/src/transform_state/transform_date.rs deleted file mode 100644 index 57adae0..0000000 --- a/actualbudget_to_schist_transformer/src/transform_state/transform_date.rs +++ /dev/null @@ -1,31 +0,0 @@ -use actualbudget_models::ActualbudgetDate; -use anyhow::Result; -use schist_models::date_utc::DateUtc; - -pub fn transform_date(actualbudget_date: &ActualbudgetDate) -> Result<DateUtc> { - actualbudget_date.to_iso_date().parse() -} - -#[cfg(test)] -mod test { - use actualbudget_models::ActualbudgetDate; - use schist_models::date_utc::DateUtc; - - use super::transform_date; - - #[test] - fn when_utc_epoch_as_day_then_utc_epoch() { - let actualbudget_date = ActualbudgetDate::from_i32(19700101); - let date_utc = transform_date(&actualbudget_date).unwrap(); - let expected_date_utc: DateUtc = "1970-01-01".parse().unwrap(); - assert_eq!(expected_date_utc, date_utc); - } - - #[test] - fn when_utc_epoch_as_month_then_utc_epoch() { - let actualbudget_date = ActualbudgetDate::from_i32(197001); - let date_utc = transform_date(&actualbudget_date).unwrap(); - let expected_date_utc: DateUtc = "1970-01-01".parse().unwrap(); - assert_eq!(expected_date_utc, date_utc); - } -} diff --git a/actualbudget_to_schist_transformer/src/transform_state/transform_id.rs b/actualbudget_to_schist_transformer/src/transform_state/transform_id.rs deleted file mode 100644 index 2d2f3af..0000000 --- a/actualbudget_to_schist_transformer/src/transform_state/transform_id.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::hash::{DefaultHasher, Hash, Hasher}; - -pub fn transform_id(actualbudget_id: &str) -> i32 { - let mut hasher = DefaultHasher::new(); - actualbudget_id.hash(&mut hasher); - (hasher.finish() % std::convert::TryInto::<u64>::try_into(i32::MAX).unwrap()) - .try_into() - .unwrap() -} - -#[cfg(test)] -mod test { - use crate::test_resources::RANDOM_UUIDS; - - use super::transform_id; - - #[test] - fn when_transform_many_random_uuids_then_no_repeats() { - let mut results_so_far: Vec<i32> = Vec::new(); - for &uuid in RANDOM_UUIDS { - let transformed_uuid = transform_id(uuid); - assert!(!results_so_far.contains(&transformed_uuid)); - results_so_far.push(transformed_uuid); - } - } -} diff --git a/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs b/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs deleted file mode 100644 index 7c64e35..0000000 --- a/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs +++ /dev/null @@ -1,64 +0,0 @@ -use anyhow::{bail, Context, Result}; -use schist_models::{account::Account, category::Category, category_transfer::CategoryTransfer, date_utc::DateUtc, transaction::Transaction, transaction_categorisation::TransactionCategorisation}; - -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}; - -pub fn transform_state(actualbudget_state: ActualbudgetState) -> Result<SchistState> { - transform_state_without_context(actualbudget_state) - .with_context(|| "failed to transform actualbudget state to schist state") -} - -fn transform_state_without_context(actualbudget_state: ActualbudgetState) -> Result<SchistState> { - let (remaining_transactions, account_transfers) = transform_account_transfers( - &actualbudget_state.actualbudget_transactions, - )?; - let first_activity_date = - transform_date(&actualbudget_state.actualbudget_first_activity_date)?; - let before_first_activity_date = find_date_before(&first_activity_date)?; - let accounts: Vec<Account> = - transform_accounts( - &actualbudget_state.actualbudget_accounts, - &actualbudget_state.actualbudget_category_transfers_account, - &first_activity_date, - ); - let budget_drips = transform_budget_drips(&actualbudget_state.actualbudget_zero_budgets)?; - let categories: Vec<Category> = transform_categories( - &actualbudget_state.actualbudget_categories, - &actualbudget_state.actualbudget_zero_budgets, - &before_first_activity_date, - ); - let (transactions, transaction_categorisations): (Vec<Transaction>, Vec<TransactionCategorisation>) = - transform_transactions( - &remaining_transactions, - &actualbudget_state.actualbudget_category_transfers_account, - &actualbudget_state.actualbudget_payees, - )?; - let category_transfers: Vec<CategoryTransfer> = transform_category_transfers( - &remaining_transactions, - &actualbudget_state.actualbudget_category_transfers_account, - )?; - - Ok(SchistState { - first_transaction_date: first_activity_date, - accounts, - account_transfers, - categories, - budget_drips, - transactions, - transaction_categorisations, - category_transfers, - }) -} - -fn find_date_before(date: &DateUtc) -> Result<DateUtc> { - for days_before in 1..31 { - let result = date.sub_days(days_before); - if result.is_ok() { - return result; - } - } - - bail!("failed to find a date before {}", date) -} diff --git a/actualbudget_to_schist_transformer/src/transform_state/transform_transactions.rs b/actualbudget_to_schist_transformer/src/transform_state/transform_transactions.rs deleted file mode 100644 index f7fb098..0000000 --- a/actualbudget_to_schist_transformer/src/transform_state/transform_transactions.rs +++ /dev/null @@ -1,253 +0,0 @@ -use anyhow::{Context, Result}; -use actualbudget_models::{ActualbudgetAccount, ActualbudgetPayee, ActualbudgetTransaction}; -use schist_models::{transaction::Transaction, transaction_categorisation::TransactionCategorisation}; - -use super::{is_not_category_transfer, transform_date, transform_id}; - -pub fn transform_transactions( - actualbudget_transactions: &[ActualbudgetTransaction], - actualbudget_category_transfers_account: &Option<ActualbudgetAccount>, - actualbudget_payees: &[ActualbudgetPayee], -) -> Result<(Vec<Transaction>, Vec<TransactionCategorisation>)> { - transform_transactions_without_context( - actualbudget_transactions, - actualbudget_category_transfers_account, - actualbudget_payees, - ) - .with_context(|| "failed to transform actualbudget transactions to schist transactions") -} - -fn transform_transactions_without_context( - actualbudget_transactions: &[ActualbudgetTransaction], - actualbudget_category_transfers_account: &Option<ActualbudgetAccount>, - actualbudget_payees: &[ActualbudgetPayee], -) -> Result<(Vec<Transaction>, Vec<TransactionCategorisation>)> { - let mut transactions = Vec::<Transaction>::new(); - let mut transaction_categorisations = Vec::<TransactionCategorisation>::new(); - - let actualbudget_transactions = actualbudget_transactions - .iter() - .filter(|t| is_not_category_transfer(t, actualbudget_category_transfers_account)); - let parent_actualbudget_transactions: Vec<&ActualbudgetTransaction> = actualbudget_transactions - .clone() - .filter(|t| t.is_parent) - .collect(); - let child_actualbudget_transactions: Vec<&ActualbudgetTransaction> = actualbudget_transactions - .clone() - .filter(|t| t.is_child) - .collect(); - let atomic_actualbudget_transactions: Vec<&ActualbudgetTransaction> = actualbudget_transactions - .filter(|t| !t.is_parent && !t.is_child) - .collect(); - - for atomic_actualbudget_transaction in atomic_actualbudget_transactions { - let transaction_id = transform_id(&atomic_actualbudget_transaction.id); - - let payee_name = atomic_actualbudget_transaction - .payee - .clone() - .map(|p| actualbudget_payees - .iter() - .find(|pp| pp.id == p) - .map(|pp| pp.name.clone()) - ).unwrap_or(None) - .unwrap_or_else(String::new); - - transactions.push(Transaction { - id: transaction_id, - description: atomic_actualbudget_transaction.notes.clone().unwrap_or_else(String::new), - payee: payee_name, - quantity: atomic_actualbudget_transaction.amount.try_into().unwrap(), - date: transform_date(&atomic_actualbudget_transaction.date)?, - account_id: transform_id(&atomic_actualbudget_transaction.account_id), - }); - - let actualbudget_category_id = atomic_actualbudget_transaction - .category_id - .clone() - .with_context(|| format!( - "atomic actualbudget transaction {} did not have a category ID", - atomic_actualbudget_transaction.id, - ))?; - - transaction_categorisations.push(TransactionCategorisation { - id: transaction_id, - description: String::new(), - quantity: atomic_actualbudget_transaction.amount.try_into().unwrap(), - transaction_id, - category_id: transform_id(&actualbudget_category_id), - }); - } - - for parent_actualbudget_transaction in parent_actualbudget_transactions { - let child_actualbudget_transactions = child_actualbudget_transactions - .iter() - .filter(|t| t - .parent_id - .clone() - .is_some_and(|parent_id| parent_id == parent_actualbudget_transaction.id.clone()) - ); - - let transaction_id = transform_id(&parent_actualbudget_transaction.id); - - transactions.push(Transaction { - id: transaction_id, - description: parent_actualbudget_transaction.notes.clone().unwrap_or_else(String::new), - payee: parent_actualbudget_transaction.payee.clone().unwrap_or_else(String::new), - quantity: parent_actualbudget_transaction.amount.try_into().unwrap(), - date: transform_date(&parent_actualbudget_transaction.date)?, - account_id: transform_id(&parent_actualbudget_transaction.account_id), - }); - - for child_actualbudget_transaction in child_actualbudget_transactions { - let actualbudget_category_id = child_actualbudget_transaction - .category_id - .clone() - .with_context(|| format!( - "actualbudget child transaction {} had no category ID", - child_actualbudget_transaction.id, - ))?; - - transaction_categorisations.push(TransactionCategorisation { - id: transform_id(&child_actualbudget_transaction.id), - description: child_actualbudget_transaction.notes.clone().unwrap_or_else(String::new), - quantity: child_actualbudget_transaction.amount.try_into().unwrap(), - transaction_id, - category_id: transform_id(&actualbudget_category_id), - }); - } - } - - Ok((transactions, transaction_categorisations)) -} - -#[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_transaction::make_fake_actualbudget_transaction_builder}; - use actualbudget_models::ActualbudgetDate; - use schist_models::date_utc::DateUtc; - - 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, transaction_categorisations) = transform_transactions( - &actualbudget_transactions, - &Some(actualbudget_category_transfers_account), - &actualbudget_payees, - ).unwrap(); - - assert_eq!(0, transactions.len()); - assert_eq!(0, transaction_categorisations.len()); - } - - #[test] - fn when_one_atomic_transaction_then_returns_transformed_transaction() { - let actualbudget_transactions = [ - make_fake_actualbudget_transaction_builder("0") - .account_id(String::from("0")) - .amount(100) - .category_id(Some(String::from("0"))) - .payee(Some(String::from("0"))) - .notes(Some(String::from("Savings"))) - .date(ActualbudgetDate::from_i32(19700101)) - .build() - .unwrap(), - ]; - let actualbudget_payees = [ - make_fake_actualbudget_payee_builder("0") - .name(String::from("Gringotts")) - .build() - .unwrap(), - ]; - - let (transactions, transaction_categorisations) = transform_transactions( - &actualbudget_transactions, - &None, - &actualbudget_payees, - ).unwrap(); - - assert_eq!(1, transactions.len()); - let transaction = transactions.get(0).unwrap(); - assert_eq!("Savings", transaction.description); - assert_eq!("Gringotts", transaction.payee); - assert_eq!(100, transaction.quantity); - assert_eq!(DateUtc::from_ymd(1970, 1, 1).unwrap(), transaction.date); - assert_eq!(transform_id("0"), transaction.account_id); - - assert_eq!(1, transaction_categorisations.len()); - let transaction_categorisation = transaction_categorisations.get(0).unwrap(); - assert_eq!("", transaction_categorisation.description); - assert_eq!(100, transaction_categorisation.quantity); - assert_eq!(transaction.id, transaction_categorisation.transaction_id); - assert_eq!(transform_id("0"), transaction_categorisation.category_id); - } - - #[test] - fn when_one_split_transaction_then_returns_transformed_transaction() { - let actualbudget_transactions = [ - make_fake_actualbudget_transaction_builder("0") - .is_parent(true) - .amount(100) - .build() - .unwrap(), - make_fake_actualbudget_transaction_builder("1") - .amount(200) - .category_id(Some(String::from("0"))) - .is_child(true) - .parent_id(Some(String::from("0"))) - .notes(Some(String::from("notes A"))) - .build() - .unwrap(), - make_fake_actualbudget_transaction_builder("2") - .amount(300) - .category_id(Some(String::from("0"))) - .is_child(true) - .parent_id(Some(String::from("0"))) - .notes(Some(String::from("notes B"))) - .build() - .unwrap(), - ]; - - let (transactions, transaction_categorisations) = transform_transactions( - &actualbudget_transactions, - &None, - &[], - ).unwrap(); - - assert_eq!(1, transactions.len()); - assert_eq!(2, transaction_categorisations.len()); - - let transaction = transactions.get(0).unwrap(); - assert_eq!(100, transaction.quantity); - - let transaction_categorisation_0 = transaction_categorisations - .iter() - .filter(|tc| tc.quantity == 200) - .last() - .unwrap(); - assert_eq!(transaction.id, transaction_categorisation_0.transaction_id); - assert_eq!(transform_id("0"), transaction_categorisation_0.category_id); - assert_eq!("notes A", transaction_categorisation_0.description); - - let transaction_categorisation_1 = transaction_categorisations - .iter() - .filter(|tc| tc.quantity == 300) - .last() - .unwrap(); - assert_eq!(transaction.id, transaction_categorisation_1.transaction_id); - assert_eq!(transform_id("0"), transaction_categorisation_1.category_id); - assert_eq!("notes B", transaction_categorisation_1.description); - } -} |
