diff options
5 files changed, 335 insertions, 5 deletions
diff --git a/rust/actualbudget_to_schist_transformer/src/schist_state.rs b/rust/actualbudget_to_schist_transformer/src/schist_state.rs index e72d56a..370a0e8 100644 --- a/rust/actualbudget_to_schist_transformer/src/schist_state.rs +++ b/rust/actualbudget_to_schist_transformer/src/schist_state.rs @@ -1,12 +1,13 @@ use anyhow::Result; use diesel::SqliteConnection; use schist_models::{ - account::Account, budget_drip::BudgetDrip, category::Category, category_transfer::CategoryTransfer, date_utc::DateUtc, transaction::Transaction, transaction_categorisation::TransactionCategorisation + account::Account, account_transfer::AccountTransfer, budget_drip::BudgetDrip, category::Category, category_transfer::CategoryTransfer, date_utc::DateUtc, transaction::Transaction, transaction_categorisation::TransactionCategorisation }; pub struct SchistState { pub first_transaction_date: DateUtc, pub accounts: Vec<Account>, + pub account_transfers: Vec<AccountTransfer>, pub categories: Vec<Category>, pub budget_drips: Vec<BudgetDrip>, pub transactions: Vec<Transaction>, @@ -17,6 +18,7 @@ pub struct SchistState { pub fn export_schist_state(schist_state: &SchistState, schist_connection: &mut SqliteConnection) -> Result<()> { schist_queries::clear::clear(schist_connection)?; schist_queries::accounts::insert_accounts(&schist_state.accounts, schist_connection)?; + schist_queries::account_transfers::insert_account_transfers(&schist_state.account_transfers, schist_connection)?; schist_queries::categories::insert_categories(&schist_state.categories, schist_connection)?; schist_queries::budget_drips::insert_budget_drips(&schist_state.budget_drips, schist_connection)?; schist_queries::transactions::insert_transactions(&schist_state.transactions, schist_connection)?; diff --git a/rust/actualbudget_to_schist_transformer/src/transform_state/is_account_transfer.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/is_account_transfer.rs new file mode 100644 index 0000000..c9540ed --- /dev/null +++ b/rust/actualbudget_to_schist_transformer/src/transform_state/is_account_transfer.rs @@ -0,0 +1,13 @@ +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/rust/actualbudget_to_schist_transformer/src/transform_state/mod.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/mod.rs index 916894a..8787699 100644 --- a/rust/actualbudget_to_schist_transformer/src/transform_state/mod.rs +++ b/rust/actualbudget_to_schist_transformer/src/transform_state/mod.rs @@ -1,4 +1,6 @@ +mod is_account_transfer; mod is_category_transfer; +mod transform_account_transfers; mod transform_accounts; mod transform_budget_drips; mod transform_categories; @@ -8,13 +10,16 @@ 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::*; +pub use transform_state::transform_state; diff --git a/rust/actualbudget_to_schist_transformer/src/transform_state/transform_account_transfers.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_account_transfers.rs new file mode 100644 index 0000000..7c33aa6 --- /dev/null +++ b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_account_transfers.rs @@ -0,0 +1,306 @@ +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 { + (actualbudget_transaction.clone(), companion.clone()) + } else { + (companion.clone(), actualbudget_transaction.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_from.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/rust/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs index 395b72b..66bb489 100644 --- a/rust/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs +++ b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs @@ -3,7 +3,7 @@ use schist_models::{account::Account, category::Category, category_transfer::Cat use crate::{actualbudget_state::ActualbudgetState, schist_state::SchistState}; -use super::{transform_accounts, transform_budget_drips::transform_budget_drips, transform_categories, transform_category_transfers, transform_date, transform_transactions}; +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) @@ -11,6 +11,9 @@ pub fn transform_state(actualbudget_state: ActualbudgetState) -> Result<SchistSt } fn transform_state_without_context(actualbudget_state: ActualbudgetState) -> Result<SchistState> { + let (remaining_transactions, account_transfers) = transform_account_transfers( + &actualbudget_state.actualbudget_transactions, + )?; let first_transaction_date = transform_date(&actualbudget_state.actualbudget_first_transaction_date)?; let before_first_transaction_date = find_date_before(&first_transaction_date)?; @@ -24,17 +27,18 @@ fn transform_state_without_context(actualbudget_state: ActualbudgetState) -> Res ); let (transactions, transaction_categorisations): (Vec<Transaction>, Vec<TransactionCategorisation>) = transform_transactions( - &actualbudget_state.actualbudget_transactions, + &remaining_transactions, &actualbudget_state.actualbudget_category_transfers_account, )?; let category_transfers: Vec<CategoryTransfer> = transform_category_transfers( - &actualbudget_state.actualbudget_transactions, + &remaining_transactions, &actualbudget_state.actualbudget_category_transfers_account, )?; Ok(SchistState { first_transaction_date, accounts, + account_transfers, categories, budget_drips, transactions, |
