diff options
| author | Joe Carstairs <me@joeac.net> | 2024-11-17 12:16:19 +0000 |
|---|---|---|
| committer | Joe Carstairs <me@joeac.net> | 2024-11-17 12:20:48 +0000 |
| commit | 99fa72e2b9d572e00cd232bd55a3fa24722c3134 (patch) | |
| tree | 6b50ce600fffa07a0c21b7f14d2447fe5ac3c2ed /rust/schist_actualbudget_importer/src/transformers.rs | |
| parent | 10f071447594f2bdcec84a461df9ad648f71f44b (diff) | |
Refactors workspace
Diffstat (limited to 'rust/schist_actualbudget_importer/src/transformers.rs')
| -rw-r--r-- | rust/schist_actualbudget_importer/src/transformers.rs | 225 |
1 files changed, 225 insertions, 0 deletions
diff --git a/rust/schist_actualbudget_importer/src/transformers.rs b/rust/schist_actualbudget_importer/src/transformers.rs new file mode 100644 index 0000000..d1b1564 --- /dev/null +++ b/rust/schist_actualbudget_importer/src/transformers.rs @@ -0,0 +1,225 @@ +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}; + +use crate::models::{actualbudget_account::ActualbudgetAccount, actualbudget_category::ActualbudgetCategory, actualbudget_date::ActualbudgetDate, actualbudget_transaction::ActualbudgetTransaction, actualbudget_zero_budget::ActualbudgetZeroBudget}; + +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: &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(|t| t.category_id.clone() != actualbudget_category_transfers_account.id.clone()); + 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, + payee: atomic_actualbudget_transaction.payee, + quantity: atomic_actualbudget_transaction.amount.try_into().unwrap(), + date: transform_date(&atomic_actualbudget_transaction.date)?, + account_id: transform_id(&atomic_actualbudget_transaction.account_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(&atomic_actualbudget_transaction.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, + payee: parent_actualbudget_transaction.payee, + 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 { + transaction_categorisations.push(TransactionCategorisation { + id: transform_id(&child_actualbudget_transaction.id), + description: child_actualbudget_transaction.notes, + quantity: child_actualbudget_transaction.amount.try_into().unwrap(), + transaction_id, + category_id: transform_id(&child_actualbudget_transaction.category_id), + }); + } + } + + Ok((transactions, transaction_categorisations)) +} + +pub fn transform_category_transfers( + actualbudget_transactions: &[ActualbudgetTransaction], + actualbudget_category_transfers_account: &ActualbudgetAccount, +) -> Vec<CategoryTransfer> { + let mut category_transfers = Vec::<CategoryTransfer>::new(); + + let mut actualbudget_category_transfers = actualbudget_transactions.to_vec(); + actualbudget_category_transfers + .retain(|t| t.category_id.clone() == actualbudget_category_transfers_account.id.clone()); + 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, + 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 +} |
