diff options
Diffstat (limited to 'rust/schist_actualbudget_importer/src')
16 files changed, 393 insertions, 300 deletions
diff --git a/rust/schist_actualbudget_importer/src/actualbudget_state.rs b/rust/schist_actualbudget_importer/src/actualbudget_state.rs new file mode 100644 index 0000000..a83a289 --- /dev/null +++ b/rust/schist_actualbudget_importer/src/actualbudget_state.rs @@ -0,0 +1,24 @@ +use actualbudget_models::{actualbudget_account::ActualbudgetAccount, actualbudget_category::ActualbudgetCategory, actualbudget_date::ActualbudgetDate, actualbudget_transaction::ActualbudgetTransaction, actualbudget_zero_budget::ActualbudgetZeroBudget}; +use actualbudget_queries::{actualbudget_accounts::{get_all_actualbudget_accounts, get_category_transfers_account}, actualbudget_categories::get_all_actualbudget_categories, actualbudget_transactions::{get_all_actualbudget_transactions, get_first_actualbudget_transaction_date}, actualbudget_zero_budgets::get_all_actualbudget_zero_budgets}; +use anyhow::Result; +use diesel::SqliteConnection; + +pub struct ActualbudgetState { + pub actualbudget_first_transaction_date: ActualbudgetDate, + pub actualbudget_accounts: Vec<ActualbudgetAccount>, + pub actualbudget_categories: Vec<ActualbudgetCategory>, + pub actualbudget_zero_budgets: Vec<ActualbudgetZeroBudget>, + pub actualbudget_category_transfers_account: Option<ActualbudgetAccount>, + pub actualbudget_transactions: Vec<ActualbudgetTransaction>, +} + +pub fn read_actualbudget_state(actualbudget_connection: &mut SqliteConnection) -> Result<ActualbudgetState> { + Ok(ActualbudgetState { + actualbudget_first_transaction_date: get_first_actualbudget_transaction_date(actualbudget_connection)?, + actualbudget_accounts: get_all_actualbudget_accounts(actualbudget_connection)?, + actualbudget_categories: get_all_actualbudget_categories(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/rust/schist_actualbudget_importer/src/lib.rs b/rust/schist_actualbudget_importer/src/lib.rs new file mode 100644 index 0000000..99bd311 --- /dev/null +++ b/rust/schist_actualbudget_importer/src/lib.rs @@ -0,0 +1,4 @@ +pub mod actualbudget_state; +pub mod run; +pub mod schist_state; +pub mod transform_state; diff --git a/rust/schist_actualbudget_importer/src/main.rs b/rust/schist_actualbudget_importer/src/main.rs index 2236f73..00e53c1 100644 --- a/rust/schist_actualbudget_importer/src/main.rs +++ b/rust/schist_actualbudget_importer/src/main.rs @@ -1,47 +1,5 @@ -pub mod transformers; - use anyhow::Result; -use schist_models::{account::Account, budget_update::BudgetUpdate, category::Category, category_transfer::CategoryTransfer, transaction::Transaction, transaction_categorisation::TransactionCategorisation}; -use schist_queries; -use diesel::{Connection, SqliteConnection}; -use actualbudget_queries::{actualbudget_accounts::{get_all_actualbudget_accounts, get_category_transfers_account}, actualbudget_categories::get_all_actualbudget_categories, actualbudget_transactions::{get_all_actualbudget_transactions, get_first_actualbudget_transaction_date}, actualbudget_zero_budgets::get_all_actualbudget_zero_budgets}; fn main() -> Result<()> { - let connection_src = &mut SqliteConnection::establish("in.sqlite").unwrap(); - let connection_dest = &mut SqliteConnection::establish("out.sqlite").unwrap(); - - let actualbudget_first_transaction_date = get_first_actualbudget_transaction_date(connection_src)?; - let actualbudget_accounts = get_all_actualbudget_accounts(connection_src)?; - let actualbudget_categories = get_all_actualbudget_categories(connection_src)?; - let actualbudget_zero_budgets = get_all_actualbudget_zero_budgets(connection_src)?; - let actualbudget_category_transfers_account = get_category_transfers_account(connection_src)?; - let actualbudget_transactions = get_all_actualbudget_transactions(connection_src)?; - - let first_transaction_date = - transformers::transform_date(&actualbudget_first_transaction_date)?; - let accounts: Vec<Account> = - transformers::transform_accounts(&actualbudget_accounts, &first_transaction_date); - let categories: Vec<Category> = - transformers::transform_categories(&actualbudget_categories); - let budget_updates: Vec<BudgetUpdate> = - transformers::transform_budget_updates(&actualbudget_zero_budgets)?; - let (transactions, transaction_categorisations): (Vec<Transaction>, Vec<TransactionCategorisation>) = - transformers::transform_transactions( - &actualbudget_transactions, - &actualbudget_category_transfers_account, - )?; - let category_transfers: Vec<CategoryTransfer> = transformers::transform_category_transfers( - &actualbudget_transactions, - &actualbudget_category_transfers_account, - ); - - schist_queries::clear::clear(connection_dest)?; - schist_queries::accounts::insert_accounts(&accounts, connection_dest)?; - schist_queries::categories::insert_categories(&categories, connection_dest)?; - schist_queries::budget_updates::insert_budget_updates(&budget_updates, connection_dest)?; - schist_queries::transactions::insert_transactions(&transactions, connection_dest)?; - schist_queries::transaction_categorisations::insert_transaction_categorisations(&transaction_categorisations, connection_dest)?; - schist_queries::category_transfers::insert_category_transfers(&category_transfers, connection_dest)?; - - Ok(()) + schist_actualbudget_importer::run::run() } diff --git a/rust/schist_actualbudget_importer/src/run.rs b/rust/schist_actualbudget_importer/src/run.rs new file mode 100644 index 0000000..77f0e45 --- /dev/null +++ b/rust/schist_actualbudget_importer/src/run.rs @@ -0,0 +1,15 @@ +use anyhow::Result; +use diesel::{Connection, SqliteConnection}; + +use crate::{actualbudget_state::read_actualbudget_state, schist_state::export_schist_state, transform_state::transform_state}; + +pub fn run() -> Result<()> { + let actualbudget_connection = &mut SqliteConnection::establish("in.sqlite").unwrap(); + let schist_connection = &mut SqliteConnection::establish("out.sqlite").unwrap(); + + let actualbudget_state = read_actualbudget_state(actualbudget_connection)?; + let schist_state = transform_state(actualbudget_state)?; + export_schist_state(&schist_state, schist_connection)?; + + Ok(()) +} diff --git a/rust/schist_actualbudget_importer/src/schist_state.rs b/rust/schist_actualbudget_importer/src/schist_state.rs new file mode 100644 index 0000000..a42e446 --- /dev/null +++ b/rust/schist_actualbudget_importer/src/schist_state.rs @@ -0,0 +1,24 @@ +use anyhow::Result; +use diesel::SqliteConnection; +use schist_models::{account::Account, budget_update::BudgetUpdate, category::Category, category_transfer::CategoryTransfer, datetime_utc::DatetimeUtc, transaction::Transaction, transaction_categorisation::TransactionCategorisation}; + +pub struct SchistState { + pub first_transaction_date: DatetimeUtc, + pub accounts: Vec<Account>, + pub categories: Vec<Category>, + pub budget_updates: Vec<BudgetUpdate>, + pub transactions: Vec<Transaction>, + pub transaction_categorisations: Vec<TransactionCategorisation>, + pub category_transfers: Vec<CategoryTransfer>, +} + +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::categories::insert_categories(&schist_state.categories, schist_connection)?; + schist_queries::budget_updates::insert_budget_updates(&schist_state.budget_updates, schist_connection)?; + schist_queries::transactions::insert_transactions(&schist_state.transactions, schist_connection)?; + schist_queries::transaction_categorisations::insert_transaction_categorisations(&schist_state.transaction_categorisations, schist_connection)?; + schist_queries::category_transfers::insert_category_transfers(&schist_state.category_transfers, schist_connection)?; + Ok(()) +} diff --git a/rust/schist_actualbudget_importer/src/transform_state/is_category_transfer.rs b/rust/schist_actualbudget_importer/src/transform_state/is_category_transfer.rs new file mode 100644 index 0000000..0f4299a --- /dev/null +++ b/rust/schist_actualbudget_importer/src/transform_state/is_category_transfer.rs @@ -0,0 +1,18 @@ +use actualbudget_models::{actualbudget_account::ActualbudgetAccount, actualbudget_transaction::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/rust/schist_actualbudget_importer/src/transform_state/mod.rs b/rust/schist_actualbudget_importer/src/transform_state/mod.rs new file mode 100644 index 0000000..d82a59e --- /dev/null +++ b/rust/schist_actualbudget_importer/src/transform_state/mod.rs @@ -0,0 +1,21 @@ +mod is_category_transfer; +mod transform_accounts; +mod transform_budget_updates; +mod transform_categories; +mod transform_category_transfers; +mod transform_date; +mod transform_id; +mod transform_state; +mod transform_transactions; + +use is_category_transfer::is_category_transfer; +use is_category_transfer::is_not_category_transfer; +use transform_accounts::transform_accounts; +use transform_budget_updates::transform_budget_updates; +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::*; diff --git a/rust/schist_actualbudget_importer/src/transform_state/transform_accounts.rs b/rust/schist_actualbudget_importer/src/transform_state/transform_accounts.rs new file mode 100644 index 0000000..beb1de7 --- /dev/null +++ b/rust/schist_actualbudget_importer/src/transform_state/transform_accounts.rs @@ -0,0 +1,27 @@ +use actualbudget_models::actualbudget_account::ActualbudgetAccount; +use schist_models::{account::Account, datetime_utc::DatetimeUtc}; + +use super::transform_id; + +pub fn transform_accounts( + actualbudget_accounts: &[ActualbudgetAccount], + first_transaction_date: &DatetimeUtc, +) -> Vec<Account> { + let mut accounts = Vec::<Account>::new(); + for account in actualbudget_accounts { + accounts.push(transform_account(account, first_transaction_date)); + } + accounts +} + +fn transform_account( + actualbudget_account: &ActualbudgetAccount, + opening_date: &DatetimeUtc, +) -> 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/rust/schist_actualbudget_importer/src/transform_state/transform_budget_updates.rs b/rust/schist_actualbudget_importer/src/transform_state/transform_budget_updates.rs new file mode 100644 index 0000000..fc2ee23 --- /dev/null +++ b/rust/schist_actualbudget_importer/src/transform_state/transform_budget_updates.rs @@ -0,0 +1,40 @@ +use anyhow::Result; +use actualbudget_models::actualbudget_zero_budget::ActualbudgetZeroBudget; +use schist_models::{budget_update::BudgetUpdate, date_utc::DateUtc}; + +use super::{transform_date, transform_id}; + +pub fn transform_budget_updates( + actualbudget_zero_budgets: &[ActualbudgetZeroBudget], +) -> Result<Vec<BudgetUpdate>> { + let mut budget_updates = Vec::<BudgetUpdate>::new(); + for zero_budget in actualbudget_zero_budgets { + let first_zero_budget_in_category = get_first_zero_budget_in_category( + actualbudget_zero_budgets, + &zero_budget.category_id.clone(), + ); + if first_zero_budget_in_category.is_some_and(|zb| zb.id == zero_budget.id) { + continue; + } + budget_updates.push(BudgetUpdate { + id: transform_id(&zero_budget.id.clone()), + category_id: transform_id(&zero_budget.category_id.clone()), + date: DateUtc::from(transform_date(&zero_budget.month.clone())?), + new_budget: zero_budget.amount.try_into().unwrap(), + new_period: 30, + }) + } + Ok(budget_updates) +} + +fn get_first_zero_budget_in_category( + actualbudget_zero_budgets: &[ActualbudgetZeroBudget], + actualbudget_category_id: &str, +) -> Option<ActualbudgetZeroBudget> { + let mut category_zero_budgets = actualbudget_zero_budgets.to_vec(); + category_zero_budgets.retain(|zb| zb.category_id == actualbudget_category_id); + category_zero_budgets + .iter() + .min_by_key(|zb| zb.month.clone()) + .cloned() +} diff --git a/rust/schist_actualbudget_importer/src/transform_state/transform_categories.rs b/rust/schist_actualbudget_importer/src/transform_state/transform_categories.rs new file mode 100644 index 0000000..80ff80b --- /dev/null +++ b/rust/schist_actualbudget_importer/src/transform_state/transform_categories.rs @@ -0,0 +1,17 @@ +use actualbudget_models::actualbudget_category::ActualbudgetCategory; +use schist_models::category::Category; + +use super::transform_id; + +pub fn transform_categories( + actualbudget_categories: &[ActualbudgetCategory], +) -> Vec<Category> { + let mut categories = Vec::<Category>::new(); + for actualbudget_category in actualbudget_categories { + categories.push(Category { + id: transform_id(&actualbudget_category.id.clone()), + name: actualbudget_category.name.clone(), + }) + } + categories +} diff --git a/rust/schist_actualbudget_importer/src/transform_state/transform_category_transfers.rs b/rust/schist_actualbudget_importer/src/transform_state/transform_category_transfers.rs new file mode 100644 index 0000000..86ad498 --- /dev/null +++ b/rust/schist_actualbudget_importer/src/transform_state/transform_category_transfers.rs @@ -0,0 +1,65 @@ +use actualbudget_models::{ + actualbudget_account::ActualbudgetAccount, + actualbudget_transaction::ActualbudgetTransaction, +}; +use schist_models::category_transfer::CategoryTransfer; +use core::panic; + +use super::{is_category_transfer, transform_id}; + +pub fn transform_category_transfers( + actualbudget_transactions: &[ActualbudgetTransaction], + actualbudget_category_transfers_account: &Option<ActualbudgetAccount>, +) -> Vec<CategoryTransfer> { + let mut category_transfers = Vec::<CategoryTransfer>::new(); + + let mut actualbudget_category_transfers = actualbudget_transactions.to_vec(); + actualbudget_category_transfers.retain(|transaction| + is_category_transfer(transaction, actualbudget_category_transfers_account) + ); + let mut actualbudget_parent_category_transfers = actualbudget_category_transfers.to_vec(); + actualbudget_parent_category_transfers.retain(|t| t.is_parent); + let mut actualbudget_child_category_transfers = actualbudget_category_transfers.to_vec(); + actualbudget_child_category_transfers.retain(|t| t.is_child); + + for actualbudget_parent_category_transfer in actualbudget_parent_category_transfers { + let mut actualbudget_child_category_transfers = + actualbudget_child_category_transfers.to_vec(); + actualbudget_child_category_transfers.retain(|t| { + t.parent_id + .clone() + .is_some_and(|parent_id| parent_id == actualbudget_parent_category_transfer.id) + }); + + if actualbudget_child_category_transfers.len() != 2 { + panic!("Category transfers should always have exactly two children."); + } + + let category_1 = actualbudget_child_category_transfers[0].clone(); + let category_2 = actualbudget_child_category_transfers[1].clone(); + + // To avoid duplicates + if category_1.amount >= 0 { + continue; + } + + if actualbudget_child_category_transfers[0].amount + != -actualbudget_child_category_transfers[1].amount + { + panic!("Category transfer children should sum to zero."); + } + + category_transfers.push(CategoryTransfer { + id: transform_id(&actualbudget_parent_category_transfer.id), + description: actualbudget_parent_category_transfer.notes.unwrap_or_else(String::new), + quantity: actualbudget_child_category_transfers[0] + .amount + .try_into() + .unwrap(), + from_category_id: transform_id(&category_1.id), + to_category_id: transform_id(&category_2.id), + }); + } + + category_transfers +} diff --git a/rust/schist_actualbudget_importer/src/transform_state/transform_date.rs b/rust/schist_actualbudget_importer/src/transform_state/transform_date.rs new file mode 100644 index 0000000..803f3ed --- /dev/null +++ b/rust/schist_actualbudget_importer/src/transform_state/transform_date.rs @@ -0,0 +1,7 @@ +use anyhow::Result; +use actualbudget_models::actualbudget_date::ActualbudgetDate; +use schist_models::datetime_utc::DatetimeUtc; + +pub fn transform_date(actualbudget_date: &ActualbudgetDate) -> Result<DatetimeUtc> { + actualbudget_date.to_iso().parse() +} diff --git a/rust/schist_actualbudget_importer/src/transform_state/transform_id.rs b/rust/schist_actualbudget_importer/src/transform_state/transform_id.rs new file mode 100644 index 0000000..f370efe --- /dev/null +++ b/rust/schist_actualbudget_importer/src/transform_state/transform_id.rs @@ -0,0 +1,9 @@ +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() +} diff --git a/rust/schist_actualbudget_importer/src/transform_state/transform_state.rs b/rust/schist_actualbudget_importer/src/transform_state/transform_state.rs new file mode 100644 index 0000000..9236532 --- /dev/null +++ b/rust/schist_actualbudget_importer/src/transform_state/transform_state.rs @@ -0,0 +1,36 @@ +use anyhow::Result; +use schist_models::{account::Account, budget_update::BudgetUpdate, category::Category, category_transfer::CategoryTransfer, transaction::Transaction, transaction_categorisation::TransactionCategorisation}; + +use crate::{actualbudget_state::ActualbudgetState, schist_state::SchistState}; + +use super::{transform_accounts, transform_budget_updates, transform_categories, transform_category_transfers, transform_date, transform_transactions}; + +pub fn transform_state(actualbudget_state: ActualbudgetState) -> Result<SchistState> { + let first_transaction_date = + transform_date(&actualbudget_state.actualbudget_first_transaction_date)?; + let accounts: Vec<Account> = + transform_accounts(&actualbudget_state.actualbudget_accounts, &first_transaction_date); + let categories: Vec<Category> = + transform_categories(&actualbudget_state.actualbudget_categories); + let budget_updates: Vec<BudgetUpdate> = + transform_budget_updates(&actualbudget_state.actualbudget_zero_budgets)?; + let (transactions, transaction_categorisations): (Vec<Transaction>, Vec<TransactionCategorisation>) = + transform_transactions( + &actualbudget_state.actualbudget_transactions, + &actualbudget_state.actualbudget_category_transfers_account, + )?; + let category_transfers: Vec<CategoryTransfer> = transform_category_transfers( + &actualbudget_state.actualbudget_transactions, + &actualbudget_state.actualbudget_category_transfers_account, + ); + + Ok(SchistState { + first_transaction_date, + accounts, + categories, + budget_updates, + transactions, + transaction_categorisations, + category_transfers, + }) +} diff --git a/rust/schist_actualbudget_importer/src/transform_state/transform_transactions.rs b/rust/schist_actualbudget_importer/src/transform_state/transform_transactions.rs new file mode 100644 index 0000000..7bb1de9 --- /dev/null +++ b/rust/schist_actualbudget_importer/src/transform_state/transform_transactions.rs @@ -0,0 +1,85 @@ +use anyhow::Result; +use actualbudget_models::{actualbudget_account::ActualbudgetAccount, actualbudget_transaction::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>, +) -> Result<(Vec<Transaction>, Vec<TransactionCategorisation>)> { + let mut transactions = Vec::<Transaction>::new(); + let mut transaction_categorisations = Vec::<TransactionCategorisation>::new(); + + let mut actualbudget_transactions = actualbudget_transactions.to_vec(); + actualbudget_transactions.retain(|transaction| + is_not_category_transfer(transaction, actualbudget_category_transfers_account) + ); + let mut parent_actualbudget_transactions = actualbudget_transactions.to_vec(); + parent_actualbudget_transactions.retain(|t| t.is_parent); + let mut child_actualbudget_transactions = actualbudget_transactions.to_vec(); + child_actualbudget_transactions.retain(|t| t.is_child); + let mut atomic_actualbudget_transactions = actualbudget_transactions.to_vec(); + atomic_actualbudget_transactions.retain(|t| !t.is_parent && !t.is_child); + + for atomic_actualbudget_transaction in atomic_actualbudget_transactions { + let transaction_id = transform_id(&atomic_actualbudget_transaction.id); + + transactions.push(Transaction { + id: transaction_id, + description: atomic_actualbudget_transaction.notes.unwrap_or_else(String::new), + payee: atomic_actualbudget_transaction.payee.unwrap_or_else(String::new), + 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 + .expect("actualbudget transaction should have been atomic, but in fact had no category 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 mut child_actualbudget_transactions = child_actualbudget_transactions.to_vec(); + child_actualbudget_transactions.retain(|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.unwrap_or_else(String::new), + payee: parent_actualbudget_transaction.payee.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 + .expect("actualbudget transaction should have been a child transaction, but in fact had no category ID"); + + transaction_categorisations.push(TransactionCategorisation { + id: transform_id(&child_actualbudget_transaction.id), + description: child_actualbudget_transaction.notes.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)) +} diff --git a/rust/schist_actualbudget_importer/src/transformers.rs b/rust/schist_actualbudget_importer/src/transformers.rs deleted file mode 100644 index 711904b..0000000 --- a/rust/schist_actualbudget_importer/src/transformers.rs +++ /dev/null @@ -1,257 +0,0 @@ -use actualbudget_models::{ - actualbudget_account::ActualbudgetAccount, - actualbudget_category::ActualbudgetCategory, - actualbudget_date::ActualbudgetDate, - actualbudget_transaction::ActualbudgetTransaction, - actualbudget_zero_budget::ActualbudgetZeroBudget, -}; -use anyhow::Result; -use schist_models::{ - account::Account, - budget_update::BudgetUpdate, - category::Category, - category_transfer::CategoryTransfer, - date_utc::DateUtc, - datetime_utc::DatetimeUtc, - transaction::Transaction, - transaction_categorisation::TransactionCategorisation, -}; -use core::panic; -use std::hash::{DefaultHasher, Hash, Hasher}; - -pub fn transform_date(actualbudget_date: &ActualbudgetDate) -> Result<DatetimeUtc> { - actualbudget_date.to_iso().parse() -} - -pub fn transform_accounts( - actualbudget_accounts: &[ActualbudgetAccount], - first_transaction_date: &DatetimeUtc, -) -> Vec<Account> { - let mut accounts = Vec::<Account>::new(); - for account in actualbudget_accounts { - accounts.push(transform_account(account, first_transaction_date)); - } - accounts -} - -fn transform_account( - actualbudget_account: &ActualbudgetAccount, - opening_date: &DatetimeUtc, -) -> Account { - Account { - id: transform_id(&actualbudget_account.id.clone()), - name: actualbudget_account.name.clone(), - opening_balance: 0, - opening_date: opening_date.clone(), - } -} - -pub fn transform_categories( - actualbudget_categories: &[ActualbudgetCategory], -) -> Vec<Category> { - let mut categories = Vec::<Category>::new(); - for actualbudget_category in actualbudget_categories { - categories.push(Category { - id: transform_id(&actualbudget_category.id.clone()), - name: actualbudget_category.name.clone(), - }) - } - categories -} - -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() -} - -pub fn transform_budget_updates( - actualbudget_zero_budgets: &[ActualbudgetZeroBudget], -) -> Result<Vec<BudgetUpdate>> { - let mut budget_updates = Vec::<BudgetUpdate>::new(); - for zero_budget in actualbudget_zero_budgets { - let first_zero_budget_in_category = get_first_zero_budget_in_category( - actualbudget_zero_budgets, - &zero_budget.category_id.clone(), - ); - if first_zero_budget_in_category.is_some_and(|zb| zb.id == zero_budget.id) { - continue; - } - budget_updates.push(BudgetUpdate { - id: transform_id(&zero_budget.id.clone()), - category_id: transform_id(&zero_budget.category_id.clone()), - date: DateUtc::from(transform_date(&zero_budget.month.clone())?), - new_budget: zero_budget.amount.try_into().unwrap(), - new_period: 30, - }) - } - Ok(budget_updates) -} - -fn get_first_zero_budget_in_category( - actualbudget_zero_budgets: &[ActualbudgetZeroBudget], - actualbudget_category_id: &str, -) -> Option<ActualbudgetZeroBudget> { - let mut category_zero_budgets = actualbudget_zero_budgets.to_vec(); - category_zero_budgets.retain(|zb| zb.category_id == actualbudget_category_id); - category_zero_budgets - .iter() - .min_by_key(|zb| zb.month.clone()) - .cloned() -} - -pub fn transform_transactions( - actualbudget_transactions: &[ActualbudgetTransaction], - actualbudget_category_transfers_account: &Option<ActualbudgetAccount>, -) -> Result<(Vec<Transaction>, Vec<TransactionCategorisation>)> { - let mut transactions = Vec::<Transaction>::new(); - let mut transaction_categorisations = Vec::<TransactionCategorisation>::new(); - - let mut actualbudget_transactions = actualbudget_transactions.to_vec(); - actualbudget_transactions.retain(|transaction| - is_not_category_transfer(transaction, actualbudget_category_transfers_account) - ); - let mut parent_actualbudget_transactions = actualbudget_transactions.to_vec(); - parent_actualbudget_transactions.retain(|t| t.is_parent); - let mut child_actualbudget_transactions = actualbudget_transactions.to_vec(); - child_actualbudget_transactions.retain(|t| t.is_child); - let mut atomic_actualbudget_transactions = actualbudget_transactions.to_vec(); - atomic_actualbudget_transactions.retain(|t| !t.is_parent && !t.is_child); - - for atomic_actualbudget_transaction in atomic_actualbudget_transactions { - let transaction_id = transform_id(&atomic_actualbudget_transaction.id); - - transactions.push(Transaction { - id: transaction_id, - description: atomic_actualbudget_transaction.notes.unwrap_or_else(String::new), - payee: atomic_actualbudget_transaction.payee.unwrap_or_else(String::new), - 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 - .expect("actualbudget transaction should have been atomic, but in fact had no category 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 mut child_actualbudget_transactions = child_actualbudget_transactions.to_vec(); - child_actualbudget_transactions.retain(|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.unwrap_or_else(String::new), - payee: parent_actualbudget_transaction.payee.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 - .expect("actualbudget transaction should have been a child transaction, but in fact had no category ID"); - - transaction_categorisations.push(TransactionCategorisation { - id: transform_id(&child_actualbudget_transaction.id), - description: child_actualbudget_transaction.notes.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)) -} - -pub fn transform_category_transfers( - actualbudget_transactions: &[ActualbudgetTransaction], - actualbudget_category_transfers_account: &Option<ActualbudgetAccount>, -) -> Vec<CategoryTransfer> { - let mut category_transfers = Vec::<CategoryTransfer>::new(); - - let mut actualbudget_category_transfers = actualbudget_transactions.to_vec(); - actualbudget_category_transfers.retain(|transaction| - is_category_transfer(transaction, actualbudget_category_transfers_account) - ); - let mut actualbudget_parent_category_transfers = actualbudget_category_transfers.to_vec(); - actualbudget_parent_category_transfers.retain(|t| t.is_parent); - let mut actualbudget_child_category_transfers = actualbudget_category_transfers.to_vec(); - actualbudget_child_category_transfers.retain(|t| t.is_child); - - for actualbudget_parent_category_transfer in actualbudget_parent_category_transfers { - let mut actualbudget_child_category_transfers = - actualbudget_child_category_transfers.to_vec(); - actualbudget_child_category_transfers.retain(|t| { - t.parent_id - .clone() - .is_some_and(|parent_id| parent_id == actualbudget_parent_category_transfer.id) - }); - - if actualbudget_child_category_transfers.len() != 2 { - panic!("Category transfers should always have exactly two children."); - } - - let category_1 = actualbudget_child_category_transfers[0].clone(); - let category_2 = actualbudget_child_category_transfers[1].clone(); - - // To avoid duplicates - if category_1.amount >= 0 { - continue; - } - - if actualbudget_child_category_transfers[0].amount - != -actualbudget_child_category_transfers[1].amount - { - panic!("Category transfer children should sum to zero."); - } - - category_transfers.push(CategoryTransfer { - id: transform_id(&actualbudget_parent_category_transfer.id), - description: actualbudget_parent_category_transfer.notes.unwrap_or_else(String::new), - quantity: actualbudget_child_category_transfers[0] - .amount - .try_into() - .unwrap(), - from_category_id: transform_id(&category_1.id), - to_category_id: transform_id(&category_2.id), - }); - } - - category_transfers -} - -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(), - } -} - -fn is_not_category_transfer( - transaction: &ActualbudgetTransaction, - actualbudget_category_transfers_account: &Option<ActualbudgetAccount> -) -> bool { - !is_category_transfer(transaction, actualbudget_category_transfers_account) -} |
