diff options
| author | Joe Carstairs <me@joeac.net> | 2024-11-17 09:23:28 +0000 |
|---|---|---|
| committer | Joe Carstairs <me@joeac.net> | 2024-11-17 09:23:28 +0000 |
| commit | 1ae19eb3315c40e3f9062592c27fa76c11cf626c (patch) | |
| tree | 09d9ce380795971f4b50e1d4b0ca3c57c9e1db1a /rust/actualbudget_importer/src | |
| parent | 1a494937021561595d0fb29e26fb707aef9e55de (diff) | |
Renames workspace folder
Diffstat (limited to 'rust/actualbudget_importer/src')
20 files changed, 508 insertions, 0 deletions
diff --git a/rust/actualbudget_importer/src/main.rs b/rust/actualbudget_importer/src/main.rs new file mode 100644 index 0000000..cb69bb5 --- /dev/null +++ b/rust/actualbudget_importer/src/main.rs @@ -0,0 +1,45 @@ +pub mod models; +pub mod queries; +pub mod schema; +pub mod transformers; + +use anyhow::Result; +use budgeting_app_core::queries as budgeting_app_queries; +use diesel::{Connection, SqliteConnection}; +use 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 = + transformers::transform_accounts(&actualbudget_accounts, &first_transaction_date); + let categories = transformers::transform_categories(&actualbudget_categories); + let budget_updates = transformers::transform_budget_updates(&actualbudget_zero_budgets)?; + let (transactions, transaction_categorisations) = transformers::transform_transactions( + &actualbudget_transactions, + &actualbudget_category_transfers_account, + )?; + let category_transfers = transformers::transform_category_transfers( + &actualbudget_transactions, + &actualbudget_category_transfers_account, + ); + + budgeting_app_queries::clear::clear(connection_dest)?; + budgeting_app_queries::accounts::insert_accounts(&accounts, connection_dest)?; + budgeting_app_queries::categories::insert_categories(&categories, connection_dest)?; + budgeting_app_queries::budget_updates::insert_budget_updates(&budget_updates, connection_dest)?; + budgeting_app_queries::transactions::insert_transactions(&transactions, connection_dest)?; + budgeting_app_queries::transaction_categorisations::insert_transaction_categorisations(&transaction_categorisations, connection_dest)?; + budgeting_app_queries::category_transfers::insert_category_transfers(&category_transfers, connection_dest)?; + + Ok(()) +} diff --git a/rust/actualbudget_importer/src/models/actualbudget_account.rs b/rust/actualbudget_importer/src/models/actualbudget_account.rs new file mode 100644 index 0000000..b3d00ac --- /dev/null +++ b/rust/actualbudget_importer/src/models/actualbudget_account.rs @@ -0,0 +1,8 @@ +use diesel::prelude::{Queryable, Selectable}; + +#[derive(Queryable, Selectable)] +#[diesel(table_name = crate::schema::accounts)] +pub struct ActualbudgetAccount { + pub id: String, + pub name: String, +} diff --git a/rust/actualbudget_importer/src/models/actualbudget_category.rs b/rust/actualbudget_importer/src/models/actualbudget_category.rs new file mode 100644 index 0000000..cbea99b --- /dev/null +++ b/rust/actualbudget_importer/src/models/actualbudget_category.rs @@ -0,0 +1,8 @@ +use diesel::{prelude::Queryable, Selectable}; + +#[derive(Queryable, Selectable)] +#[diesel(table_name = crate::schema::categories)] +pub struct ActualbudgetCategory { + pub id: String, + pub name: String, +} diff --git a/rust/actualbudget_importer/src/models/actualbudget_date.rs b/rust/actualbudget_importer/src/models/actualbudget_date.rs new file mode 100644 index 0000000..bef160c --- /dev/null +++ b/rust/actualbudget_importer/src/models/actualbudget_date.rs @@ -0,0 +1,75 @@ +use diesel::{backend::Backend, deserialize::{FromSql, FromSqlRow}, expression::AsExpression, sql_types::Integer, sqlite::Sqlite}; + +#[derive(Clone, PartialEq, Eq, Debug, AsExpression, FromSqlRow)] +#[diesel(sql_type=Integer)] +pub struct ActualbudgetDate(i32); + +#[derive(Clone, PartialOrd, PartialEq, Eq)] +enum DatePart { + MonthOfYear, + DayOfMonth, +} + +// actualbudget represents dates as numbers which "look like" the corresponding +// ISO string when written in decimal. E.g. 20240901 represents 1 Sep 2024. It +// also uses month specifiers, e.g. 202409 to represent Sep 2024. In this case, +// we interpret this as the first day of the month, e.g. 2024-09-01. + +impl ActualbudgetDate { + pub fn to_iso(&self) -> String { + format!("{0}-{1}-{2}", self.year(), self.month(), self.day()) + } + + pub fn year(&self) -> String { + self.0.to_string()[0..4].to_string() + } + + pub fn month(&self) -> String { + self.0.to_string()[4..6].to_string() + } + + pub fn day(&self) -> String { + match self.date_part() { + DatePart::DayOfMonth => self.0.to_string()[6..8].to_string(), + DatePart::MonthOfYear => String::from("01"), + } + } + + pub fn from_i32(i: i32) -> Self { + ActualbudgetDate(i) + } + + fn date_part(&self) -> DatePart { + if self.0 < 10000000 { + DatePart::MonthOfYear + } else { + DatePart::DayOfMonth + } + } + + fn dayified_value(&self) -> i32 { + match self.date_part() { + DatePart::DayOfMonth => self.0, + DatePart::MonthOfYear => self.0 * 100 + 1, + } + } +} + +impl Ord for ActualbudgetDate { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.dayified_value().cmp(&other.dayified_value()) + } +} + +impl PartialOrd for ActualbudgetDate { + fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { + Some(self.cmp(other)) + } +} + +impl FromSql<Integer, Sqlite> for ActualbudgetDate { + fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> diesel::deserialize::Result<Self> { + let int: i32 = FromSql::<Integer, Sqlite>::from_sql(bytes)?; + Ok(ActualbudgetDate(int)) + } +} diff --git a/rust/actualbudget_importer/src/models/actualbudget_transaction.rs b/rust/actualbudget_importer/src/models/actualbudget_transaction.rs new file mode 100644 index 0000000..be2ca1a --- /dev/null +++ b/rust/actualbudget_importer/src/models/actualbudget_transaction.rs @@ -0,0 +1,18 @@ +use diesel::prelude::{Identifiable, Queryable, QueryableByName, Selectable}; + +use crate::models::actualbudget_date::ActualbudgetDate; + +#[derive(Identifiable, QueryableByName, Queryable, Selectable, Clone)] +#[diesel(table_name = crate::schema::transactions)] +pub struct ActualbudgetTransaction { + pub id: String, + pub is_parent: bool, + pub is_child: bool, + pub account_id: String, + pub category_id: String, + pub amount: i32, + pub payee: String, + pub notes: String, + pub date: ActualbudgetDate, + pub parent_id: Option<String>, +} diff --git a/rust/actualbudget_importer/src/models/actualbudget_zero_budget.rs b/rust/actualbudget_importer/src/models/actualbudget_zero_budget.rs new file mode 100644 index 0000000..6478d34 --- /dev/null +++ b/rust/actualbudget_importer/src/models/actualbudget_zero_budget.rs @@ -0,0 +1,13 @@ +use crate::models::actualbudget_date::ActualbudgetDate; +use diesel::{prelude::{Identifiable, Queryable, QueryableByName, Selectable}, sqlite::Sqlite}; + +#[derive(Identifiable, QueryableByName, Queryable, Selectable, Clone)] +#[diesel(table_name = crate::schema::zero_budgets)] +#[diesel(check_for_backend(Sqlite))] +pub struct ActualbudgetZeroBudget { + pub id: String, + pub month: ActualbudgetDate, + pub category_id: String, + pub amount: i32, + pub do_carry_over: i32, +} diff --git a/rust/actualbudget_importer/src/models/mod.rs b/rust/actualbudget_importer/src/models/mod.rs new file mode 100644 index 0000000..1c97c65 --- /dev/null +++ b/rust/actualbudget_importer/src/models/mod.rs @@ -0,0 +1,5 @@ +pub mod actualbudget_account; +pub mod actualbudget_category; +pub mod actualbudget_date; +pub mod actualbudget_transaction; +pub mod actualbudget_zero_budget; diff --git a/rust/actualbudget_importer/src/queries/actualbudget_accounts/get_all.rs b/rust/actualbudget_importer/src/queries/actualbudget_accounts/get_all.rs new file mode 100644 index 0000000..6f667de --- /dev/null +++ b/rust/actualbudget_importer/src/queries/actualbudget_accounts/get_all.rs @@ -0,0 +1,6 @@ +use crate::{models::actualbudget_account::ActualbudgetAccount, schema::accounts::dsl::accounts}; +use diesel::{result::Error, QueryDsl, RunQueryDsl, SelectableHelper, SqliteConnection}; + +pub fn get_all_actualbudget_accounts(connection: &mut SqliteConnection) -> Result<Vec<ActualbudgetAccount>, Error> { + accounts.select(ActualbudgetAccount::as_select()).load(connection) +} diff --git a/rust/actualbudget_importer/src/queries/actualbudget_accounts/get_category_transfers_account.rs b/rust/actualbudget_importer/src/queries/actualbudget_accounts/get_category_transfers_account.rs new file mode 100644 index 0000000..f05dc78 --- /dev/null +++ b/rust/actualbudget_importer/src/queries/actualbudget_accounts/get_category_transfers_account.rs @@ -0,0 +1,13 @@ +use diesel::{result::Error, ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper, SqliteConnection}; + +use crate::{models::actualbudget_account::ActualbudgetAccount, schema::accounts::{ + self as accounts_schema, + dsl::accounts as accounts_table, +}}; + +pub fn get_category_transfers_account(connection: &mut SqliteConnection) -> Result<ActualbudgetAccount, Error> { + accounts_table + .filter(accounts_schema::name.eq("Category transfers")) + .select(ActualbudgetAccount::as_select()) + .first(connection) +} diff --git a/rust/actualbudget_importer/src/queries/actualbudget_accounts/mod.rs b/rust/actualbudget_importer/src/queries/actualbudget_accounts/mod.rs new file mode 100644 index 0000000..c6d6015 --- /dev/null +++ b/rust/actualbudget_importer/src/queries/actualbudget_accounts/mod.rs @@ -0,0 +1,5 @@ +mod get_all; +mod get_category_transfers_account; + +pub use get_all::get_all_actualbudget_accounts; +pub use get_category_transfers_account::get_category_transfers_account; diff --git a/rust/actualbudget_importer/src/queries/actualbudget_categories/get_all.rs b/rust/actualbudget_importer/src/queries/actualbudget_categories/get_all.rs new file mode 100644 index 0000000..4cb7ebc --- /dev/null +++ b/rust/actualbudget_importer/src/queries/actualbudget_categories/get_all.rs @@ -0,0 +1,6 @@ +use crate::{models::actualbudget_category::ActualbudgetCategory, schema::categories::dsl::categories}; +use diesel::{result::Error, QueryDsl, RunQueryDsl, SelectableHelper, SqliteConnection}; + +pub fn get_all_actualbudget_categories(connection: &mut SqliteConnection) -> Result<Vec<ActualbudgetCategory>, Error> { + categories.select(ActualbudgetCategory::as_select()).load(connection) +} diff --git a/rust/actualbudget_importer/src/queries/actualbudget_categories/mod.rs b/rust/actualbudget_importer/src/queries/actualbudget_categories/mod.rs new file mode 100644 index 0000000..0c04e53 --- /dev/null +++ b/rust/actualbudget_importer/src/queries/actualbudget_categories/mod.rs @@ -0,0 +1,3 @@ +mod get_all; + +pub use get_all::get_all_actualbudget_categories; diff --git a/rust/actualbudget_importer/src/queries/actualbudget_transactions/get_all.rs b/rust/actualbudget_importer/src/queries/actualbudget_transactions/get_all.rs new file mode 100644 index 0000000..84c8d08 --- /dev/null +++ b/rust/actualbudget_importer/src/queries/actualbudget_transactions/get_all.rs @@ -0,0 +1,6 @@ +use crate::{models::actualbudget_transaction::ActualbudgetTransaction, schema::transactions::dsl::transactions}; +use diesel::{result::Error, QueryDsl, RunQueryDsl, SelectableHelper, SqliteConnection}; + +pub fn get_all_actualbudget_transactions(connection: &mut SqliteConnection) -> Result<Vec<ActualbudgetTransaction>, Error> { + transactions.select(ActualbudgetTransaction::as_select()).load(connection) +} diff --git a/rust/actualbudget_importer/src/queries/actualbudget_transactions/get_first_date.rs b/rust/actualbudget_importer/src/queries/actualbudget_transactions/get_first_date.rs new file mode 100644 index 0000000..b958c37 --- /dev/null +++ b/rust/actualbudget_importer/src/queries/actualbudget_transactions/get_first_date.rs @@ -0,0 +1,14 @@ +use anyhow::Result; +use crate::{models::actualbudget_date::ActualbudgetDate, schema::transactions::{ + self as transactions_schema, + dsl::transactions as transactions_table, +}}; +use diesel::{dsl::min, result::Error, QueryDsl, RunQueryDsl, SqliteConnection}; + +pub fn get_first_actualbudget_transaction_date(connection: &mut SqliteConnection) -> Result<ActualbudgetDate> { + let date_as_int = transactions_table + .select(min(transactions_schema::date)) + .first::<Option<i32>>(connection) + .map_or_else(|e| Err(e), |o| o.map_or_else(|| Err(Error::NotFound), |i| Ok(i)))?; + Ok(ActualbudgetDate::from_i32(date_as_int)) +} diff --git a/rust/actualbudget_importer/src/queries/actualbudget_transactions/mod.rs b/rust/actualbudget_importer/src/queries/actualbudget_transactions/mod.rs new file mode 100644 index 0000000..b4ed0dd --- /dev/null +++ b/rust/actualbudget_importer/src/queries/actualbudget_transactions/mod.rs @@ -0,0 +1,5 @@ +mod get_all; +mod get_first_date; + +pub use get_all::get_all_actualbudget_transactions; +pub use get_first_date::get_first_actualbudget_transaction_date; diff --git a/rust/actualbudget_importer/src/queries/actualbudget_zero_budgets/get_all.rs b/rust/actualbudget_importer/src/queries/actualbudget_zero_budgets/get_all.rs new file mode 100644 index 0000000..0c3f695 --- /dev/null +++ b/rust/actualbudget_importer/src/queries/actualbudget_zero_budgets/get_all.rs @@ -0,0 +1,6 @@ +use crate::{models::actualbudget_zero_budget::ActualbudgetZeroBudget, schema::zero_budgets::dsl::zero_budgets}; +use diesel::{result::Error, QueryDsl, RunQueryDsl, SelectableHelper, SqliteConnection}; + +pub fn get_all_actualbudget_zero_budgets(connection: &mut SqliteConnection) -> Result<Vec<ActualbudgetZeroBudget>, Error> { + zero_budgets.select(ActualbudgetZeroBudget::as_select()).load(connection) +} diff --git a/rust/actualbudget_importer/src/queries/actualbudget_zero_budgets/mod.rs b/rust/actualbudget_importer/src/queries/actualbudget_zero_budgets/mod.rs new file mode 100644 index 0000000..ba7e21b --- /dev/null +++ b/rust/actualbudget_importer/src/queries/actualbudget_zero_budgets/mod.rs @@ -0,0 +1,3 @@ +mod get_all; + +pub use get_all::get_all_actualbudget_zero_budgets; diff --git a/rust/actualbudget_importer/src/queries/mod.rs b/rust/actualbudget_importer/src/queries/mod.rs new file mode 100644 index 0000000..e16dafc --- /dev/null +++ b/rust/actualbudget_importer/src/queries/mod.rs @@ -0,0 +1,4 @@ +pub mod actualbudget_accounts; +pub mod actualbudget_categories; +pub mod actualbudget_transactions; +pub mod actualbudget_zero_budgets; diff --git a/rust/actualbudget_importer/src/schema.rs b/rust/actualbudget_importer/src/schema.rs new file mode 100644 index 0000000..a872231 --- /dev/null +++ b/rust/actualbudget_importer/src/schema.rs @@ -0,0 +1,47 @@ +diesel::table! { + accounts (id) { + id -> Text, + name -> Text, + } +} + +diesel::table! { + categories (id) { + id -> Text, + name -> Text, + } +} + +diesel::table! { + transactions (id) { + id -> Text, + #[sql_name = "isParent"] + is_parent -> Bool, + #[sql_name = "isChild"] + is_child -> Bool, + #[sql_name = "acct"] + account_id -> Text, + #[sql_name = "category"] + category_id -> Text, + amount -> Integer, + #[sql_name = "description"] + payee -> Text, + notes -> Text, + date -> Integer, + parent_id -> Nullable<Text>, + } +} + +diesel::table! { + zero_budgets (id) { + id -> Text, + month -> Integer, + #[sql_name = "category"] + category_id -> Text, + amount -> Integer, + #[sql_name = "carryover"] + do_carry_over -> Integer, + // goal -> Integer, + // long_goal -> Integer, + } +} diff --git a/rust/actualbudget_importer/src/transformers.rs b/rust/actualbudget_importer/src/transformers.rs new file mode 100644 index 0000000..dd4d154 --- /dev/null +++ b/rust/actualbudget_importer/src/transformers.rs @@ -0,0 +1,218 @@ +use anyhow::Result; +use budgeting_app_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<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 +} |
