summaryrefslogtreecommitdiff
path: root/rust
diff options
context:
space:
mode:
authorJoe Carstairs <me@joeac.net>2024-11-21 22:35:57 +0000
committerJoe Carstairs <me@joeac.net>2024-11-21 22:35:57 +0000
commit6d441ca9318762a19d5e56002a6be6fe78d7262a (patch)
tree56100e914019d9fc32070d020defdda369b39fac /rust
parent11cdd2ad3368e349d1cebe079629af61e8c45d78 (diff)
Refactors actualbudget importer
Diffstat (limited to 'rust')
-rw-r--r--rust/Cargo.lock26
-rw-r--r--rust/schist_actualbudget_importer/Cargo.toml2
-rw-r--r--rust/schist_actualbudget_importer/src/actualbudget_state.rs24
-rw-r--r--rust/schist_actualbudget_importer/src/lib.rs4
-rw-r--r--rust/schist_actualbudget_importer/src/main.rs44
-rw-r--r--rust/schist_actualbudget_importer/src/run.rs15
-rw-r--r--rust/schist_actualbudget_importer/src/schist_state.rs24
-rw-r--r--rust/schist_actualbudget_importer/src/transform_state/is_category_transfer.rs18
-rw-r--r--rust/schist_actualbudget_importer/src/transform_state/mod.rs21
-rw-r--r--rust/schist_actualbudget_importer/src/transform_state/transform_accounts.rs27
-rw-r--r--rust/schist_actualbudget_importer/src/transform_state/transform_budget_updates.rs40
-rw-r--r--rust/schist_actualbudget_importer/src/transform_state/transform_categories.rs17
-rw-r--r--rust/schist_actualbudget_importer/src/transform_state/transform_category_transfers.rs65
-rw-r--r--rust/schist_actualbudget_importer/src/transform_state/transform_date.rs7
-rw-r--r--rust/schist_actualbudget_importer/src/transform_state/transform_id.rs9
-rw-r--r--rust/schist_actualbudget_importer/src/transform_state/transform_state.rs36
-rw-r--r--rust/schist_actualbudget_importer/src/transform_state/transform_transactions.rs85
-rw-r--r--rust/schist_actualbudget_importer/src/transformers.rs257
18 files changed, 407 insertions, 314 deletions
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index ce690fa..55a9545 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -3,19 +3,6 @@
version = 3
[[package]]
-name = "actualbudget_importer"
-version = "0.1.0"
-dependencies = [
- "actualbudget_models",
- "actualbudget_queries",
- "anyhow",
- "diesel",
- "libsqlite3-sys",
- "schist_models",
- "schist_queries",
-]
-
-[[package]]
name = "actualbudget_models"
version = "0.1.0"
dependencies = [
@@ -1325,6 +1312,19 @@ dependencies = [
]
[[package]]
+name = "schist_actualbudget_importer"
+version = "0.1.0"
+dependencies = [
+ "actualbudget_models",
+ "actualbudget_queries",
+ "anyhow",
+ "diesel",
+ "libsqlite3-sys",
+ "schist_models",
+ "schist_queries",
+]
+
+[[package]]
name = "schist_backend"
version = "0.1.0"
dependencies = [
diff --git a/rust/schist_actualbudget_importer/Cargo.toml b/rust/schist_actualbudget_importer/Cargo.toml
index 9afefef..3fa308a 100644
--- a/rust/schist_actualbudget_importer/Cargo.toml
+++ b/rust/schist_actualbudget_importer/Cargo.toml
@@ -1,5 +1,5 @@
[package]
-name = "actualbudget_importer"
+name = "schist_actualbudget_importer"
version = "0.1.0"
edition = "2021"
diff --git a/rust/schist_actualbudget_importer/src/actualbudget_state.rs b/rust/schist_actualbudget_importer/src/actualbudget_state.rs
new file mode 100644
index 0000000..a83a289
--- /dev/null
+++ b/rust/schist_actualbudget_importer/src/actualbudget_state.rs
@@ -0,0 +1,24 @@
+use actualbudget_models::{actualbudget_account::ActualbudgetAccount, actualbudget_category::ActualbudgetCategory, actualbudget_date::ActualbudgetDate, actualbudget_transaction::ActualbudgetTransaction, actualbudget_zero_budget::ActualbudgetZeroBudget};
+use actualbudget_queries::{actualbudget_accounts::{get_all_actualbudget_accounts, get_category_transfers_account}, actualbudget_categories::get_all_actualbudget_categories, actualbudget_transactions::{get_all_actualbudget_transactions, get_first_actualbudget_transaction_date}, actualbudget_zero_budgets::get_all_actualbudget_zero_budgets};
+use anyhow::Result;
+use diesel::SqliteConnection;
+
+pub struct ActualbudgetState {
+ pub actualbudget_first_transaction_date: ActualbudgetDate,
+ pub actualbudget_accounts: Vec<ActualbudgetAccount>,
+ pub actualbudget_categories: Vec<ActualbudgetCategory>,
+ pub actualbudget_zero_budgets: Vec<ActualbudgetZeroBudget>,
+ pub actualbudget_category_transfers_account: Option<ActualbudgetAccount>,
+ pub actualbudget_transactions: Vec<ActualbudgetTransaction>,
+}
+
+pub fn read_actualbudget_state(actualbudget_connection: &mut SqliteConnection) -> Result<ActualbudgetState> {
+ Ok(ActualbudgetState {
+ actualbudget_first_transaction_date: get_first_actualbudget_transaction_date(actualbudget_connection)?,
+ actualbudget_accounts: get_all_actualbudget_accounts(actualbudget_connection)?,
+ actualbudget_categories: get_all_actualbudget_categories(actualbudget_connection)?,
+ actualbudget_zero_budgets: get_all_actualbudget_zero_budgets(actualbudget_connection)?,
+ actualbudget_category_transfers_account: get_category_transfers_account(actualbudget_connection)?,
+ actualbudget_transactions: get_all_actualbudget_transactions(actualbudget_connection)?,
+ })
+}
diff --git a/rust/schist_actualbudget_importer/src/lib.rs b/rust/schist_actualbudget_importer/src/lib.rs
new file mode 100644
index 0000000..99bd311
--- /dev/null
+++ b/rust/schist_actualbudget_importer/src/lib.rs
@@ -0,0 +1,4 @@
+pub mod actualbudget_state;
+pub mod run;
+pub mod schist_state;
+pub mod transform_state;
diff --git a/rust/schist_actualbudget_importer/src/main.rs b/rust/schist_actualbudget_importer/src/main.rs
index 2236f73..00e53c1 100644
--- a/rust/schist_actualbudget_importer/src/main.rs
+++ b/rust/schist_actualbudget_importer/src/main.rs
@@ -1,47 +1,5 @@
-pub mod transformers;
-
use anyhow::Result;
-use schist_models::{account::Account, budget_update::BudgetUpdate, category::Category, category_transfer::CategoryTransfer, transaction::Transaction, transaction_categorisation::TransactionCategorisation};
-use schist_queries;
-use diesel::{Connection, SqliteConnection};
-use actualbudget_queries::{actualbudget_accounts::{get_all_actualbudget_accounts, get_category_transfers_account}, actualbudget_categories::get_all_actualbudget_categories, actualbudget_transactions::{get_all_actualbudget_transactions, get_first_actualbudget_transaction_date}, actualbudget_zero_budgets::get_all_actualbudget_zero_budgets};
fn main() -> Result<()> {
- let connection_src = &mut SqliteConnection::establish("in.sqlite").unwrap();
- let connection_dest = &mut SqliteConnection::establish("out.sqlite").unwrap();
-
- let actualbudget_first_transaction_date = get_first_actualbudget_transaction_date(connection_src)?;
- let actualbudget_accounts = get_all_actualbudget_accounts(connection_src)?;
- let actualbudget_categories = get_all_actualbudget_categories(connection_src)?;
- let actualbudget_zero_budgets = get_all_actualbudget_zero_budgets(connection_src)?;
- let actualbudget_category_transfers_account = get_category_transfers_account(connection_src)?;
- let actualbudget_transactions = get_all_actualbudget_transactions(connection_src)?;
-
- let first_transaction_date =
- transformers::transform_date(&actualbudget_first_transaction_date)?;
- let accounts: Vec<Account> =
- transformers::transform_accounts(&actualbudget_accounts, &first_transaction_date);
- let categories: Vec<Category> =
- transformers::transform_categories(&actualbudget_categories);
- let budget_updates: Vec<BudgetUpdate> =
- transformers::transform_budget_updates(&actualbudget_zero_budgets)?;
- let (transactions, transaction_categorisations): (Vec<Transaction>, Vec<TransactionCategorisation>) =
- transformers::transform_transactions(
- &actualbudget_transactions,
- &actualbudget_category_transfers_account,
- )?;
- let category_transfers: Vec<CategoryTransfer> = transformers::transform_category_transfers(
- &actualbudget_transactions,
- &actualbudget_category_transfers_account,
- );
-
- schist_queries::clear::clear(connection_dest)?;
- schist_queries::accounts::insert_accounts(&accounts, connection_dest)?;
- schist_queries::categories::insert_categories(&categories, connection_dest)?;
- schist_queries::budget_updates::insert_budget_updates(&budget_updates, connection_dest)?;
- schist_queries::transactions::insert_transactions(&transactions, connection_dest)?;
- schist_queries::transaction_categorisations::insert_transaction_categorisations(&transaction_categorisations, connection_dest)?;
- schist_queries::category_transfers::insert_category_transfers(&category_transfers, connection_dest)?;
-
- Ok(())
+ schist_actualbudget_importer::run::run()
}
diff --git a/rust/schist_actualbudget_importer/src/run.rs b/rust/schist_actualbudget_importer/src/run.rs
new file mode 100644
index 0000000..77f0e45
--- /dev/null
+++ b/rust/schist_actualbudget_importer/src/run.rs
@@ -0,0 +1,15 @@
+use anyhow::Result;
+use diesel::{Connection, SqliteConnection};
+
+use crate::{actualbudget_state::read_actualbudget_state, schist_state::export_schist_state, transform_state::transform_state};
+
+pub fn run() -> Result<()> {
+ let actualbudget_connection = &mut SqliteConnection::establish("in.sqlite").unwrap();
+ let schist_connection = &mut SqliteConnection::establish("out.sqlite").unwrap();
+
+ let actualbudget_state = read_actualbudget_state(actualbudget_connection)?;
+ let schist_state = transform_state(actualbudget_state)?;
+ export_schist_state(&schist_state, schist_connection)?;
+
+ Ok(())
+}
diff --git a/rust/schist_actualbudget_importer/src/schist_state.rs b/rust/schist_actualbudget_importer/src/schist_state.rs
new file mode 100644
index 0000000..a42e446
--- /dev/null
+++ b/rust/schist_actualbudget_importer/src/schist_state.rs
@@ -0,0 +1,24 @@
+use anyhow::Result;
+use diesel::SqliteConnection;
+use schist_models::{account::Account, budget_update::BudgetUpdate, category::Category, category_transfer::CategoryTransfer, datetime_utc::DatetimeUtc, transaction::Transaction, transaction_categorisation::TransactionCategorisation};
+
+pub struct SchistState {
+ pub first_transaction_date: DatetimeUtc,
+ pub accounts: Vec<Account>,
+ pub categories: Vec<Category>,
+ pub budget_updates: Vec<BudgetUpdate>,
+ pub transactions: Vec<Transaction>,
+ pub transaction_categorisations: Vec<TransactionCategorisation>,
+ pub category_transfers: Vec<CategoryTransfer>,
+}
+
+pub fn export_schist_state(schist_state: &SchistState, schist_connection: &mut SqliteConnection) -> Result<()> {
+ schist_queries::clear::clear(schist_connection)?;
+ schist_queries::accounts::insert_accounts(&schist_state.accounts, schist_connection)?;
+ schist_queries::categories::insert_categories(&schist_state.categories, schist_connection)?;
+ schist_queries::budget_updates::insert_budget_updates(&schist_state.budget_updates, schist_connection)?;
+ schist_queries::transactions::insert_transactions(&schist_state.transactions, schist_connection)?;
+ schist_queries::transaction_categorisations::insert_transaction_categorisations(&schist_state.transaction_categorisations, schist_connection)?;
+ schist_queries::category_transfers::insert_category_transfers(&schist_state.category_transfers, schist_connection)?;
+ Ok(())
+}
diff --git a/rust/schist_actualbudget_importer/src/transform_state/is_category_transfer.rs b/rust/schist_actualbudget_importer/src/transform_state/is_category_transfer.rs
new file mode 100644
index 0000000..0f4299a
--- /dev/null
+++ b/rust/schist_actualbudget_importer/src/transform_state/is_category_transfer.rs
@@ -0,0 +1,18 @@
+use actualbudget_models::{actualbudget_account::ActualbudgetAccount, actualbudget_transaction::ActualbudgetTransaction};
+
+pub fn is_category_transfer(
+ transaction: &ActualbudgetTransaction,
+ actualbudget_category_transfers_account: &Option<ActualbudgetAccount>
+) -> bool {
+ match actualbudget_category_transfers_account {
+ None => false,
+ Some(acc) => transaction.account_id.clone() == acc.id.clone(),
+ }
+}
+
+pub fn is_not_category_transfer(
+ transaction: &ActualbudgetTransaction,
+ actualbudget_category_transfers_account: &Option<ActualbudgetAccount>
+) -> bool {
+ !is_category_transfer(transaction, actualbudget_category_transfers_account)
+}
diff --git a/rust/schist_actualbudget_importer/src/transform_state/mod.rs b/rust/schist_actualbudget_importer/src/transform_state/mod.rs
new file mode 100644
index 0000000..d82a59e
--- /dev/null
+++ b/rust/schist_actualbudget_importer/src/transform_state/mod.rs
@@ -0,0 +1,21 @@
+mod is_category_transfer;
+mod transform_accounts;
+mod transform_budget_updates;
+mod transform_categories;
+mod transform_category_transfers;
+mod transform_date;
+mod transform_id;
+mod transform_state;
+mod transform_transactions;
+
+use is_category_transfer::is_category_transfer;
+use is_category_transfer::is_not_category_transfer;
+use transform_accounts::transform_accounts;
+use transform_budget_updates::transform_budget_updates;
+use transform_categories::transform_categories;
+use transform_category_transfers::transform_category_transfers;
+use transform_date::transform_date;
+use transform_id::transform_id;
+use transform_transactions::transform_transactions;
+
+pub use transform_state::*;
diff --git a/rust/schist_actualbudget_importer/src/transform_state/transform_accounts.rs b/rust/schist_actualbudget_importer/src/transform_state/transform_accounts.rs
new file mode 100644
index 0000000..beb1de7
--- /dev/null
+++ b/rust/schist_actualbudget_importer/src/transform_state/transform_accounts.rs
@@ -0,0 +1,27 @@
+use actualbudget_models::actualbudget_account::ActualbudgetAccount;
+use schist_models::{account::Account, datetime_utc::DatetimeUtc};
+
+use super::transform_id;
+
+pub fn transform_accounts(
+ actualbudget_accounts: &[ActualbudgetAccount],
+ first_transaction_date: &DatetimeUtc,
+) -> Vec<Account> {
+ let mut accounts = Vec::<Account>::new();
+ for account in actualbudget_accounts {
+ accounts.push(transform_account(account, first_transaction_date));
+ }
+ accounts
+}
+
+fn transform_account(
+ actualbudget_account: &ActualbudgetAccount,
+ opening_date: &DatetimeUtc,
+) -> Account {
+ Account {
+ id: transform_id(&actualbudget_account.id.clone()),
+ name: actualbudget_account.name.clone(),
+ opening_balance: 0,
+ opening_date: opening_date.clone(),
+ }
+}
diff --git a/rust/schist_actualbudget_importer/src/transform_state/transform_budget_updates.rs b/rust/schist_actualbudget_importer/src/transform_state/transform_budget_updates.rs
new file mode 100644
index 0000000..fc2ee23
--- /dev/null
+++ b/rust/schist_actualbudget_importer/src/transform_state/transform_budget_updates.rs
@@ -0,0 +1,40 @@
+use anyhow::Result;
+use actualbudget_models::actualbudget_zero_budget::ActualbudgetZeroBudget;
+use schist_models::{budget_update::BudgetUpdate, date_utc::DateUtc};
+
+use super::{transform_date, transform_id};
+
+pub fn transform_budget_updates(
+ actualbudget_zero_budgets: &[ActualbudgetZeroBudget],
+) -> Result<Vec<BudgetUpdate>> {
+ let mut budget_updates = Vec::<BudgetUpdate>::new();
+ for zero_budget in actualbudget_zero_budgets {
+ let first_zero_budget_in_category = get_first_zero_budget_in_category(
+ actualbudget_zero_budgets,
+ &zero_budget.category_id.clone(),
+ );
+ if first_zero_budget_in_category.is_some_and(|zb| zb.id == zero_budget.id) {
+ continue;
+ }
+ budget_updates.push(BudgetUpdate {
+ id: transform_id(&zero_budget.id.clone()),
+ category_id: transform_id(&zero_budget.category_id.clone()),
+ date: DateUtc::from(transform_date(&zero_budget.month.clone())?),
+ new_budget: zero_budget.amount.try_into().unwrap(),
+ new_period: 30,
+ })
+ }
+ Ok(budget_updates)
+}
+
+fn get_first_zero_budget_in_category(
+ actualbudget_zero_budgets: &[ActualbudgetZeroBudget],
+ actualbudget_category_id: &str,
+) -> Option<ActualbudgetZeroBudget> {
+ let mut category_zero_budgets = actualbudget_zero_budgets.to_vec();
+ category_zero_budgets.retain(|zb| zb.category_id == actualbudget_category_id);
+ category_zero_budgets
+ .iter()
+ .min_by_key(|zb| zb.month.clone())
+ .cloned()
+}
diff --git a/rust/schist_actualbudget_importer/src/transform_state/transform_categories.rs b/rust/schist_actualbudget_importer/src/transform_state/transform_categories.rs
new file mode 100644
index 0000000..80ff80b
--- /dev/null
+++ b/rust/schist_actualbudget_importer/src/transform_state/transform_categories.rs
@@ -0,0 +1,17 @@
+use actualbudget_models::actualbudget_category::ActualbudgetCategory;
+use schist_models::category::Category;
+
+use super::transform_id;
+
+pub fn transform_categories(
+ actualbudget_categories: &[ActualbudgetCategory],
+) -> Vec<Category> {
+ let mut categories = Vec::<Category>::new();
+ for actualbudget_category in actualbudget_categories {
+ categories.push(Category {
+ id: transform_id(&actualbudget_category.id.clone()),
+ name: actualbudget_category.name.clone(),
+ })
+ }
+ categories
+}
diff --git a/rust/schist_actualbudget_importer/src/transform_state/transform_category_transfers.rs b/rust/schist_actualbudget_importer/src/transform_state/transform_category_transfers.rs
new file mode 100644
index 0000000..86ad498
--- /dev/null
+++ b/rust/schist_actualbudget_importer/src/transform_state/transform_category_transfers.rs
@@ -0,0 +1,65 @@
+use actualbudget_models::{
+ actualbudget_account::ActualbudgetAccount,
+ actualbudget_transaction::ActualbudgetTransaction,
+};
+use schist_models::category_transfer::CategoryTransfer;
+use core::panic;
+
+use super::{is_category_transfer, transform_id};
+
+pub fn transform_category_transfers(
+ actualbudget_transactions: &[ActualbudgetTransaction],
+ actualbudget_category_transfers_account: &Option<ActualbudgetAccount>,
+) -> Vec<CategoryTransfer> {
+ let mut category_transfers = Vec::<CategoryTransfer>::new();
+
+ let mut actualbudget_category_transfers = actualbudget_transactions.to_vec();
+ actualbudget_category_transfers.retain(|transaction|
+ is_category_transfer(transaction, actualbudget_category_transfers_account)
+ );
+ let mut actualbudget_parent_category_transfers = actualbudget_category_transfers.to_vec();
+ actualbudget_parent_category_transfers.retain(|t| t.is_parent);
+ let mut actualbudget_child_category_transfers = actualbudget_category_transfers.to_vec();
+ actualbudget_child_category_transfers.retain(|t| t.is_child);
+
+ for actualbudget_parent_category_transfer in actualbudget_parent_category_transfers {
+ let mut actualbudget_child_category_transfers =
+ actualbudget_child_category_transfers.to_vec();
+ actualbudget_child_category_transfers.retain(|t| {
+ t.parent_id
+ .clone()
+ .is_some_and(|parent_id| parent_id == actualbudget_parent_category_transfer.id)
+ });
+
+ if actualbudget_child_category_transfers.len() != 2 {
+ panic!("Category transfers should always have exactly two children.");
+ }
+
+ let category_1 = actualbudget_child_category_transfers[0].clone();
+ let category_2 = actualbudget_child_category_transfers[1].clone();
+
+ // To avoid duplicates
+ if category_1.amount >= 0 {
+ continue;
+ }
+
+ if actualbudget_child_category_transfers[0].amount
+ != -actualbudget_child_category_transfers[1].amount
+ {
+ panic!("Category transfer children should sum to zero.");
+ }
+
+ category_transfers.push(CategoryTransfer {
+ id: transform_id(&actualbudget_parent_category_transfer.id),
+ description: actualbudget_parent_category_transfer.notes.unwrap_or_else(String::new),
+ quantity: actualbudget_child_category_transfers[0]
+ .amount
+ .try_into()
+ .unwrap(),
+ from_category_id: transform_id(&category_1.id),
+ to_category_id: transform_id(&category_2.id),
+ });
+ }
+
+ category_transfers
+}
diff --git a/rust/schist_actualbudget_importer/src/transform_state/transform_date.rs b/rust/schist_actualbudget_importer/src/transform_state/transform_date.rs
new file mode 100644
index 0000000..803f3ed
--- /dev/null
+++ b/rust/schist_actualbudget_importer/src/transform_state/transform_date.rs
@@ -0,0 +1,7 @@
+use anyhow::Result;
+use actualbudget_models::actualbudget_date::ActualbudgetDate;
+use schist_models::datetime_utc::DatetimeUtc;
+
+pub fn transform_date(actualbudget_date: &ActualbudgetDate) -> Result<DatetimeUtc> {
+ actualbudget_date.to_iso().parse()
+}
diff --git a/rust/schist_actualbudget_importer/src/transform_state/transform_id.rs b/rust/schist_actualbudget_importer/src/transform_state/transform_id.rs
new file mode 100644
index 0000000..f370efe
--- /dev/null
+++ b/rust/schist_actualbudget_importer/src/transform_state/transform_id.rs
@@ -0,0 +1,9 @@
+use std::hash::{DefaultHasher, Hash, Hasher};
+
+pub fn transform_id(actualbudget_id: &str) -> i32 {
+ let mut hasher = DefaultHasher::new();
+ actualbudget_id.hash(&mut hasher);
+ (hasher.finish() % std::convert::TryInto::<u64>::try_into(i32::MAX).unwrap())
+ .try_into()
+ .unwrap()
+}
diff --git a/rust/schist_actualbudget_importer/src/transform_state/transform_state.rs b/rust/schist_actualbudget_importer/src/transform_state/transform_state.rs
new file mode 100644
index 0000000..9236532
--- /dev/null
+++ b/rust/schist_actualbudget_importer/src/transform_state/transform_state.rs
@@ -0,0 +1,36 @@
+use anyhow::Result;
+use schist_models::{account::Account, budget_update::BudgetUpdate, category::Category, category_transfer::CategoryTransfer, transaction::Transaction, transaction_categorisation::TransactionCategorisation};
+
+use crate::{actualbudget_state::ActualbudgetState, schist_state::SchistState};
+
+use super::{transform_accounts, transform_budget_updates, transform_categories, transform_category_transfers, transform_date, transform_transactions};
+
+pub fn transform_state(actualbudget_state: ActualbudgetState) -> Result<SchistState> {
+ let first_transaction_date =
+ transform_date(&actualbudget_state.actualbudget_first_transaction_date)?;
+ let accounts: Vec<Account> =
+ transform_accounts(&actualbudget_state.actualbudget_accounts, &first_transaction_date);
+ let categories: Vec<Category> =
+ transform_categories(&actualbudget_state.actualbudget_categories);
+ let budget_updates: Vec<BudgetUpdate> =
+ transform_budget_updates(&actualbudget_state.actualbudget_zero_budgets)?;
+ let (transactions, transaction_categorisations): (Vec<Transaction>, Vec<TransactionCategorisation>) =
+ transform_transactions(
+ &actualbudget_state.actualbudget_transactions,
+ &actualbudget_state.actualbudget_category_transfers_account,
+ )?;
+ let category_transfers: Vec<CategoryTransfer> = transform_category_transfers(
+ &actualbudget_state.actualbudget_transactions,
+ &actualbudget_state.actualbudget_category_transfers_account,
+ );
+
+ Ok(SchistState {
+ first_transaction_date,
+ accounts,
+ categories,
+ budget_updates,
+ transactions,
+ transaction_categorisations,
+ category_transfers,
+ })
+}
diff --git a/rust/schist_actualbudget_importer/src/transform_state/transform_transactions.rs b/rust/schist_actualbudget_importer/src/transform_state/transform_transactions.rs
new file mode 100644
index 0000000..7bb1de9
--- /dev/null
+++ b/rust/schist_actualbudget_importer/src/transform_state/transform_transactions.rs
@@ -0,0 +1,85 @@
+use anyhow::Result;
+use actualbudget_models::{actualbudget_account::ActualbudgetAccount, actualbudget_transaction::ActualbudgetTransaction};
+use schist_models::{transaction::Transaction, transaction_categorisation::TransactionCategorisation};
+
+use super::{is_not_category_transfer, transform_date, transform_id};
+
+pub fn transform_transactions(
+ actualbudget_transactions: &[ActualbudgetTransaction],
+ actualbudget_category_transfers_account: &Option<ActualbudgetAccount>,
+) -> Result<(Vec<Transaction>, Vec<TransactionCategorisation>)> {
+ let mut transactions = Vec::<Transaction>::new();
+ let mut transaction_categorisations = Vec::<TransactionCategorisation>::new();
+
+ let mut actualbudget_transactions = actualbudget_transactions.to_vec();
+ actualbudget_transactions.retain(|transaction|
+ is_not_category_transfer(transaction, actualbudget_category_transfers_account)
+ );
+ let mut parent_actualbudget_transactions = actualbudget_transactions.to_vec();
+ parent_actualbudget_transactions.retain(|t| t.is_parent);
+ let mut child_actualbudget_transactions = actualbudget_transactions.to_vec();
+ child_actualbudget_transactions.retain(|t| t.is_child);
+ let mut atomic_actualbudget_transactions = actualbudget_transactions.to_vec();
+ atomic_actualbudget_transactions.retain(|t| !t.is_parent && !t.is_child);
+
+ for atomic_actualbudget_transaction in atomic_actualbudget_transactions {
+ let transaction_id = transform_id(&atomic_actualbudget_transaction.id);
+
+ transactions.push(Transaction {
+ id: transaction_id,
+ description: atomic_actualbudget_transaction.notes.unwrap_or_else(String::new),
+ payee: atomic_actualbudget_transaction.payee.unwrap_or_else(String::new),
+ quantity: atomic_actualbudget_transaction.amount.try_into().unwrap(),
+ date: transform_date(&atomic_actualbudget_transaction.date)?,
+ account_id: transform_id(&atomic_actualbudget_transaction.account_id),
+ });
+
+ let actualbudget_category_id = atomic_actualbudget_transaction
+ .category_id
+ .expect("actualbudget transaction should have been atomic, but in fact had no category ID");
+
+ transaction_categorisations.push(TransactionCategorisation {
+ id: transaction_id,
+ description: String::new(),
+ quantity: atomic_actualbudget_transaction.amount.try_into().unwrap(),
+ transaction_id,
+ category_id: transform_id(&actualbudget_category_id),
+ });
+ }
+
+ for parent_actualbudget_transaction in parent_actualbudget_transactions {
+ let mut child_actualbudget_transactions = child_actualbudget_transactions.to_vec();
+ child_actualbudget_transactions.retain(|t| {
+ t.parent_id
+ .clone()
+ .is_some_and(|parent_id| parent_id == parent_actualbudget_transaction.id.clone())
+ });
+
+ let transaction_id = transform_id(&parent_actualbudget_transaction.id);
+
+ transactions.push(Transaction {
+ id: transaction_id,
+ description: parent_actualbudget_transaction.notes.unwrap_or_else(String::new),
+ payee: parent_actualbudget_transaction.payee.unwrap_or_else(String::new),
+ quantity: parent_actualbudget_transaction.amount.try_into().unwrap(),
+ date: transform_date(&parent_actualbudget_transaction.date)?,
+ account_id: transform_id(&parent_actualbudget_transaction.account_id),
+ });
+
+ for child_actualbudget_transaction in child_actualbudget_transactions {
+ let actualbudget_category_id = child_actualbudget_transaction
+ .category_id
+ .expect("actualbudget transaction should have been a child transaction, but in fact had no category ID");
+
+ transaction_categorisations.push(TransactionCategorisation {
+ id: transform_id(&child_actualbudget_transaction.id),
+ description: child_actualbudget_transaction.notes.unwrap_or_else(String::new),
+ quantity: child_actualbudget_transaction.amount.try_into().unwrap(),
+ transaction_id,
+ category_id: transform_id(&actualbudget_category_id),
+ });
+ }
+ }
+
+ Ok((transactions, transaction_categorisations))
+}
diff --git a/rust/schist_actualbudget_importer/src/transformers.rs b/rust/schist_actualbudget_importer/src/transformers.rs
deleted file mode 100644
index 711904b..0000000
--- a/rust/schist_actualbudget_importer/src/transformers.rs
+++ /dev/null
@@ -1,257 +0,0 @@
-use actualbudget_models::{
- actualbudget_account::ActualbudgetAccount,
- actualbudget_category::ActualbudgetCategory,
- actualbudget_date::ActualbudgetDate,
- actualbudget_transaction::ActualbudgetTransaction,
- actualbudget_zero_budget::ActualbudgetZeroBudget,
-};
-use anyhow::Result;
-use schist_models::{
- account::Account,
- budget_update::BudgetUpdate,
- category::Category,
- category_transfer::CategoryTransfer,
- date_utc::DateUtc,
- datetime_utc::DatetimeUtc,
- transaction::Transaction,
- transaction_categorisation::TransactionCategorisation,
-};
-use core::panic;
-use std::hash::{DefaultHasher, Hash, Hasher};
-
-pub fn transform_date(actualbudget_date: &ActualbudgetDate) -> Result<DatetimeUtc> {
- actualbudget_date.to_iso().parse()
-}
-
-pub fn transform_accounts(
- actualbudget_accounts: &[ActualbudgetAccount],
- first_transaction_date: &DatetimeUtc,
-) -> Vec<Account> {
- let mut accounts = Vec::<Account>::new();
- for account in actualbudget_accounts {
- accounts.push(transform_account(account, first_transaction_date));
- }
- accounts
-}
-
-fn transform_account(
- actualbudget_account: &ActualbudgetAccount,
- opening_date: &DatetimeUtc,
-) -> Account {
- Account {
- id: transform_id(&actualbudget_account.id.clone()),
- name: actualbudget_account.name.clone(),
- opening_balance: 0,
- opening_date: opening_date.clone(),
- }
-}
-
-pub fn transform_categories(
- actualbudget_categories: &[ActualbudgetCategory],
-) -> Vec<Category> {
- let mut categories = Vec::<Category>::new();
- for actualbudget_category in actualbudget_categories {
- categories.push(Category {
- id: transform_id(&actualbudget_category.id.clone()),
- name: actualbudget_category.name.clone(),
- })
- }
- categories
-}
-
-fn transform_id(actualbudget_id: &str) -> i32 {
- let mut hasher = DefaultHasher::new();
- actualbudget_id.hash(&mut hasher);
- (hasher.finish() % std::convert::TryInto::<u64>::try_into(i32::MAX).unwrap())
- .try_into()
- .unwrap()
-}
-
-pub fn transform_budget_updates(
- actualbudget_zero_budgets: &[ActualbudgetZeroBudget],
-) -> Result<Vec<BudgetUpdate>> {
- let mut budget_updates = Vec::<BudgetUpdate>::new();
- for zero_budget in actualbudget_zero_budgets {
- let first_zero_budget_in_category = get_first_zero_budget_in_category(
- actualbudget_zero_budgets,
- &zero_budget.category_id.clone(),
- );
- if first_zero_budget_in_category.is_some_and(|zb| zb.id == zero_budget.id) {
- continue;
- }
- budget_updates.push(BudgetUpdate {
- id: transform_id(&zero_budget.id.clone()),
- category_id: transform_id(&zero_budget.category_id.clone()),
- date: DateUtc::from(transform_date(&zero_budget.month.clone())?),
- new_budget: zero_budget.amount.try_into().unwrap(),
- new_period: 30,
- })
- }
- Ok(budget_updates)
-}
-
-fn get_first_zero_budget_in_category(
- actualbudget_zero_budgets: &[ActualbudgetZeroBudget],
- actualbudget_category_id: &str,
-) -> Option<ActualbudgetZeroBudget> {
- let mut category_zero_budgets = actualbudget_zero_budgets.to_vec();
- category_zero_budgets.retain(|zb| zb.category_id == actualbudget_category_id);
- category_zero_budgets
- .iter()
- .min_by_key(|zb| zb.month.clone())
- .cloned()
-}
-
-pub fn transform_transactions(
- actualbudget_transactions: &[ActualbudgetTransaction],
- actualbudget_category_transfers_account: &Option<ActualbudgetAccount>,
-) -> Result<(Vec<Transaction>, Vec<TransactionCategorisation>)> {
- let mut transactions = Vec::<Transaction>::new();
- let mut transaction_categorisations = Vec::<TransactionCategorisation>::new();
-
- let mut actualbudget_transactions = actualbudget_transactions.to_vec();
- actualbudget_transactions.retain(|transaction|
- is_not_category_transfer(transaction, actualbudget_category_transfers_account)
- );
- let mut parent_actualbudget_transactions = actualbudget_transactions.to_vec();
- parent_actualbudget_transactions.retain(|t| t.is_parent);
- let mut child_actualbudget_transactions = actualbudget_transactions.to_vec();
- child_actualbudget_transactions.retain(|t| t.is_child);
- let mut atomic_actualbudget_transactions = actualbudget_transactions.to_vec();
- atomic_actualbudget_transactions.retain(|t| !t.is_parent && !t.is_child);
-
- for atomic_actualbudget_transaction in atomic_actualbudget_transactions {
- let transaction_id = transform_id(&atomic_actualbudget_transaction.id);
-
- transactions.push(Transaction {
- id: transaction_id,
- description: atomic_actualbudget_transaction.notes.unwrap_or_else(String::new),
- payee: atomic_actualbudget_transaction.payee.unwrap_or_else(String::new),
- quantity: atomic_actualbudget_transaction.amount.try_into().unwrap(),
- date: transform_date(&atomic_actualbudget_transaction.date)?,
- account_id: transform_id(&atomic_actualbudget_transaction.account_id),
- });
-
- let actualbudget_category_id = atomic_actualbudget_transaction
- .category_id
- .expect("actualbudget transaction should have been atomic, but in fact had no category ID");
-
- transaction_categorisations.push(TransactionCategorisation {
- id: transaction_id,
- description: String::new(),
- quantity: atomic_actualbudget_transaction.amount.try_into().unwrap(),
- transaction_id,
- category_id: transform_id(&actualbudget_category_id),
- });
- }
-
- for parent_actualbudget_transaction in parent_actualbudget_transactions {
- let mut child_actualbudget_transactions = child_actualbudget_transactions.to_vec();
- child_actualbudget_transactions.retain(|t| {
- t.parent_id
- .clone()
- .is_some_and(|parent_id| parent_id == parent_actualbudget_transaction.id.clone())
- });
-
- let transaction_id = transform_id(&parent_actualbudget_transaction.id);
-
- transactions.push(Transaction {
- id: transaction_id,
- description: parent_actualbudget_transaction.notes.unwrap_or_else(String::new),
- payee: parent_actualbudget_transaction.payee.unwrap_or_else(String::new),
- quantity: parent_actualbudget_transaction.amount.try_into().unwrap(),
- date: transform_date(&parent_actualbudget_transaction.date)?,
- account_id: transform_id(&parent_actualbudget_transaction.account_id),
- });
-
- for child_actualbudget_transaction in child_actualbudget_transactions {
- let actualbudget_category_id = child_actualbudget_transaction
- .category_id
- .expect("actualbudget transaction should have been a child transaction, but in fact had no category ID");
-
- transaction_categorisations.push(TransactionCategorisation {
- id: transform_id(&child_actualbudget_transaction.id),
- description: child_actualbudget_transaction.notes.unwrap_or_else(String::new),
- quantity: child_actualbudget_transaction.amount.try_into().unwrap(),
- transaction_id,
- category_id: transform_id(&actualbudget_category_id),
- });
- }
- }
-
- Ok((transactions, transaction_categorisations))
-}
-
-pub fn transform_category_transfers(
- actualbudget_transactions: &[ActualbudgetTransaction],
- actualbudget_category_transfers_account: &Option<ActualbudgetAccount>,
-) -> Vec<CategoryTransfer> {
- let mut category_transfers = Vec::<CategoryTransfer>::new();
-
- let mut actualbudget_category_transfers = actualbudget_transactions.to_vec();
- actualbudget_category_transfers.retain(|transaction|
- is_category_transfer(transaction, actualbudget_category_transfers_account)
- );
- let mut actualbudget_parent_category_transfers = actualbudget_category_transfers.to_vec();
- actualbudget_parent_category_transfers.retain(|t| t.is_parent);
- let mut actualbudget_child_category_transfers = actualbudget_category_transfers.to_vec();
- actualbudget_child_category_transfers.retain(|t| t.is_child);
-
- for actualbudget_parent_category_transfer in actualbudget_parent_category_transfers {
- let mut actualbudget_child_category_transfers =
- actualbudget_child_category_transfers.to_vec();
- actualbudget_child_category_transfers.retain(|t| {
- t.parent_id
- .clone()
- .is_some_and(|parent_id| parent_id == actualbudget_parent_category_transfer.id)
- });
-
- if actualbudget_child_category_transfers.len() != 2 {
- panic!("Category transfers should always have exactly two children.");
- }
-
- let category_1 = actualbudget_child_category_transfers[0].clone();
- let category_2 = actualbudget_child_category_transfers[1].clone();
-
- // To avoid duplicates
- if category_1.amount >= 0 {
- continue;
- }
-
- if actualbudget_child_category_transfers[0].amount
- != -actualbudget_child_category_transfers[1].amount
- {
- panic!("Category transfer children should sum to zero.");
- }
-
- category_transfers.push(CategoryTransfer {
- id: transform_id(&actualbudget_parent_category_transfer.id),
- description: actualbudget_parent_category_transfer.notes.unwrap_or_else(String::new),
- quantity: actualbudget_child_category_transfers[0]
- .amount
- .try_into()
- .unwrap(),
- from_category_id: transform_id(&category_1.id),
- to_category_id: transform_id(&category_2.id),
- });
- }
-
- category_transfers
-}
-
-fn is_category_transfer(
- transaction: &ActualbudgetTransaction,
- actualbudget_category_transfers_account: &Option<ActualbudgetAccount>
-) -> bool {
- match actualbudget_category_transfers_account {
- None => false,
- Some(acc) => transaction.account_id.clone() == acc.id.clone(),
- }
-}
-
-fn is_not_category_transfer(
- transaction: &ActualbudgetTransaction,
- actualbudget_category_transfers_account: &Option<ActualbudgetAccount>
-) -> bool {
- !is_category_transfer(transaction, actualbudget_category_transfers_account)
-}