summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--rust/actualbudget_to_schist_transformer/src/schist_state.rs4
-rw-r--r--rust/actualbudget_to_schist_transformer/src/transform_state/is_account_transfer.rs13
-rw-r--r--rust/actualbudget_to_schist_transformer/src/transform_state/mod.rs7
-rw-r--r--rust/actualbudget_to_schist_transformer/src/transform_state/transform_account_transfers.rs306
-rw-r--r--rust/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs10
5 files changed, 335 insertions, 5 deletions
diff --git a/rust/actualbudget_to_schist_transformer/src/schist_state.rs b/rust/actualbudget_to_schist_transformer/src/schist_state.rs
index e72d56a..370a0e8 100644
--- a/rust/actualbudget_to_schist_transformer/src/schist_state.rs
+++ b/rust/actualbudget_to_schist_transformer/src/schist_state.rs
@@ -1,12 +1,13 @@
use anyhow::Result;
use diesel::SqliteConnection;
use schist_models::{
- account::Account, budget_drip::BudgetDrip, category::Category, category_transfer::CategoryTransfer, date_utc::DateUtc, transaction::Transaction, transaction_categorisation::TransactionCategorisation
+ account::Account, account_transfer::AccountTransfer, budget_drip::BudgetDrip, category::Category, category_transfer::CategoryTransfer, date_utc::DateUtc, transaction::Transaction, transaction_categorisation::TransactionCategorisation
};
pub struct SchistState {
pub first_transaction_date: DateUtc,
pub accounts: Vec<Account>,
+ pub account_transfers: Vec<AccountTransfer>,
pub categories: Vec<Category>,
pub budget_drips: Vec<BudgetDrip>,
pub transactions: Vec<Transaction>,
@@ -17,6 +18,7 @@ pub struct SchistState {
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::account_transfers::insert_account_transfers(&schist_state.account_transfers, 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)?;
diff --git a/rust/actualbudget_to_schist_transformer/src/transform_state/is_account_transfer.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/is_account_transfer.rs
new file mode 100644
index 0000000..c9540ed
--- /dev/null
+++ b/rust/actualbudget_to_schist_transformer/src/transform_state/is_account_transfer.rs
@@ -0,0 +1,13 @@
+use actualbudget_models::ActualbudgetTransaction;
+
+pub fn is_account_transfer(
+ transaction: &ActualbudgetTransaction,
+) -> bool {
+ transaction.transfer_id.is_some()
+}
+
+pub fn is_not_account_transfer(
+ transaction: &ActualbudgetTransaction,
+) -> bool {
+ transaction.transfer_id.is_none()
+}
diff --git a/rust/actualbudget_to_schist_transformer/src/transform_state/mod.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/mod.rs
index 916894a..8787699 100644
--- a/rust/actualbudget_to_schist_transformer/src/transform_state/mod.rs
+++ b/rust/actualbudget_to_schist_transformer/src/transform_state/mod.rs
@@ -1,4 +1,6 @@
+mod is_account_transfer;
mod is_category_transfer;
+mod transform_account_transfers;
mod transform_accounts;
mod transform_budget_drips;
mod transform_categories;
@@ -8,13 +10,16 @@ mod transform_id;
mod transform_state;
mod transform_transactions;
+use is_account_transfer::is_account_transfer;
+use is_account_transfer::is_not_account_transfer;
use is_category_transfer::is_category_transfer;
use is_category_transfer::is_not_category_transfer;
use transform_accounts::transform_accounts;
+use transform_account_transfers::transform_account_transfers;
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::*;
+pub use transform_state::transform_state;
diff --git a/rust/actualbudget_to_schist_transformer/src/transform_state/transform_account_transfers.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_account_transfers.rs
new file mode 100644
index 0000000..7c33aa6
--- /dev/null
+++ b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_account_transfers.rs
@@ -0,0 +1,306 @@
+use actualbudget_models::ActualbudgetTransaction;
+use anyhow::{Context, Result};
+use itertools::Itertools;
+use schist_models::account_transfer::AccountTransfer;
+
+use super::{is_account_transfer, is_not_account_transfer, transform_date::transform_date, transform_id::transform_id};
+
+pub fn transform_account_transfers(
+ actualbudget_transactions: &[ActualbudgetTransaction],
+) -> Result<(Vec<ActualbudgetTransaction>, Vec<AccountTransfer>)> {
+ transform_account_transfers_without_context(actualbudget_transactions)
+ .with_context(|| "failed to transform account transfers")
+}
+
+pub fn transform_account_transfers_without_context(
+ actualbudget_transactions: &[ActualbudgetTransaction],
+) -> Result<(Vec<ActualbudgetTransaction>, Vec<AccountTransfer>)> {
+ let mut account_transfer_transaction_pairs: Vec<(ActualbudgetTransaction, ActualbudgetTransaction)> = Vec::new();
+ let mut transactions_not_transformed: Vec<ActualbudgetTransaction> = Vec::new();
+ let account_transfer_transactions = actualbudget_transactions
+ .iter()
+ .cloned()
+ .filter(is_account_transfer);
+
+ for actualbudget_transaction in actualbudget_transactions {
+ if is_not_account_transfer(actualbudget_transaction) {
+ transactions_not_transformed.push(actualbudget_transaction.clone());
+ continue;
+ }
+
+ let is_already_transformed = |t: &ActualbudgetTransaction|
+ account_transfer_transaction_pairs
+ .iter().any(|(t1, t2)| *t1 == *t || *t2 == *t);
+
+ if is_already_transformed(actualbudget_transaction) {
+ continue;
+ }
+
+ let companion = account_transfer_transactions
+ .clone()
+ .filter(|t| !is_already_transformed(t)
+ && t.date == actualbudget_transaction.date
+ && t.amount == -actualbudget_transaction.amount
+ ).last();
+
+ if companion.is_none() {
+ transactions_not_transformed.push(actualbudget_transaction.clone());
+ continue;
+ }
+ let companion = companion.unwrap();
+
+ let (from, to) = if actualbudget_transaction.amount >= 0 {
+ (actualbudget_transaction.clone(), companion.clone())
+ } else {
+ (companion.clone(), actualbudget_transaction.clone())
+ };
+
+ account_transfer_transaction_pairs.push((from, to));
+ }
+
+ Ok((
+ transactions_not_transformed,
+ account_transfer_transaction_pairs
+ .iter()
+ .map(transform_account_transfer)
+ .try_collect()?,
+ ))
+}
+
+fn transform_account_transfer(
+ (actualbudget_transaction_from, actualbudget_transaction_to):
+ &(ActualbudgetTransaction, ActualbudgetTransaction),
+) -> Result<AccountTransfer> {
+ let context = || format!(
+ "failed to transform account transfer from transactions ({:?}, {:?})",
+ actualbudget_transaction_from,
+ actualbudget_transaction_to,
+ );
+
+ Ok(AccountTransfer {
+ id: transform_id(&actualbudget_transaction_from.id),
+ date: transform_date(&actualbudget_transaction_from.date)
+ .with_context(context)?,
+ description: actualbudget_transaction_from.notes.clone().unwrap_or_else(||
+ actualbudget_transaction_to.notes.clone().unwrap_or_else(String::new)
+ ),
+ quantity: actualbudget_transaction_from.amount,
+ from_account_id: transform_id(actualbudget_transaction_from.account_id.as_str()),
+ to_account_id: transform_id(actualbudget_transaction_to.account_id.as_str()),
+ })
+}
+
+#[cfg(test)]
+mod test {
+ use actualbudget_fakes::actualbudget_transaction::make_fake_actualbudget_transaction_builder;
+ use actualbudget_models::ActualbudgetDate;
+ use schist_models::date_utc::DateUtc;
+
+ use crate::transform_state::transform_id::transform_id;
+
+ use super::transform_account_transfers;
+
+ #[test]
+ fn when_one_account_transfer_then_transforms() {
+ let transactions = [
+ make_fake_actualbudget_transaction_builder("0")
+ .amount(100)
+ .account_id(String::from("0"))
+ .payee(Some(String::from("0")))
+ .transfer_id(Some(String::from("0")))
+ .date(ActualbudgetDate::from_i32(20240801))
+ .notes(Some(String::from("My notes")))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("1")
+ .amount(-100)
+ .account_id(String::from("1"))
+ .payee(Some(String::from("1")))
+ .transfer_id(Some(String::from("0")))
+ .date(ActualbudgetDate::from_i32(20240801))
+ .notes(Some(String::from("My notes")))
+ .build()
+ .unwrap(),
+ ];
+
+ let (remaining_transactions, account_transfers) =
+ transform_account_transfers(&transactions).unwrap();
+
+ assert_eq!(0, remaining_transactions.len());
+ assert_eq!(1, account_transfers.len());
+ let account_transfer = account_transfers.get(0).unwrap();
+ assert_eq!("2024-08-01".parse::<DateUtc>().unwrap(), account_transfer.date);
+ assert_eq!("My notes", account_transfer.description);
+ assert_eq!(100, account_transfer.quantity);
+ assert_eq!(transform_id("0"), account_transfer.from_account_id);
+ assert_eq!(transform_id("1"), account_transfer.to_account_id);
+ }
+
+ #[test]
+ fn when_one_account_transfer_in_reverse_order_then_transforms() {
+ let transactions = [
+ make_fake_actualbudget_transaction_builder("0")
+ .amount(-100)
+ .account_id(String::from("1"))
+ .payee(Some(String::from("1")))
+ .transfer_id(Some(String::from("0")))
+ .date(ActualbudgetDate::from_i32(20240801))
+ .notes(Some(String::from("My notes")))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("1")
+ .amount(100)
+ .account_id(String::from("0"))
+ .payee(Some(String::from("0")))
+ .transfer_id(Some(String::from("0")))
+ .date(ActualbudgetDate::from_i32(20240801))
+ .notes(Some(String::from("My notes")))
+ .build()
+ .unwrap(),
+ ];
+
+ let (remaining_transactions, account_transfers) =
+ transform_account_transfers(&transactions).unwrap();
+
+ assert_eq!(0, remaining_transactions.len());
+ assert_eq!(1, account_transfers.len());
+ let account_transfer = account_transfers.get(0).unwrap();
+ assert_eq!("2024-08-01".parse::<DateUtc>().unwrap(), account_transfer.date);
+ assert_eq!("My notes", account_transfer.description);
+ assert_eq!(100, account_transfer.quantity);
+ assert_eq!(transform_id("0"), account_transfer.from_account_id);
+ assert_eq!(transform_id("1"), account_transfer.to_account_id);
+ }
+
+ #[test]
+ fn when_two_account_transfers_different_amounts_then_transforms() {
+ let transactions = [
+ make_fake_actualbudget_transaction_builder("0")
+ .amount(100)
+ .account_id(String::from("0"))
+ .payee(Some(String::from("0")))
+ .transfer_id(Some(String::from("0")))
+ .date(ActualbudgetDate::from_i32(20240801))
+ .notes(Some(String::from("My notes 1")))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("1")
+ .amount(200)
+ .account_id(String::from("0"))
+ .payee(Some(String::from("0")))
+ .transfer_id(Some(String::from("0")))
+ .date(ActualbudgetDate::from_i32(20240801))
+ .notes(Some(String::from("My notes 2")))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("2")
+ .amount(-100)
+ .account_id(String::from("1"))
+ .payee(Some(String::from("1")))
+ .transfer_id(Some(String::from("0")))
+ .date(ActualbudgetDate::from_i32(20240801))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("2")
+ .amount(-200)
+ .account_id(String::from("1"))
+ .payee(Some(String::from("1")))
+ .transfer_id(Some(String::from("0")))
+ .date(ActualbudgetDate::from_i32(20240801))
+ .build()
+ .unwrap(),
+ ];
+
+ let (remaining_transactions, account_transfers) =
+ transform_account_transfers(&transactions).unwrap();
+
+ assert_eq!(0, remaining_transactions.len());
+ assert_eq!(2, account_transfers.len());
+
+ let account_transfer_0 = account_transfers
+ .iter()
+ .filter(|at| at.quantity == 100)
+ .last()
+ .unwrap();
+ assert_eq!("2024-08-01".parse::<DateUtc>().unwrap(), account_transfer_0.date);
+ assert_eq!(transform_id("0"), account_transfer_0.from_account_id);
+ assert_eq!(transform_id("1"), account_transfer_0.to_account_id);
+ assert_eq!("My notes 1", account_transfer_0.description);
+
+ let account_transfer_1 = account_transfers
+ .iter()
+ .filter(|at| at.quantity == 200)
+ .last()
+ .unwrap();
+ assert_eq!("2024-08-01".parse::<DateUtc>().unwrap(), account_transfer_1.date);
+ assert_eq!(transform_id("0"), account_transfer_1.from_account_id);
+ assert_eq!(transform_id("1"), account_transfer_1.to_account_id);
+ assert_eq!("My notes 2", account_transfer_1.description);
+ }
+
+ #[test]
+ fn when_two_account_transfers_different_dates_then_transforms() {
+ let transactions = [
+ make_fake_actualbudget_transaction_builder("0")
+ .amount(100)
+ .account_id(String::from("0"))
+ .payee(Some(String::from("0")))
+ .transfer_id(Some(String::from("0")))
+ .date(ActualbudgetDate::from_i32(20240801))
+ .notes(Some(String::from("My notes 1")))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("1")
+ .amount(100)
+ .account_id(String::from("0"))
+ .payee(Some(String::from("0")))
+ .transfer_id(Some(String::from("0")))
+ .date(ActualbudgetDate::from_i32(20240802))
+ .notes(Some(String::from("My notes 2")))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("2")
+ .amount(-100)
+ .account_id(String::from("1"))
+ .payee(Some(String::from("1")))
+ .transfer_id(Some(String::from("0")))
+ .date(ActualbudgetDate::from_i32(20240801))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("3")
+ .amount(-100)
+ .account_id(String::from("1"))
+ .payee(Some(String::from("1")))
+ .transfer_id(Some(String::from("0")))
+ .date(ActualbudgetDate::from_i32(20240802))
+ .build()
+ .unwrap(),
+ ];
+
+ let (remaining_transactions, account_transfers) =
+ transform_account_transfers(&transactions).unwrap();
+
+ assert_eq!(0, remaining_transactions.len());
+ assert_eq!(2, account_transfers.len());
+
+ let account_transfer_0 = account_transfers
+ .iter()
+ .filter(|at| at.date == "2024-08-01".parse().unwrap())
+ .last()
+ .unwrap();
+ assert_eq!(100, account_transfer_0.quantity);
+ assert_eq!(transform_id("0"), account_transfer_0.from_account_id);
+ assert_eq!(transform_id("1"), account_transfer_0.to_account_id);
+ assert_eq!("My notes 1", account_transfer_0.description);
+
+ let account_transfer_1 = account_transfers
+ .iter()
+ .filter(|at| at.date == "2024-08-02".parse().unwrap())
+ .last()
+ .unwrap();
+ assert_eq!(100, account_transfer_1.quantity);
+ assert_eq!(transform_id("0"), account_transfer_1.from_account_id);
+ assert_eq!(transform_id("1"), account_transfer_1.to_account_id);
+ assert_eq!("My notes 2", account_transfer_1.description);
+ }
+}
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
index 395b72b..66bb489 100644
--- a/rust/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs
+++ b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs
@@ -3,7 +3,7 @@ use schist_models::{account::Account, category::Category, category_transfer::Cat
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};
+use super::{transform_account_transfers, 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<SchistState> {
transform_state_without_context(actualbudget_state)
@@ -11,6 +11,9 @@ pub fn transform_state(actualbudget_state: ActualbudgetState) -> Result<SchistSt
}
fn transform_state_without_context(actualbudget_state: ActualbudgetState) -> Result<SchistState> {
+ let (remaining_transactions, account_transfers) = transform_account_transfers(
+ &actualbudget_state.actualbudget_transactions,
+ )?;
let first_transaction_date =
transform_date(&actualbudget_state.actualbudget_first_transaction_date)?;
let before_first_transaction_date = find_date_before(&first_transaction_date)?;
@@ -24,17 +27,18 @@ fn transform_state_without_context(actualbudget_state: ActualbudgetState) -> Res
);
let (transactions, transaction_categorisations): (Vec<Transaction>, Vec<TransactionCategorisation>) =
transform_transactions(
- &actualbudget_state.actualbudget_transactions,
+ &remaining_transactions,
&actualbudget_state.actualbudget_category_transfers_account,
)?;
let category_transfers: Vec<CategoryTransfer> = transform_category_transfers(
- &actualbudget_state.actualbudget_transactions,
+ &remaining_transactions,
&actualbudget_state.actualbudget_category_transfers_account,
)?;
Ok(SchistState {
first_transaction_date,
accounts,
+ account_transfers,
categories,
budget_drips,
transactions,