use anyhow::Result; use schist_core::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 { actualbudget_date.to_iso().parse() } pub fn transform_accounts( actualbudget_accounts: &[ActualbudgetAccount], first_transaction_date: &DatetimeUtc, ) -> Vec { let mut accounts = Vec::::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 { let mut categories = Vec::::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::::try_into(i32::MAX).unwrap()) .try_into() .unwrap() } pub fn transform_budget_updates( actualbudget_zero_budgets: &[ActualbudgetZeroBudget], ) -> Result> { let mut budget_updates = Vec::::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 { 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, Vec)> { let mut transactions = Vec::::new(); let mut transaction_categorisations = Vec::::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 { let mut category_transfers = Vec::::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 }