From 5bd223ee9c6853502d509627678f696c6ea5fcdc Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Mon, 25 Nov 2024 08:21:29 +0000 Subject: Rename AB transformer --- rust/Cargo.lock | 32 +-- rust/Cargo.toml | 2 +- rust/actualbudget_to_schist_transformer/Cargo.toml | 16 ++ .../src/actualbudget_state.rs | 24 ++ rust/actualbudget_to_schist_transformer/src/lib.rs | 4 + .../actualbudget_to_schist_transformer/src/main.rs | 5 + rust/actualbudget_to_schist_transformer/src/run.rs | 15 ++ .../src/schist_state.rs | 32 +++ .../src/transform_state/is_category_transfer.rs | 18 ++ .../src/transform_state/mod.rs | 20 ++ .../src/transform_state/transform_accounts.rs | 27 ++ .../src/transform_state/transform_budget_drips.rs | 283 +++++++++++++++++++++ .../src/transform_state/transform_categories.rs | 38 +++ .../transform_category_transfers.rs | 73 ++++++ .../src/transform_state/transform_date.rs | 15 ++ .../src/transform_state/transform_id.rs | 17 ++ .../src/transform_state/transform_state.rs | 35 +++ .../src/transform_state/transform_transactions.rs | 93 +++++++ rust/schist_actualbudget_importer/Cargo.toml | 16 -- .../src/actualbudget_state.rs | 24 -- rust/schist_actualbudget_importer/src/lib.rs | 4 - rust/schist_actualbudget_importer/src/main.rs | 5 - rust/schist_actualbudget_importer/src/run.rs | 15 -- .../src/schist_state.rs | 32 --- .../src/transform_state/is_category_transfer.rs | 18 -- .../src/transform_state/mod.rs | 20 -- .../src/transform_state/transform_accounts.rs | 27 -- .../src/transform_state/transform_budget_drips.rs | 283 --------------------- .../src/transform_state/transform_categories.rs | 38 --- .../transform_category_transfers.rs | 73 ------ .../src/transform_state/transform_date.rs | 15 -- .../src/transform_state/transform_id.rs | 17 -- .../src/transform_state/transform_state.rs | 35 --- .../src/transform_state/transform_transactions.rs | 93 ------- 34 files changed, 732 insertions(+), 732 deletions(-) create mode 100644 rust/actualbudget_to_schist_transformer/Cargo.toml create mode 100644 rust/actualbudget_to_schist_transformer/src/actualbudget_state.rs create mode 100644 rust/actualbudget_to_schist_transformer/src/lib.rs create mode 100644 rust/actualbudget_to_schist_transformer/src/main.rs create mode 100644 rust/actualbudget_to_schist_transformer/src/run.rs create mode 100644 rust/actualbudget_to_schist_transformer/src/schist_state.rs create mode 100644 rust/actualbudget_to_schist_transformer/src/transform_state/is_category_transfer.rs create mode 100644 rust/actualbudget_to_schist_transformer/src/transform_state/mod.rs create mode 100644 rust/actualbudget_to_schist_transformer/src/transform_state/transform_accounts.rs create mode 100644 rust/actualbudget_to_schist_transformer/src/transform_state/transform_budget_drips.rs create mode 100644 rust/actualbudget_to_schist_transformer/src/transform_state/transform_categories.rs create mode 100644 rust/actualbudget_to_schist_transformer/src/transform_state/transform_category_transfers.rs create mode 100644 rust/actualbudget_to_schist_transformer/src/transform_state/transform_date.rs create mode 100644 rust/actualbudget_to_schist_transformer/src/transform_state/transform_id.rs create mode 100644 rust/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs create mode 100644 rust/actualbudget_to_schist_transformer/src/transform_state/transform_transactions.rs delete mode 100644 rust/schist_actualbudget_importer/Cargo.toml delete mode 100644 rust/schist_actualbudget_importer/src/actualbudget_state.rs delete mode 100644 rust/schist_actualbudget_importer/src/lib.rs delete mode 100644 rust/schist_actualbudget_importer/src/main.rs delete mode 100644 rust/schist_actualbudget_importer/src/run.rs delete mode 100644 rust/schist_actualbudget_importer/src/schist_state.rs delete mode 100644 rust/schist_actualbudget_importer/src/transform_state/is_category_transfer.rs delete mode 100644 rust/schist_actualbudget_importer/src/transform_state/mod.rs delete mode 100644 rust/schist_actualbudget_importer/src/transform_state/transform_accounts.rs delete mode 100644 rust/schist_actualbudget_importer/src/transform_state/transform_budget_drips.rs delete mode 100644 rust/schist_actualbudget_importer/src/transform_state/transform_categories.rs delete mode 100644 rust/schist_actualbudget_importer/src/transform_state/transform_category_transfers.rs delete mode 100644 rust/schist_actualbudget_importer/src/transform_state/transform_date.rs delete mode 100644 rust/schist_actualbudget_importer/src/transform_state/transform_id.rs delete mode 100644 rust/schist_actualbudget_importer/src/transform_state/transform_state.rs delete mode 100644 rust/schist_actualbudget_importer/src/transform_state/transform_transactions.rs (limited to 'rust') diff --git a/rust/Cargo.lock b/rust/Cargo.lock index ea2b78a..1acaf33 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -36,6 +36,22 @@ dependencies = [ "actualbudget_models", ] +[[package]] +name = "actualbudget_to_schist_transformer" +version = "0.1.0" +dependencies = [ + "actualbudget_fakes", + "actualbudget_models", + "actualbudget_queries", + "anyhow", + "diesel", + "itertools", + "libsqlite3-sys", + "schist_models", + "schist_queries", + "schist_traits", +] + [[package]] name = "addr2line" version = "0.24.2" @@ -1329,22 +1345,6 @@ dependencies = [ "parking_lot", ] -[[package]] -name = "schist_actualbudget_importer" -version = "0.1.0" -dependencies = [ - "actualbudget_fakes", - "actualbudget_models", - "actualbudget_queries", - "anyhow", - "diesel", - "itertools", - "libsqlite3-sys", - "schist_models", - "schist_queries", - "schist_traits", -] - [[package]] name = "schist_backend" version = "0.1.0" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index ee7450f..9162889 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -4,7 +4,7 @@ members = [ "actualbudget_models", "actualbudget_queries", "actualbudget_schema", - "schist_actualbudget_importer", + "actualbudget_to_schist_transformer", "schist_backend", "schist_fakes", "schist_models", diff --git a/rust/actualbudget_to_schist_transformer/Cargo.toml b/rust/actualbudget_to_schist_transformer/Cargo.toml new file mode 100644 index 0000000..f892bde --- /dev/null +++ b/rust/actualbudget_to_schist_transformer/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "actualbudget_to_schist_transformer" +version = "0.1.0" +edition = "2021" + +[dependencies] +actualbudget_fakes = { path = "../actualbudget_fakes" } +actualbudget_models = { path = "../actualbudget_models" } +actualbudget_queries = { path = "../actualbudget_queries" } +anyhow = { workspace = true } +diesel = { workspace = true, features = ["sqlite"] } +itertools = { workspace = true } +libsqlite3-sys = { workspace = true, features = ["bundled"] } +schist_models = { path = "../schist_models" } +schist_queries = { path = "../schist_queries" } +schist_traits = { path = "../schist_traits" } diff --git a/rust/actualbudget_to_schist_transformer/src/actualbudget_state.rs b/rust/actualbudget_to_schist_transformer/src/actualbudget_state.rs new file mode 100644 index 0000000..73698a5 --- /dev/null +++ b/rust/actualbudget_to_schist_transformer/src/actualbudget_state.rs @@ -0,0 +1,24 @@ +use actualbudget_models::{ActualbudgetAccount, ActualbudgetCategory, ActualbudgetDate, ActualbudgetTransaction, 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, + pub actualbudget_categories: Vec, + pub actualbudget_zero_budgets: Vec, + pub actualbudget_category_transfers_account: Option, + pub actualbudget_transactions: Vec, +} + +pub fn read_actualbudget_state(actualbudget_connection: &mut SqliteConnection) -> Result { + 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/actualbudget_to_schist_transformer/src/lib.rs b/rust/actualbudget_to_schist_transformer/src/lib.rs new file mode 100644 index 0000000..99bd311 --- /dev/null +++ b/rust/actualbudget_to_schist_transformer/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/actualbudget_to_schist_transformer/src/main.rs b/rust/actualbudget_to_schist_transformer/src/main.rs new file mode 100644 index 0000000..987cd72 --- /dev/null +++ b/rust/actualbudget_to_schist_transformer/src/main.rs @@ -0,0 +1,5 @@ +use anyhow::Result; + +fn main() -> Result<()> { + actualbudget_to_schist_transformer::run::run() +} diff --git a/rust/actualbudget_to_schist_transformer/src/run.rs b/rust/actualbudget_to_schist_transformer/src/run.rs new file mode 100644 index 0000000..77f0e45 --- /dev/null +++ b/rust/actualbudget_to_schist_transformer/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/actualbudget_to_schist_transformer/src/schist_state.rs b/rust/actualbudget_to_schist_transformer/src/schist_state.rs new file mode 100644 index 0000000..92c97b3 --- /dev/null +++ b/rust/actualbudget_to_schist_transformer/src/schist_state.rs @@ -0,0 +1,32 @@ +use anyhow::Result; +use diesel::SqliteConnection; +use schist_models::{ + account::Account, + budget_drip::BudgetDrip, + category::Category, + category_transfer::CategoryTransfer, + datetime_utc::DatetimeUtc, + transaction::Transaction, + transaction_categorisation::TransactionCategorisation, +}; + +pub struct SchistState { + pub first_transaction_date: DatetimeUtc, + pub accounts: Vec, + pub categories: Vec, + pub budget_drips: Vec, + pub transactions: Vec, + pub transaction_categorisations: Vec, + pub category_transfers: Vec, +} + +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_drips::insert_budget_drips(&schist_state.budget_drips, 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/actualbudget_to_schist_transformer/src/transform_state/is_category_transfer.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/is_category_transfer.rs new file mode 100644 index 0000000..c82387c --- /dev/null +++ b/rust/actualbudget_to_schist_transformer/src/transform_state/is_category_transfer.rs @@ -0,0 +1,18 @@ +use actualbudget_models::{ActualbudgetAccount, ActualbudgetTransaction}; + +pub fn is_category_transfer( + transaction: &ActualbudgetTransaction, + actualbudget_category_transfers_account: &Option +) -> 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 +) -> bool { + !is_category_transfer(transaction, actualbudget_category_transfers_account) +} diff --git a/rust/actualbudget_to_schist_transformer/src/transform_state/mod.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/mod.rs new file mode 100644 index 0000000..916894a --- /dev/null +++ b/rust/actualbudget_to_schist_transformer/src/transform_state/mod.rs @@ -0,0 +1,20 @@ +mod is_category_transfer; +mod transform_accounts; +mod transform_budget_drips; +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_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/actualbudget_to_schist_transformer/src/transform_state/transform_accounts.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_accounts.rs new file mode 100644 index 0000000..917c62d --- /dev/null +++ b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_accounts.rs @@ -0,0 +1,27 @@ +use actualbudget_models::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 { + 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(), + } +} diff --git a/rust/actualbudget_to_schist_transformer/src/transform_state/transform_budget_drips.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_budget_drips.rs new file mode 100644 index 0000000..2e9125b --- /dev/null +++ b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_budget_drips.rs @@ -0,0 +1,283 @@ +use actualbudget_models::ActualbudgetZeroBudget; +use anyhow::{Context, Result}; +use itertools::Itertools; +use schist_models::{budget_drip::BudgetDrip, date_utc::DateUtc}; +use schist_traits::dateable::Dateable; + +use super::{transform_date::transform_date, transform_id::transform_id}; + +pub fn transform_budget_drips(actualbudget_zero_budgets: &[ActualbudgetZeroBudget]) -> Result> { + let budget_drips = group_actualbudget_zero_budgets_by_category_id(actualbudget_zero_budgets) + .iter() + .map(sort_actualbudget_zero_budgets_by_month) + .flat_map(|sorted_actualbudget_zero_budgets| sorted_actualbudget_zero_budgets + .iter() + .map(map_actualbudget_zero_budget_to_budget_drips) + .collect::>>>() + ).flatten() + .flatten() + .collect(); + Ok(budget_drips) +} + +fn map_actualbudget_zero_budget_to_budget_drips( + actualbudget_zero_budget: &ActualbudgetZeroBudget, +) -> Result> { + let context = || "failed to map actualbudget zero budgets to budget drips"; + + let first_day_of_month: DateUtc = Into::::into(transform_date(&actualbudget_zero_budget.month).with_context(context)?); + let month = first_day_of_month.month(); + let days_in_month: Vec = (0..31) + .map(|i| first_day_of_month.add_days(i)) + .take_while(|d| match d { + Err(_) => true, + Ok(d) => d.month() == month, + }) + .filter(Result::is_ok) + .map(Result::unwrap) + .collect(); + + // unwrap() can panic, but days_in_month is guaranteed to have between 0 and + // 31 elements, so we should be grand + let num_days_in_month: i32 = days_in_month.len().try_into().unwrap(); + + let quantity = actualbudget_zero_budget.amount / num_days_in_month; + let remainder_quantity = actualbudget_zero_budget.amount - (quantity * num_days_in_month); + + let budget_drips: Vec = days_in_month.iter().map(|&date| + BudgetDrip { + id: transform_id(&actualbudget_zero_budget.id), + category_id: transform_id(&actualbudget_zero_budget.category_id), + date, + quantity: if date == first_day_of_month { + quantity + remainder_quantity + } else { + quantity + } + } + ).collect(); + + Ok(budget_drips) +} + +fn sort_actualbudget_zero_budgets_by_month( + actualbudget_zero_budgets_by_category_id: &Vec, +) -> Vec { + let mut actualbudget_zero_budgets = actualbudget_zero_budgets_by_category_id.clone(); + actualbudget_zero_budgets.sort_by_key(|zb| zb.month.clone()); + actualbudget_zero_budgets +} + +fn group_actualbudget_zero_budgets_by_category_id( + actualbudget_zero_budgets: &[ActualbudgetZeroBudget], +) -> Vec> { + let category_ids = actualbudget_zero_budgets + .iter() + .map(|zb| zb.category_id.clone()) + .unique(); + + category_ids + .map(|category_id| actualbudget_zero_budgets + .iter() + .filter(|zb| zb.category_id == category_id) + .map(ActualbudgetZeroBudget::clone) + .collect() + ).collect() +} + +#[cfg(test)] +mod test { + use actualbudget_fakes::actualbudget_zero_budget::make_fake_actualbudget_zero_budget_builder; + use actualbudget_models::ActualbudgetDate; + use schist_models::budget_drip::BudgetDrip; + use schist_traits::dateable::Dateable; + + use crate::transform_state::transform_id::transform_id; + + use super::transform_budget_drips; + + mod map_actualbudget_zero_budget_to_budget_drips { + use actualbudget_fakes::actualbudget_zero_budget::make_fake_actualbudget_zero_budget_builder; + use actualbudget_models::ActualbudgetDate; + use schist_models::budget_drip::BudgetDrip; + use schist_traits::dateable::Dateable; + + use crate::transform_state::transform_budget_drips::map_actualbudget_zero_budget_to_budget_drips; + + #[test] + fn when_zero_budget_with_integer_budget_per_day_then_evenly_divided_budget_drips() { + let actualbudget_zero_budget = make_fake_actualbudget_zero_budget_builder("0") + .amount(31 * 100) + .category_id(String::from("0")) + .month(ActualbudgetDate::from_i32(202401)) + .build() + .unwrap(); + + let result = map_actualbudget_zero_budget_to_budget_drips(&actualbudget_zero_budget); + + assert!(result.is_ok()); + let mut budget_drips = result.unwrap(); + assert_eq!(31, budget_drips.len()); + assert!(budget_drips.iter().all(|bd| bd.quantity == 100)); + budget_drips.sort_by_key(|bd| bd.date); + for i in 1..31 { + assert_eq!(Ok(i), budget_drips.get(i - 1).unwrap().date.day().try_into()); + } + } + + #[test] + fn when_zero_budget_with_non_integer_budget_per_day_then_remainder_on_one_day() { + let actualbudget_zero_budget = make_fake_actualbudget_zero_budget_builder("0") + .amount(31 * 100 + 30) + .category_id(String::from("0")) + .month(ActualbudgetDate::from_i32(202401)) + .build() + .unwrap(); + + let result = map_actualbudget_zero_budget_to_budget_drips(&actualbudget_zero_budget); + + assert!(result.is_ok()); + let budget_drips = result.unwrap(); + assert_eq!(31, budget_drips.len()); + + let budget_drips_quantity_not_floored: Vec<&BudgetDrip> = budget_drips + .iter() + .filter(|bd| bd.quantity != 100) + .collect(); + assert_eq!(1, budget_drips_quantity_not_floored.len()); + + let budget_drips_floored: Vec<&BudgetDrip> = budget_drips + .iter() + .filter(|bd| bd.quantity == 100) + .collect(); + assert_eq!(30, budget_drips_floored.len()); + } + } + + #[test] + fn when_no_zero_budgets_then_no_budget_drips() { + let actualbudget_zero_budgets = []; + let result = transform_budget_drips(&actualbudget_zero_budgets); + assert!(result.is_ok()); + assert_eq!(result.unwrap().len(), 0); + } + + #[test] + fn when_one_zero_budget_with_integer_budget_per_day_then_evenly_divided_budget_drips() { + let actualbudget_zero_budgets = [ + make_fake_actualbudget_zero_budget_builder("0") + .amount(31 * 100) + .category_id(String::from("0")) + .month(ActualbudgetDate::from_i32(202401)) + .build() + .unwrap() + ]; + + let result = transform_budget_drips(&actualbudget_zero_budgets); + + assert!(result.is_ok()); + let mut budget_drips = result.unwrap(); + assert_eq!(31, budget_drips.len()); + assert!(budget_drips.iter().all(|bd| bd.quantity == 100)); + budget_drips.sort_by_key(|bd| bd.date); + for i in 1..31 { + assert_eq!(Ok(i), budget_drips.get(i - 1).unwrap().date.day().try_into()); + } + } + + #[test] + fn when_one_zero_budgets_with_non_integer_budget_per_day_then_remainder_on_one_day() { + let actualbudget_zero_budgets = [ + make_fake_actualbudget_zero_budget_builder("0") + .amount(31 * 100 + 30) + .category_id(String::from("0")) + .month(ActualbudgetDate::from_i32(202401)) + .build() + .unwrap() + ]; + + let result = transform_budget_drips(&actualbudget_zero_budgets); + + assert!(result.is_ok()); + let budget_drips = result.unwrap(); + assert_eq!(31, budget_drips.len()); + + let budget_drips_quantity_not_floored: Vec<&BudgetDrip> = budget_drips + .iter() + .filter(|bd| bd.quantity != 100) + .collect(); + assert_eq!(1, budget_drips_quantity_not_floored.len()); + + let budget_drips_floored: Vec<&BudgetDrip> = budget_drips + .iter() + .filter(|bd| bd.quantity == 100) + .collect(); + assert_eq!(30, budget_drips_floored.len()); + } + + #[test] + fn when_multiple_zero_budgets_with_integer_budget_per_day_across_multiple_categories_then_evenly_divided_budget_drips() { + let actualbudget_zero_budgets = [ + make_fake_actualbudget_zero_budget_builder("0") + .amount(31 * 100) + .category_id(String::from("0")) + .month(ActualbudgetDate::from_i32(202401)) + .build() + .unwrap(), + make_fake_actualbudget_zero_budget_builder("1") + .amount(31 * 200) + .category_id(String::from("1")) + .month(ActualbudgetDate::from_i32(202401)) + .build() + .unwrap(), + make_fake_actualbudget_zero_budget_builder("2") + .amount(29 * 110) + .category_id(String::from("0")) + .month(ActualbudgetDate::from_i32(202402)) + .build() + .unwrap(), + make_fake_actualbudget_zero_budget_builder("3") + .amount(29 * 210) + .category_id(String::from("1")) + .month(ActualbudgetDate::from_i32(202402)) + .build() + .unwrap(), + ]; + + let result = transform_budget_drips(&actualbudget_zero_budgets); + let category_0_id = transform_id("0"); + let category_1_id = transform_id("1"); + + assert!(result.is_ok()); + let budget_drips = result.unwrap(); + assert_eq!(31 + 31 + 29 + 29, budget_drips.len()); + + let budget_drips_jan_category_0: Vec<&BudgetDrip> = budget_drips + .iter() + .filter(|bd| bd.category_id == category_0_id && bd.date.month() == 1) + .collect(); + assert_eq!(31, budget_drips_jan_category_0.len()); + assert!(budget_drips_jan_category_0.iter().all(|bd| bd.quantity == 100)); + + let budget_drips_jan_category_1: Vec<&BudgetDrip> = budget_drips + .iter() + .filter(|bd| bd.category_id == category_1_id && bd.date.month() == 1) + .collect(); + assert_eq!(31, budget_drips_jan_category_1.len()); + assert!(budget_drips_jan_category_1.iter().all(|bd| bd.quantity == 200)); + + let budget_drips_feb_category_0: Vec<&BudgetDrip> = budget_drips + .iter() + .filter(|bd| bd.category_id == category_0_id && bd.date.month() == 2) + .collect(); + assert_eq!(29, budget_drips_feb_category_0.len()); + assert!(budget_drips_feb_category_0.iter().all(|bd| bd.quantity == 110)); + + let budget_drips_feb_category_1: Vec<&BudgetDrip> = budget_drips + .iter() + .filter(|bd| bd.category_id == category_1_id && bd.date.month() == 2) + .collect(); + assert_eq!(29, budget_drips_feb_category_1.len()); + assert!(budget_drips_feb_category_1.iter().all(|bd| bd.quantity == 210)); + } +} diff --git a/rust/actualbudget_to_schist_transformer/src/transform_state/transform_categories.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_categories.rs new file mode 100644 index 0000000..e0fc687 --- /dev/null +++ b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_categories.rs @@ -0,0 +1,38 @@ +use actualbudget_models::ActualbudgetCategory; +use schist_models::{budget_period_unit::BudgetPeriodUnit, category::Category}; + +use super::transform_id; + +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(), + balance: 0, + balance_date: get_balance_date(), + budget_period: 1, + budget_period_unit: BudgetPeriodUnit::Month, + budget_quantity: get_budget_quantity(), + }) + } + categories +} + +fn get_balance_date() -> schist_models::date_utc::DateUtc { + todo!() +} + +fn get_budget_quantity() -> i32 { + todo!() +} + +#[cfg(test)] +mod test { + #[test] + fn todo() { + todo!() + } +} diff --git a/rust/actualbudget_to_schist_transformer/src/transform_state/transform_category_transfers.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_category_transfers.rs new file mode 100644 index 0000000..14b34f2 --- /dev/null +++ b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_category_transfers.rs @@ -0,0 +1,73 @@ +use actualbudget_models::{ + ActualbudgetAccount, + 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, +) -> Vec { + let mut category_transfers = Vec::::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 +} + +#[cfg(test)] +mod test { + #[test] + fn todo() { + todo!() + } +} diff --git a/rust/actualbudget_to_schist_transformer/src/transform_state/transform_date.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_date.rs new file mode 100644 index 0000000..d45b8bb --- /dev/null +++ b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_date.rs @@ -0,0 +1,15 @@ +use anyhow::Result; +use actualbudget_models::ActualbudgetDate; +use schist_models::datetime_utc::DatetimeUtc; + +pub fn transform_date(actualbudget_date: &ActualbudgetDate) -> Result { + actualbudget_date.to_iso().parse() +} + +#[cfg(test)] +mod test { + #[test] + fn todo() { + todo!() + } +} diff --git a/rust/actualbudget_to_schist_transformer/src/transform_state/transform_id.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_id.rs new file mode 100644 index 0000000..0b2102b --- /dev/null +++ b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_id.rs @@ -0,0 +1,17 @@ +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::::try_into(i32::MAX).unwrap()) + .try_into() + .unwrap() +} + +#[cfg(test)] +mod test { + #[test] + fn todo() { + todo!() + } +} 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 new file mode 100644 index 0000000..2285276 --- /dev/null +++ b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs @@ -0,0 +1,35 @@ +use anyhow::Result; +use schist_models::{account::Account, category::Category, category_transfer::CategoryTransfer, transaction::Transaction, transaction_categorisation::TransactionCategorisation}; + +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}; + +pub fn transform_state(actualbudget_state: ActualbudgetState) -> Result { + let first_transaction_date = + transform_date(&actualbudget_state.actualbudget_first_transaction_date)?; + let accounts: Vec = + transform_accounts(&actualbudget_state.actualbudget_accounts, &first_transaction_date); + let budget_drips = transform_budget_drips(&actualbudget_state.actualbudget_zero_budgets)?; + let categories: Vec = + transform_categories(&actualbudget_state.actualbudget_categories); + let (transactions, transaction_categorisations): (Vec, Vec) = + transform_transactions( + &actualbudget_state.actualbudget_transactions, + &actualbudget_state.actualbudget_category_transfers_account, + )?; + let category_transfers: Vec = transform_category_transfers( + &actualbudget_state.actualbudget_transactions, + &actualbudget_state.actualbudget_category_transfers_account, + ); + + Ok(SchistState { + first_transaction_date, + accounts, + categories, + budget_drips, + transactions, + transaction_categorisations, + category_transfers, + }) +} diff --git a/rust/actualbudget_to_schist_transformer/src/transform_state/transform_transactions.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_transactions.rs new file mode 100644 index 0000000..3fbcb0f --- /dev/null +++ b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_transactions.rs @@ -0,0 +1,93 @@ +use anyhow::Result; +use actualbudget_models::{ActualbudgetAccount, 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, +) -> 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(|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)) +} + +#[cfg(test)] +mod test { + #[test] + fn todo() { + todo!() + } +} diff --git a/rust/schist_actualbudget_importer/Cargo.toml b/rust/schist_actualbudget_importer/Cargo.toml deleted file mode 100644 index a4bfbd1..0000000 --- a/rust/schist_actualbudget_importer/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "schist_actualbudget_importer" -version = "0.1.0" -edition = "2021" - -[dependencies] -actualbudget_fakes = { path = "../actualbudget_fakes" } -actualbudget_models = { path = "../actualbudget_models" } -actualbudget_queries = { path = "../actualbudget_queries" } -anyhow = { workspace = true } -diesel = { workspace = true, features = ["sqlite"] } -itertools = { workspace = true } -libsqlite3-sys = { workspace = true, features = ["bundled"] } -schist_models = { path = "../schist_models" } -schist_queries = { path = "../schist_queries" } -schist_traits = { path = "../schist_traits" } diff --git a/rust/schist_actualbudget_importer/src/actualbudget_state.rs b/rust/schist_actualbudget_importer/src/actualbudget_state.rs deleted file mode 100644 index 73698a5..0000000 --- a/rust/schist_actualbudget_importer/src/actualbudget_state.rs +++ /dev/null @@ -1,24 +0,0 @@ -use actualbudget_models::{ActualbudgetAccount, ActualbudgetCategory, ActualbudgetDate, ActualbudgetTransaction, 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, - pub actualbudget_categories: Vec, - pub actualbudget_zero_budgets: Vec, - pub actualbudget_category_transfers_account: Option, - pub actualbudget_transactions: Vec, -} - -pub fn read_actualbudget_state(actualbudget_connection: &mut SqliteConnection) -> Result { - 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 deleted file mode 100644 index 99bd311..0000000 --- a/rust/schist_actualbudget_importer/src/lib.rs +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index 00e53c1..0000000 --- a/rust/schist_actualbudget_importer/src/main.rs +++ /dev/null @@ -1,5 +0,0 @@ -use anyhow::Result; - -fn main() -> Result<()> { - schist_actualbudget_importer::run::run() -} diff --git a/rust/schist_actualbudget_importer/src/run.rs b/rust/schist_actualbudget_importer/src/run.rs deleted file mode 100644 index 77f0e45..0000000 --- a/rust/schist_actualbudget_importer/src/run.rs +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index 92c97b3..0000000 --- a/rust/schist_actualbudget_importer/src/schist_state.rs +++ /dev/null @@ -1,32 +0,0 @@ -use anyhow::Result; -use diesel::SqliteConnection; -use schist_models::{ - account::Account, - budget_drip::BudgetDrip, - category::Category, - category_transfer::CategoryTransfer, - datetime_utc::DatetimeUtc, - transaction::Transaction, - transaction_categorisation::TransactionCategorisation, -}; - -pub struct SchistState { - pub first_transaction_date: DatetimeUtc, - pub accounts: Vec, - pub categories: Vec, - pub budget_drips: Vec, - pub transactions: Vec, - pub transaction_categorisations: Vec, - pub category_transfers: Vec, -} - -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_drips::insert_budget_drips(&schist_state.budget_drips, 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 deleted file mode 100644 index c82387c..0000000 --- a/rust/schist_actualbudget_importer/src/transform_state/is_category_transfer.rs +++ /dev/null @@ -1,18 +0,0 @@ -use actualbudget_models::{ActualbudgetAccount, ActualbudgetTransaction}; - -pub fn is_category_transfer( - transaction: &ActualbudgetTransaction, - actualbudget_category_transfers_account: &Option -) -> 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 -) -> 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 deleted file mode 100644 index 916894a..0000000 --- a/rust/schist_actualbudget_importer/src/transform_state/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -mod is_category_transfer; -mod transform_accounts; -mod transform_budget_drips; -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_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 deleted file mode 100644 index 917c62d..0000000 --- a/rust/schist_actualbudget_importer/src/transform_state/transform_accounts.rs +++ /dev/null @@ -1,27 +0,0 @@ -use actualbudget_models::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 { - 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(), - } -} diff --git a/rust/schist_actualbudget_importer/src/transform_state/transform_budget_drips.rs b/rust/schist_actualbudget_importer/src/transform_state/transform_budget_drips.rs deleted file mode 100644 index 2e9125b..0000000 --- a/rust/schist_actualbudget_importer/src/transform_state/transform_budget_drips.rs +++ /dev/null @@ -1,283 +0,0 @@ -use actualbudget_models::ActualbudgetZeroBudget; -use anyhow::{Context, Result}; -use itertools::Itertools; -use schist_models::{budget_drip::BudgetDrip, date_utc::DateUtc}; -use schist_traits::dateable::Dateable; - -use super::{transform_date::transform_date, transform_id::transform_id}; - -pub fn transform_budget_drips(actualbudget_zero_budgets: &[ActualbudgetZeroBudget]) -> Result> { - let budget_drips = group_actualbudget_zero_budgets_by_category_id(actualbudget_zero_budgets) - .iter() - .map(sort_actualbudget_zero_budgets_by_month) - .flat_map(|sorted_actualbudget_zero_budgets| sorted_actualbudget_zero_budgets - .iter() - .map(map_actualbudget_zero_budget_to_budget_drips) - .collect::>>>() - ).flatten() - .flatten() - .collect(); - Ok(budget_drips) -} - -fn map_actualbudget_zero_budget_to_budget_drips( - actualbudget_zero_budget: &ActualbudgetZeroBudget, -) -> Result> { - let context = || "failed to map actualbudget zero budgets to budget drips"; - - let first_day_of_month: DateUtc = Into::::into(transform_date(&actualbudget_zero_budget.month).with_context(context)?); - let month = first_day_of_month.month(); - let days_in_month: Vec = (0..31) - .map(|i| first_day_of_month.add_days(i)) - .take_while(|d| match d { - Err(_) => true, - Ok(d) => d.month() == month, - }) - .filter(Result::is_ok) - .map(Result::unwrap) - .collect(); - - // unwrap() can panic, but days_in_month is guaranteed to have between 0 and - // 31 elements, so we should be grand - let num_days_in_month: i32 = days_in_month.len().try_into().unwrap(); - - let quantity = actualbudget_zero_budget.amount / num_days_in_month; - let remainder_quantity = actualbudget_zero_budget.amount - (quantity * num_days_in_month); - - let budget_drips: Vec = days_in_month.iter().map(|&date| - BudgetDrip { - id: transform_id(&actualbudget_zero_budget.id), - category_id: transform_id(&actualbudget_zero_budget.category_id), - date, - quantity: if date == first_day_of_month { - quantity + remainder_quantity - } else { - quantity - } - } - ).collect(); - - Ok(budget_drips) -} - -fn sort_actualbudget_zero_budgets_by_month( - actualbudget_zero_budgets_by_category_id: &Vec, -) -> Vec { - let mut actualbudget_zero_budgets = actualbudget_zero_budgets_by_category_id.clone(); - actualbudget_zero_budgets.sort_by_key(|zb| zb.month.clone()); - actualbudget_zero_budgets -} - -fn group_actualbudget_zero_budgets_by_category_id( - actualbudget_zero_budgets: &[ActualbudgetZeroBudget], -) -> Vec> { - let category_ids = actualbudget_zero_budgets - .iter() - .map(|zb| zb.category_id.clone()) - .unique(); - - category_ids - .map(|category_id| actualbudget_zero_budgets - .iter() - .filter(|zb| zb.category_id == category_id) - .map(ActualbudgetZeroBudget::clone) - .collect() - ).collect() -} - -#[cfg(test)] -mod test { - use actualbudget_fakes::actualbudget_zero_budget::make_fake_actualbudget_zero_budget_builder; - use actualbudget_models::ActualbudgetDate; - use schist_models::budget_drip::BudgetDrip; - use schist_traits::dateable::Dateable; - - use crate::transform_state::transform_id::transform_id; - - use super::transform_budget_drips; - - mod map_actualbudget_zero_budget_to_budget_drips { - use actualbudget_fakes::actualbudget_zero_budget::make_fake_actualbudget_zero_budget_builder; - use actualbudget_models::ActualbudgetDate; - use schist_models::budget_drip::BudgetDrip; - use schist_traits::dateable::Dateable; - - use crate::transform_state::transform_budget_drips::map_actualbudget_zero_budget_to_budget_drips; - - #[test] - fn when_zero_budget_with_integer_budget_per_day_then_evenly_divided_budget_drips() { - let actualbudget_zero_budget = make_fake_actualbudget_zero_budget_builder("0") - .amount(31 * 100) - .category_id(String::from("0")) - .month(ActualbudgetDate::from_i32(202401)) - .build() - .unwrap(); - - let result = map_actualbudget_zero_budget_to_budget_drips(&actualbudget_zero_budget); - - assert!(result.is_ok()); - let mut budget_drips = result.unwrap(); - assert_eq!(31, budget_drips.len()); - assert!(budget_drips.iter().all(|bd| bd.quantity == 100)); - budget_drips.sort_by_key(|bd| bd.date); - for i in 1..31 { - assert_eq!(Ok(i), budget_drips.get(i - 1).unwrap().date.day().try_into()); - } - } - - #[test] - fn when_zero_budget_with_non_integer_budget_per_day_then_remainder_on_one_day() { - let actualbudget_zero_budget = make_fake_actualbudget_zero_budget_builder("0") - .amount(31 * 100 + 30) - .category_id(String::from("0")) - .month(ActualbudgetDate::from_i32(202401)) - .build() - .unwrap(); - - let result = map_actualbudget_zero_budget_to_budget_drips(&actualbudget_zero_budget); - - assert!(result.is_ok()); - let budget_drips = result.unwrap(); - assert_eq!(31, budget_drips.len()); - - let budget_drips_quantity_not_floored: Vec<&BudgetDrip> = budget_drips - .iter() - .filter(|bd| bd.quantity != 100) - .collect(); - assert_eq!(1, budget_drips_quantity_not_floored.len()); - - let budget_drips_floored: Vec<&BudgetDrip> = budget_drips - .iter() - .filter(|bd| bd.quantity == 100) - .collect(); - assert_eq!(30, budget_drips_floored.len()); - } - } - - #[test] - fn when_no_zero_budgets_then_no_budget_drips() { - let actualbudget_zero_budgets = []; - let result = transform_budget_drips(&actualbudget_zero_budgets); - assert!(result.is_ok()); - assert_eq!(result.unwrap().len(), 0); - } - - #[test] - fn when_one_zero_budget_with_integer_budget_per_day_then_evenly_divided_budget_drips() { - let actualbudget_zero_budgets = [ - make_fake_actualbudget_zero_budget_builder("0") - .amount(31 * 100) - .category_id(String::from("0")) - .month(ActualbudgetDate::from_i32(202401)) - .build() - .unwrap() - ]; - - let result = transform_budget_drips(&actualbudget_zero_budgets); - - assert!(result.is_ok()); - let mut budget_drips = result.unwrap(); - assert_eq!(31, budget_drips.len()); - assert!(budget_drips.iter().all(|bd| bd.quantity == 100)); - budget_drips.sort_by_key(|bd| bd.date); - for i in 1..31 { - assert_eq!(Ok(i), budget_drips.get(i - 1).unwrap().date.day().try_into()); - } - } - - #[test] - fn when_one_zero_budgets_with_non_integer_budget_per_day_then_remainder_on_one_day() { - let actualbudget_zero_budgets = [ - make_fake_actualbudget_zero_budget_builder("0") - .amount(31 * 100 + 30) - .category_id(String::from("0")) - .month(ActualbudgetDate::from_i32(202401)) - .build() - .unwrap() - ]; - - let result = transform_budget_drips(&actualbudget_zero_budgets); - - assert!(result.is_ok()); - let budget_drips = result.unwrap(); - assert_eq!(31, budget_drips.len()); - - let budget_drips_quantity_not_floored: Vec<&BudgetDrip> = budget_drips - .iter() - .filter(|bd| bd.quantity != 100) - .collect(); - assert_eq!(1, budget_drips_quantity_not_floored.len()); - - let budget_drips_floored: Vec<&BudgetDrip> = budget_drips - .iter() - .filter(|bd| bd.quantity == 100) - .collect(); - assert_eq!(30, budget_drips_floored.len()); - } - - #[test] - fn when_multiple_zero_budgets_with_integer_budget_per_day_across_multiple_categories_then_evenly_divided_budget_drips() { - let actualbudget_zero_budgets = [ - make_fake_actualbudget_zero_budget_builder("0") - .amount(31 * 100) - .category_id(String::from("0")) - .month(ActualbudgetDate::from_i32(202401)) - .build() - .unwrap(), - make_fake_actualbudget_zero_budget_builder("1") - .amount(31 * 200) - .category_id(String::from("1")) - .month(ActualbudgetDate::from_i32(202401)) - .build() - .unwrap(), - make_fake_actualbudget_zero_budget_builder("2") - .amount(29 * 110) - .category_id(String::from("0")) - .month(ActualbudgetDate::from_i32(202402)) - .build() - .unwrap(), - make_fake_actualbudget_zero_budget_builder("3") - .amount(29 * 210) - .category_id(String::from("1")) - .month(ActualbudgetDate::from_i32(202402)) - .build() - .unwrap(), - ]; - - let result = transform_budget_drips(&actualbudget_zero_budgets); - let category_0_id = transform_id("0"); - let category_1_id = transform_id("1"); - - assert!(result.is_ok()); - let budget_drips = result.unwrap(); - assert_eq!(31 + 31 + 29 + 29, budget_drips.len()); - - let budget_drips_jan_category_0: Vec<&BudgetDrip> = budget_drips - .iter() - .filter(|bd| bd.category_id == category_0_id && bd.date.month() == 1) - .collect(); - assert_eq!(31, budget_drips_jan_category_0.len()); - assert!(budget_drips_jan_category_0.iter().all(|bd| bd.quantity == 100)); - - let budget_drips_jan_category_1: Vec<&BudgetDrip> = budget_drips - .iter() - .filter(|bd| bd.category_id == category_1_id && bd.date.month() == 1) - .collect(); - assert_eq!(31, budget_drips_jan_category_1.len()); - assert!(budget_drips_jan_category_1.iter().all(|bd| bd.quantity == 200)); - - let budget_drips_feb_category_0: Vec<&BudgetDrip> = budget_drips - .iter() - .filter(|bd| bd.category_id == category_0_id && bd.date.month() == 2) - .collect(); - assert_eq!(29, budget_drips_feb_category_0.len()); - assert!(budget_drips_feb_category_0.iter().all(|bd| bd.quantity == 110)); - - let budget_drips_feb_category_1: Vec<&BudgetDrip> = budget_drips - .iter() - .filter(|bd| bd.category_id == category_1_id && bd.date.month() == 2) - .collect(); - assert_eq!(29, budget_drips_feb_category_1.len()); - assert!(budget_drips_feb_category_1.iter().all(|bd| bd.quantity == 210)); - } -} diff --git a/rust/schist_actualbudget_importer/src/transform_state/transform_categories.rs b/rust/schist_actualbudget_importer/src/transform_state/transform_categories.rs deleted file mode 100644 index e0fc687..0000000 --- a/rust/schist_actualbudget_importer/src/transform_state/transform_categories.rs +++ /dev/null @@ -1,38 +0,0 @@ -use actualbudget_models::ActualbudgetCategory; -use schist_models::{budget_period_unit::BudgetPeriodUnit, category::Category}; - -use super::transform_id; - -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(), - balance: 0, - balance_date: get_balance_date(), - budget_period: 1, - budget_period_unit: BudgetPeriodUnit::Month, - budget_quantity: get_budget_quantity(), - }) - } - categories -} - -fn get_balance_date() -> schist_models::date_utc::DateUtc { - todo!() -} - -fn get_budget_quantity() -> i32 { - todo!() -} - -#[cfg(test)] -mod test { - #[test] - fn todo() { - todo!() - } -} 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 deleted file mode 100644 index 14b34f2..0000000 --- a/rust/schist_actualbudget_importer/src/transform_state/transform_category_transfers.rs +++ /dev/null @@ -1,73 +0,0 @@ -use actualbudget_models::{ - ActualbudgetAccount, - 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, -) -> Vec { - let mut category_transfers = Vec::::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 -} - -#[cfg(test)] -mod test { - #[test] - fn todo() { - todo!() - } -} diff --git a/rust/schist_actualbudget_importer/src/transform_state/transform_date.rs b/rust/schist_actualbudget_importer/src/transform_state/transform_date.rs deleted file mode 100644 index d45b8bb..0000000 --- a/rust/schist_actualbudget_importer/src/transform_state/transform_date.rs +++ /dev/null @@ -1,15 +0,0 @@ -use anyhow::Result; -use actualbudget_models::ActualbudgetDate; -use schist_models::datetime_utc::DatetimeUtc; - -pub fn transform_date(actualbudget_date: &ActualbudgetDate) -> Result { - actualbudget_date.to_iso().parse() -} - -#[cfg(test)] -mod test { - #[test] - fn todo() { - todo!() - } -} diff --git a/rust/schist_actualbudget_importer/src/transform_state/transform_id.rs b/rust/schist_actualbudget_importer/src/transform_state/transform_id.rs deleted file mode 100644 index 0b2102b..0000000 --- a/rust/schist_actualbudget_importer/src/transform_state/transform_id.rs +++ /dev/null @@ -1,17 +0,0 @@ -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::::try_into(i32::MAX).unwrap()) - .try_into() - .unwrap() -} - -#[cfg(test)] -mod test { - #[test] - fn todo() { - todo!() - } -} diff --git a/rust/schist_actualbudget_importer/src/transform_state/transform_state.rs b/rust/schist_actualbudget_importer/src/transform_state/transform_state.rs deleted file mode 100644 index 2285276..0000000 --- a/rust/schist_actualbudget_importer/src/transform_state/transform_state.rs +++ /dev/null @@ -1,35 +0,0 @@ -use anyhow::Result; -use schist_models::{account::Account, category::Category, category_transfer::CategoryTransfer, transaction::Transaction, transaction_categorisation::TransactionCategorisation}; - -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}; - -pub fn transform_state(actualbudget_state: ActualbudgetState) -> Result { - let first_transaction_date = - transform_date(&actualbudget_state.actualbudget_first_transaction_date)?; - let accounts: Vec = - transform_accounts(&actualbudget_state.actualbudget_accounts, &first_transaction_date); - let budget_drips = transform_budget_drips(&actualbudget_state.actualbudget_zero_budgets)?; - let categories: Vec = - transform_categories(&actualbudget_state.actualbudget_categories); - let (transactions, transaction_categorisations): (Vec, Vec) = - transform_transactions( - &actualbudget_state.actualbudget_transactions, - &actualbudget_state.actualbudget_category_transfers_account, - )?; - let category_transfers: Vec = transform_category_transfers( - &actualbudget_state.actualbudget_transactions, - &actualbudget_state.actualbudget_category_transfers_account, - ); - - Ok(SchistState { - first_transaction_date, - accounts, - categories, - budget_drips, - 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 deleted file mode 100644 index 3fbcb0f..0000000 --- a/rust/schist_actualbudget_importer/src/transform_state/transform_transactions.rs +++ /dev/null @@ -1,93 +0,0 @@ -use anyhow::Result; -use actualbudget_models::{ActualbudgetAccount, 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, -) -> 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(|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)) -} - -#[cfg(test)] -mod test { - #[test] - fn todo() { - todo!() - } -} -- cgit v1.2.3