summaryrefslogtreecommitdiff
path: root/transformers
diff options
context:
space:
mode:
authorjoeac <me@joeac.net>2026-01-17 17:17:50 +0000
committerjoeac <me@joeac.net>2026-01-17 17:17:50 +0000
commitbf74f305894bde98033932235a4210c7d6865adf (patch)
tree051422c97fcb5d7c2eafd426ea66e1a906948747 /transformers
parentfe85652e4d3e8906824baddaafd05d19105579b8 (diff)
parentf69ec8416c4e6301dcfa06791b025785d7f61b6b (diff)
Merge pull request 'random-stuff' (#4) from random-stuff into main
Reviewed-on: https://git.joeac.net/joeac/schist/pulls/4
Diffstat (limited to 'transformers')
-rw-r--r--transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_date.rs51
-rw-r--r--transformers/actualbudget_transformer/actualbudget_queries/src/actualbudget_accounts.rs21
-rw-r--r--transformers/actualbudget_transformer/actualbudget_queries/tests/actualbudget_accounts.rs15
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/actualbudget_state.rs16
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/schist_state.rs8
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state.rs13
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_category_transfer.rs (renamed from transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_bucket_transfer.rs)14
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_accounts.rs4
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_bucket_transactions_from_budgets_not_carried_over.rs350
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_bucket_transactions_from_category_transfers.rs452
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs31
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_transactions.rs4
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_transactions_to_bucket_transfers.rs383
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets_to_drips.rs12
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_transaction_matchers.rs66
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_transfer_matchers.rs39
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/drip_matchers.rs6
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/mod.rs4
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/run.rs22
19 files changed, 997 insertions, 514 deletions
diff --git a/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_date.rs b/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_date.rs
index 13525fc..d8b6e87 100644
--- a/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_date.rs
+++ b/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_date.rs
@@ -1,8 +1,14 @@
-use diesel::{backend::Backend, deserialize::{FromSql, FromSqlRow}, expression::AsExpression, sql_types::Integer, sqlite::Sqlite};
+use diesel::{
+ backend::Backend,
+ deserialize::{FromSql, FromSqlRow},
+ expression::AsExpression,
+ sql_types::Integer,
+ sqlite::Sqlite,
+};
-#[derive(Clone, Hash, PartialEq, Eq, Debug, AsExpression, FromSqlRow)]
+#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug, AsExpression, FromSqlRow)]
#[diesel(sql_type=Integer)]
-pub struct ActualbudgetDate(i32);
+pub struct ActualbudgetDate(i64);
#[derive(Clone, PartialOrd, PartialEq, Eq)]
enum DatePart {
@@ -17,7 +23,12 @@ enum DatePart {
impl ActualbudgetDate {
pub fn to_iso(&self) -> String {
- format!("{0}-{1}-{2}T00:00:00.000Z", self.year(), self.month(), self.day())
+ format!(
+ "{0}-{1}-{2}T00:00:00.000Z",
+ self.year(),
+ self.month(),
+ self.day()
+ )
}
pub fn to_iso_date(&self) -> String {
@@ -40,7 +51,20 @@ impl ActualbudgetDate {
}
pub fn from_i32(i: i32) -> Self {
- ActualbudgetDate(i)
+ ActualbudgetDate(i.into())
+ }
+
+ pub fn from_ymd<T, S, U>(year: T, month: S, day: U) -> Self
+ where
+ T: Into<i64>,
+ S: Into<i64>,
+ U: Into<i64>,
+ {
+ Self(year.into() * 10_000 + month.into() * 100 + day.into())
+ }
+
+ pub fn oldest_valid_date() -> Self {
+ Self(10000101)
}
fn date_part(&self) -> DatePart {
@@ -51,7 +75,7 @@ impl ActualbudgetDate {
}
}
- fn dayified_value(&self) -> i32 {
+ fn dayified_value(&self) -> i64 {
match self.date_part() {
DatePart::DayOfMonth => self.0,
DatePart::MonthOfYear => self.0 * 100 + 1,
@@ -74,6 +98,19 @@ impl PartialOrd for ActualbudgetDate {
impl FromSql<Integer, Sqlite> for ActualbudgetDate {
fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> diesel::deserialize::Result<Self> {
let int: i32 = FromSql::<Integer, Sqlite>::from_sql(bytes)?;
- Ok(ActualbudgetDate(int))
+ Ok(ActualbudgetDate(int.into()))
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::ActualbudgetDate;
+
+ #[test]
+ fn when_from_ymd_2026_01_16_then_constructs_2026_01_16() {
+ let actualbudget_date = ActualbudgetDate::from_ymd(2026, 1, 16);
+ assert_eq!(actualbudget_date.year(), "2026");
+ assert_eq!(actualbudget_date.month(), "01");
+ assert_eq!(actualbudget_date.day(), "16");
}
}
diff --git a/transformers/actualbudget_transformer/actualbudget_queries/src/actualbudget_accounts.rs b/transformers/actualbudget_transformer/actualbudget_queries/src/actualbudget_accounts.rs
index d499208..8a590a1 100644
--- a/transformers/actualbudget_transformer/actualbudget_queries/src/actualbudget_accounts.rs
+++ b/transformers/actualbudget_transformer/actualbudget_queries/src/actualbudget_accounts.rs
@@ -1,25 +1,14 @@
use actualbudget_models::ActualbudgetAccount;
-use actualbudget_schema::actualbudget_schema::accounts::{
- self as accounts_schema,
- dsl::accounts as accounts_table,
-};
+use actualbudget_schema::actualbudget_schema::accounts::dsl::accounts as accounts_table;
use anyhow::{Context, Result};
-use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SelectableHelper, SqliteConnection};
+use diesel::{QueryDsl, RunQueryDsl, SelectableHelper, SqliteConnection};
-pub fn get_all_actualbudget_accounts(connection: &mut SqliteConnection) -> Result<Vec<ActualbudgetAccount>> {
+pub fn get_all_actualbudget_accounts(
+ connection: &mut SqliteConnection,
+) -> Result<Vec<ActualbudgetAccount>> {
let actualbudget_accounts = accounts_table
.select(ActualbudgetAccount::as_select())
.load(connection)
.with_context(|| "failed to get all Actualbudget accounts")?;
Ok(actualbudget_accounts)
}
-
-pub fn get_category_transfers_account(connection: &mut SqliteConnection) -> Result<Option<ActualbudgetAccount>> {
- let category_transfers_account = accounts_table
- .filter(accounts_schema::name.eq("Category transfers"))
- .select(ActualbudgetAccount::as_select())
- .first(connection)
- .optional()
- .with_context(|| "failed to get Category Transfers account")?;
- Ok(category_transfers_account)
-}
diff --git a/transformers/actualbudget_transformer/actualbudget_queries/tests/actualbudget_accounts.rs b/transformers/actualbudget_transformer/actualbudget_queries/tests/actualbudget_accounts.rs
index 954cac1..78953f2 100644
--- a/transformers/actualbudget_transformer/actualbudget_queries/tests/actualbudget_accounts.rs
+++ b/transformers/actualbudget_transformer/actualbudget_queries/tests/actualbudget_accounts.rs
@@ -1,6 +1,6 @@
pub mod common;
-use actualbudget_queries::actualbudget_accounts::{get_all_actualbudget_accounts, get_category_transfers_account};
+use actualbudget_queries::actualbudget_accounts::get_all_actualbudget_accounts;
use common::{account_names::ACCOUNT_NAMES, db_url::DB_URL};
use diesel::{Connection, SqliteConnection};
@@ -17,16 +17,3 @@ fn given_test_database_when_get_all_accounts_then_return_accounts() {
assert!(accounts.iter().any(|a| a.name.as_str() == *account_name));
}
}
-
-#[test]
-fn given_test_database_when_get_category_transfer_account_then_return_category_transfer_account() {
- let connection = &mut SqliteConnection::establish(DB_URL).unwrap();
-
- let account = get_category_transfers_account(connection);
-
- assert!(account.is_ok());
- let account = account.unwrap();
- assert!(account.is_some());
- let account = account.unwrap();
- assert!(account.name == "Category transfers");
-}
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/actualbudget_state.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/actualbudget_state.rs
index cda63b1..22c1eef 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/actualbudget_state.rs
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/actualbudget_state.rs
@@ -1,12 +1,18 @@
use std::cmp::min;
use actualbudget_models::{
- ActualbudgetAccount, ActualbudgetCategory, ActualbudgetCategoryGroup, ActualbudgetDate, ActualbudgetPayee, ActualbudgetTransaction, ActualbudgetZeroBudget
+ ActualbudgetAccount, ActualbudgetCategory, ActualbudgetCategoryGroup, ActualbudgetDate,
+ ActualbudgetPayee, ActualbudgetTransaction, ActualbudgetZeroBudget,
};
use actualbudget_queries::{
- actualbudget_accounts::get_all_actualbudget_accounts, actualbudget_categories::get_all_actualbudget_categories, actualbudget_category_groups::get_all_actualbudget_category_groups, actualbudget_payees::get_all_actualbudget_payees, actualbudget_transactions::{
+ actualbudget_accounts::get_all_actualbudget_accounts,
+ actualbudget_categories::get_all_actualbudget_categories,
+ actualbudget_category_groups::get_all_actualbudget_category_groups,
+ actualbudget_payees::get_all_actualbudget_payees,
+ actualbudget_transactions::{
get_all_actualbudget_transactions, get_first_actualbudget_transaction_date,
- }, actualbudget_zero_budgets::{get_all_actualbudget_zero_budgets, get_first_zero_budget_date}
+ },
+ actualbudget_zero_budgets::{get_all_actualbudget_zero_budgets, get_first_zero_budget_date},
};
use anyhow::Result;
use diesel::SqliteConnection;
@@ -31,7 +37,9 @@ pub fn read_actualbudget_state(
),
actualbudget_accounts: get_all_actualbudget_accounts(actualbudget_connection)?,
actualbudget_categories: get_all_actualbudget_categories(actualbudget_connection)?,
- actualbudget_category_groups: get_all_actualbudget_category_groups(actualbudget_connection)?,
+ actualbudget_category_groups: get_all_actualbudget_category_groups(
+ actualbudget_connection,
+ )?,
actualbudget_payees: get_all_actualbudget_payees(actualbudget_connection)?,
actualbudget_zero_budgets: get_all_actualbudget_zero_budgets(actualbudget_connection)?,
actualbudget_transactions: get_all_actualbudget_transactions(actualbudget_connection)?,
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/schist_state.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/schist_state.rs
index ca5a15c..f42fd16 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/schist_state.rs
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/schist_state.rs
@@ -1,7 +1,7 @@
use anyhow::Result;
use diesel::SqliteConnection;
use schist_models::{
- Account, AccountTransfer, Bucket, BucketTransfer, DateUtc, Drip, Pipe, Transaction,
+ Account, AccountTransfer, Bucket, BucketTransaction, DateUtc, Drip, Pipe, Transaction,
};
pub struct SchistState {
@@ -12,7 +12,7 @@ pub struct SchistState {
pub budget_drips: Vec<Drip>,
pub transactions: Vec<Transaction>,
pub pipes: Vec<Pipe>,
- pub bucket_transfers: Vec<BucketTransfer>,
+ pub bucket_transactions: Vec<BucketTransaction>,
}
pub fn export_schist_state(
@@ -32,8 +32,8 @@ pub fn export_schist_state(
schist_connection,
)?;
schist_queries::pipes::insert_pipes(&schist_state.pipes, schist_connection)?;
- schist_queries::bucket_transfers::insert_bucket_transfers(
- &schist_state.bucket_transfers,
+ schist_queries::bucket_transactions::insert_bucket_transactions(
+ &schist_state.bucket_transactions,
schist_connection,
)?;
Ok(())
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state.rs
index a116649..75aa5bb 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state.rs
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state.rs
@@ -1,27 +1,30 @@
mod is_account_transfer;
-mod is_bucket_transfer;
+mod is_category_transfer;
mod transform_account_transfers;
mod transform_accounts;
+mod transform_bucket_transactions_from_budgets_not_carried_over;
+mod transform_bucket_transactions_from_category_transfers;
mod transform_categories_to_buckets;
mod transform_date;
mod transform_id;
mod transform_state;
mod transform_transactions;
-mod transform_transactions_to_bucket_transfers;
mod transform_zero_budgets_to_drips;
mod transform_zero_budgets_to_pipes;
use is_account_transfer::is_account_transfer;
use is_account_transfer::is_not_account_transfer;
-use is_bucket_transfer::is_bucket_transfer;
-use is_bucket_transfer::is_not_bucket_transfer;
+use is_category_transfer::is_category_transfer;
+use is_category_transfer::is_not_category_transfer;
use transform_account_transfers::transform_account_transfers;
use transform_accounts::transform_accounts;
+use transform_bucket_transactions_from_budgets_not_carried_over::transform_bucket_transactions_from_budgets_not_carried_over;
+use transform_bucket_transactions_from_category_transfers::transform_bucket_transactions_from_category_transfers;
use transform_categories_to_buckets::transform_categories_to_buckets;
use transform_date::transform_date;
use transform_id::transform_id;
use transform_transactions::transform_transactions;
-use transform_transactions_to_bucket_transfers::transform_transactions_to_bucket_transfers;
+use transform_zero_budgets_to_drips::transform_zero_budgets_to_drips;
use transform_zero_budgets_to_pipes::transform_zero_budgets_to_pipes;
pub use transform_state::transform_state;
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_bucket_transfer.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_category_transfer.rs
index 188697e..75d48b7 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_bucket_transfer.rs
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_category_transfer.rs
@@ -1,14 +1,14 @@
use actualbudget_models::ActualbudgetTransaction;
-pub fn is_bucket_transfer(
+pub fn is_category_transfer(
transaction: &ActualbudgetTransaction,
transactions: &[ActualbudgetTransaction],
) -> bool {
- is_parent_bucket_transfer(transaction, transactions)
- || is_child_bucket_transfer(transaction, transactions)
+ is_parent_category_transfer(transaction, transactions)
+ || is_child_category_transfer(transaction, transactions)
}
-fn is_parent_bucket_transfer(
+fn is_parent_category_transfer(
transaction: &ActualbudgetTransaction,
transactions: &[ActualbudgetTransaction],
) -> bool {
@@ -24,7 +24,7 @@ fn is_parent_bucket_transfer(
sum_of_children_amounts == 0
}
-fn is_child_bucket_transfer(
+fn is_child_category_transfer(
transaction: &ActualbudgetTransaction,
transactions: &[ActualbudgetTransaction],
) -> bool {
@@ -38,9 +38,9 @@ fn is_parent_and_child(parent: &ActualbudgetTransaction, child: &ActualbudgetTra
parent.is_parent && child.is_child && child.parent_id.clone().is_some_and(|id| id == parent.id)
}
-pub fn is_not_bucket_transfer(
+pub fn is_not_category_transfer(
transaction: &ActualbudgetTransaction,
transactions: &[ActualbudgetTransaction],
) -> bool {
- !is_bucket_transfer(transaction, transactions)
+ !is_category_transfer(transaction, transactions)
}
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_accounts.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_accounts.rs
index 76eaf0c..13f63c0 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_accounts.rs
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_accounts.rs
@@ -1,7 +1,7 @@
use actualbudget_models::{ActualbudgetAccount, ActualbudgetTransaction};
use schist_models::{Account, DateUtc};
-use super::{is_bucket_transfer, transform_id};
+use super::{is_category_transfer, transform_id};
pub fn transform_accounts(
actualbudget_accounts: &[ActualbudgetAccount],
@@ -22,7 +22,7 @@ fn is_empty_account(
actualbudget_transactions.iter().all(|t| {
t.is_starting_balance() && t.amount == 0
|| t.account_id != actualbudget_account.id
- || is_bucket_transfer(t, actualbudget_transactions)
+ || is_category_transfer(t, actualbudget_transactions)
})
}
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_bucket_transactions_from_budgets_not_carried_over.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_bucket_transactions_from_budgets_not_carried_over.rs
new file mode 100644
index 0000000..1bdeaff
--- /dev/null
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_bucket_transactions_from_budgets_not_carried_over.rs
@@ -0,0 +1,350 @@
+use std::collections::HashMap;
+
+use actualbudget_models::{ActualbudgetDate, ActualbudgetTransaction, ActualbudgetZeroBudget};
+use anyhow::Result;
+use itertools::Itertools;
+use schist_models::BucketTransaction;
+use schist_traits::Dateable;
+
+use crate::transform_state::{transform_date, transform_id};
+
+pub fn transform_bucket_transactions_from_budgets_not_carried_over(
+ actualbudget_zero_budgets: &[ActualbudgetZeroBudget],
+ actualbudget_transactions: &[ActualbudgetTransaction],
+) -> Result<Vec<BucketTransaction>> {
+ let budgets_not_carried_over: Vec<&ActualbudgetZeroBudget> = actualbudget_zero_budgets
+ .iter()
+ .filter(|zb| !zb.do_carry_over)
+ .sorted_unstable_by_key(|zb| zb.month)
+ .collect();
+ let mut last_reset_to_zero_per_category_id: HashMap<String, ActualbudgetDate> =
+ HashMap::from_iter(
+ budgets_not_carried_over
+ .iter()
+ .unique_by(|zb| zb.category_id.clone())
+ .map(|zb| {
+ (
+ zb.category_id.clone(),
+ ActualbudgetDate::oldest_valid_date(),
+ )
+ }),
+ );
+
+ let mut bucket_transactions = Vec::new();
+ for budget in budgets_not_carried_over {
+ let last_not_carried_over_month_start: ActualbudgetDate =
+ last_reset_to_zero_per_category_id
+ .get(&budget.category_id)
+ .cloned()
+ .unwrap_or_else(|| ActualbudgetDate::oldest_valid_date());
+ let start_of_next_month = transform_date(&budget.month)?.first_day_in_next_month();
+ let actualbudget_start_of_next_month = ActualbudgetDate::from_ymd(
+ start_of_next_month.year(),
+ start_of_next_month.month(),
+ start_of_next_month.day(),
+ );
+
+ let balance_from_zero_budgets: i32 = actualbudget_zero_budgets
+ .iter()
+ .filter(|zb| {
+ zb.category_id == budget.category_id
+ && zb.month >= last_not_carried_over_month_start
+ && zb.month < actualbudget_start_of_next_month
+ })
+ .map(|zb| zb.amount)
+ .sum();
+ let balance_from_transactions: i32 = actualbudget_transactions
+ .iter()
+ .filter(|t| {
+ t.category_id
+ .as_ref()
+ .is_some_and(|id| *id == budget.category_id)
+ && t.date >= last_not_carried_over_month_start
+ && t.date < actualbudget_start_of_next_month
+ })
+ .map(|t| t.amount)
+ .sum();
+ let balance_at_end_of_month = balance_from_zero_budgets + balance_from_transactions;
+
+ if balance_at_end_of_month < 0 {
+ last_reset_to_zero_per_category_id
+ .insert(budget.category_id.clone(), actualbudget_start_of_next_month);
+ bucket_transactions.push(BucketTransaction {
+ id: transform_id(&budget.id),
+ description: format!("Actualbudget budget <{}> not carried over", budget.id),
+ date: start_of_next_month,
+ amount: -balance_at_end_of_month,
+ bucket_id: transform_id(&budget.category_id),
+ });
+ }
+ }
+
+ Ok(bucket_transactions)
+}
+
+#[cfg(test)]
+mod test {
+ use actualbudget_fakes::{
+ actualbudget_transaction::make_fake_actualbudget_transaction_builder,
+ actualbudget_zero_budget::make_fake_actualbudget_zero_budget_builder,
+ };
+ use actualbudget_models::ActualbudgetDate;
+ use schist_models::{BucketTransaction, DateUtc};
+
+ use crate::transform_state::{
+ transform_bucket_transactions_from_budgets_not_carried_over::transform_bucket_transactions_from_budgets_not_carried_over,
+ transform_id::transform_id,
+ };
+
+ #[test]
+ fn no_resets() {
+ let actualbudget_zero_budgets = [
+ make_fake_actualbudget_zero_budget_builder("zb0")
+ .category_id(String::from("c0"))
+ .amount(100)
+ .do_carry_over(false)
+ .month(ActualbudgetDate::from_i32(202601))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_zero_budget_builder("zb1")
+ .category_id(String::from("c0"))
+ .amount(100)
+ .do_carry_over(false)
+ .month(ActualbudgetDate::from_i32(202602))
+ .build()
+ .unwrap(),
+ ];
+
+ let actualbudget_transactions = [
+ make_fake_actualbudget_transaction_builder("t0")
+ .category_id(Some(String::from("c0")))
+ .amount(-50)
+ .date(ActualbudgetDate::from_ymd(2026, 1, 1))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("t0")
+ .category_id(Some(String::from("c0")))
+ .amount(-50)
+ .date(ActualbudgetDate::from_ymd(2026, 1, 31))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("t1")
+ .category_id(Some(String::from("c1")))
+ .amount(-101)
+ .date(ActualbudgetDate::from_ymd(2026, 1, 21))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("t1")
+ .category_id(Some(String::from("c1")))
+ .amount(-101)
+ .date(ActualbudgetDate::from_ymd(2026, 2, 21))
+ .build()
+ .unwrap(),
+ ];
+
+ let result = transform_bucket_transactions_from_budgets_not_carried_over(
+ &actualbudget_zero_budgets,
+ &actualbudget_transactions,
+ );
+ assert!(result.is_ok());
+ let result = result.unwrap();
+ assert_eq!(Vec::<BucketTransaction>::new(), result);
+ }
+
+ #[test]
+ fn one_reset() {
+ let actualbudget_zero_budgets = [
+ make_fake_actualbudget_zero_budget_builder("zb0")
+ .category_id(String::from("c0"))
+ .amount(100)
+ .do_carry_over(false)
+ .month(ActualbudgetDate::from_i32(202601))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_zero_budget_builder("zb1")
+ .category_id(String::from("c0"))
+ .amount(100)
+ .do_carry_over(false)
+ .month(ActualbudgetDate::from_i32(202602))
+ .build()
+ .unwrap(),
+ ];
+
+ let actualbudget_transactions = [
+ make_fake_actualbudget_transaction_builder("t0")
+ .category_id(Some(String::from("c0")))
+ .amount(-50)
+ .date(ActualbudgetDate::from_ymd(2026, 1, 1))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("t0")
+ .category_id(Some(String::from("c0")))
+ .amount(-51)
+ .date(ActualbudgetDate::from_ymd(2026, 1, 31))
+ .build()
+ .unwrap(),
+ ];
+
+ let result = transform_bucket_transactions_from_budgets_not_carried_over(
+ &actualbudget_zero_budgets,
+ &actualbudget_transactions,
+ );
+ assert!(result.is_ok());
+ let result = result.unwrap();
+ assert_eq!(result.len(), 1);
+ let transaction = result.first().unwrap();
+ assert_eq!(transaction.amount, 1);
+ assert_eq!(transaction.bucket_id, transform_id("c0"));
+ assert_eq!(transaction.date, DateUtc::from_ymd(2026, 2, 1).unwrap());
+ }
+
+ #[test]
+ fn two_resets() {
+ let actualbudget_zero_budgets = [
+ make_fake_actualbudget_zero_budget_builder("zb0")
+ .category_id(String::from("c0"))
+ .amount(100)
+ .do_carry_over(false)
+ .month(ActualbudgetDate::from_i32(202401))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_zero_budget_builder("zb1")
+ .category_id(String::from("c0"))
+ .amount(101)
+ .do_carry_over(false)
+ .month(ActualbudgetDate::from_i32(202402))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_zero_budget_builder("zb2")
+ .category_id(String::from("c0"))
+ .amount(102)
+ .do_carry_over(false)
+ .month(ActualbudgetDate::from_i32(202403))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_zero_budget_builder("zb3")
+ .category_id(String::from("c0"))
+ .amount(103)
+ .do_carry_over(false)
+ .month(ActualbudgetDate::from_i32(202404))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_zero_budget_builder("zb4")
+ .category_id(String::from("c0"))
+ .amount(104)
+ .do_carry_over(false)
+ .month(ActualbudgetDate::from_i32(202405))
+ .build()
+ .unwrap(),
+ ];
+
+ let actualbudget_transactions = [
+ make_fake_actualbudget_transaction_builder("t0")
+ .category_id(Some(String::from("c0")))
+ .amount(-50)
+ .date(ActualbudgetDate::from_ymd(2024, 1, 1))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("t1")
+ .category_id(Some(String::from("c0")))
+ .amount(-50)
+ .date(ActualbudgetDate::from_ymd(2024, 1, 31))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("t2")
+ .category_id(Some(String::from("c0")))
+ .amount(-50)
+ .date(ActualbudgetDate::from_ymd(2024, 2, 1))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("t3")
+ .category_id(Some(String::from("c0")))
+ .amount(-53)
+ .date(ActualbudgetDate::from_ymd(2024, 2, 29))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("t4")
+ .category_id(Some(String::from("c0")))
+ .amount(-92)
+ .date(ActualbudgetDate::from_ymd(2024, 3, 15))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("t5")
+ .category_id(Some(String::from("c0")))
+ .amount(-93)
+ .date(ActualbudgetDate::from_ymd(2024, 4, 15))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("t6")
+ .category_id(Some(String::from("c0")))
+ .amount(-127)
+ .date(ActualbudgetDate::from_ymd(2024, 5, 15))
+ .build()
+ .unwrap(),
+ ];
+
+ let result = transform_bucket_transactions_from_budgets_not_carried_over(
+ &actualbudget_zero_budgets,
+ &actualbudget_transactions,
+ );
+ assert!(result.is_ok());
+ let result = result.unwrap();
+ assert_eq!(result.len(), 2);
+
+ let transaction = result.iter().find(|bt| bt.amount == 2);
+ assert!(transaction.is_some());
+ let transaction = transaction.unwrap();
+ assert_eq!(transaction.amount, 2);
+ assert_eq!(transaction.bucket_id, transform_id("c0"));
+ assert_eq!(transaction.date, DateUtc::from_ymd(2024, 3, 1).unwrap());
+
+ let transaction = result.iter().find(|bt| bt.amount == 3);
+ assert!(transaction.is_some());
+ let transaction = transaction.unwrap();
+ assert_eq!(transaction.bucket_id, transform_id("c0"));
+ assert_eq!(transaction.date, DateUtc::from_ymd(2024, 6, 1).unwrap());
+ }
+
+ #[test]
+ fn carried_over() {
+ let actualbudget_zero_budgets = [
+ make_fake_actualbudget_zero_budget_builder("zb0")
+ .category_id(String::from("c0"))
+ .amount(100)
+ .do_carry_over(true)
+ .month(ActualbudgetDate::from_i32(202601))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_zero_budget_builder("zb1")
+ .category_id(String::from("c0"))
+ .amount(100)
+ .do_carry_over(true)
+ .month(ActualbudgetDate::from_i32(202602))
+ .build()
+ .unwrap(),
+ ];
+
+ let actualbudget_transactions = [
+ make_fake_actualbudget_transaction_builder("t0")
+ .category_id(Some(String::from("c0")))
+ .amount(-50)
+ .date(ActualbudgetDate::from_ymd(2026, 1, 1))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("t0")
+ .category_id(Some(String::from("c0")))
+ .amount(-51)
+ .date(ActualbudgetDate::from_ymd(2026, 1, 31))
+ .build()
+ .unwrap(),
+ ];
+
+ let result = transform_bucket_transactions_from_budgets_not_carried_over(
+ &actualbudget_zero_budgets,
+ &actualbudget_transactions,
+ );
+ assert!(result.is_ok());
+ let result = result.unwrap();
+ assert_eq!(result.len(), 0);
+ }
+}
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_bucket_transactions_from_category_transfers.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_bucket_transactions_from_category_transfers.rs
new file mode 100644
index 0000000..4dce8fa
--- /dev/null
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_bucket_transactions_from_category_transfers.rs
@@ -0,0 +1,452 @@
+use actualbudget_models::ActualbudgetTransaction;
+use anyhow::{bail, Context, Result};
+use schist_models::BucketTransaction;
+
+use crate::transform_state::transform_date;
+
+use super::{is_category_transfer, transform_id};
+
+pub fn transform_bucket_transactions_from_category_transfers(
+ actualbudget_transactions: &[ActualbudgetTransaction],
+) -> Result<Vec<BucketTransaction>> {
+ let actualbudget_category_transfers = actualbudget_transactions
+ .iter()
+ .filter(|transaction| is_category_transfer(transaction, actualbudget_transactions));
+ let actualbudget_parent_category_transfers: Vec<ActualbudgetTransaction> =
+ actualbudget_category_transfers
+ .clone()
+ .filter(|t| t.is_parent)
+ .map(ActualbudgetTransaction::clone)
+ .collect();
+ let actualbudget_child_category_transfers: Vec<ActualbudgetTransaction> =
+ actualbudget_category_transfers
+ .filter(|t| t.is_child)
+ .map(ActualbudgetTransaction::clone)
+ .collect();
+
+ let mut result = Vec::new();
+ for pct in actualbudget_parent_category_transfers {
+ result.extend(
+ transform_category_transfer(&pct, &actualbudget_child_category_transfers)
+ .with_context(|| "failed to transform bucket transactions")?,
+ )
+ }
+ Ok(result)
+}
+
+fn transform_category_transfer(
+ actualbudget_parent_category_transfer: &ActualbudgetTransaction,
+ actualbudget_child_category_transfers: &[ActualbudgetTransaction],
+) -> Result<Vec<BucketTransaction>> {
+ transform_category_transfer_without_context(
+ actualbudget_parent_category_transfer,
+ actualbudget_child_category_transfers,
+ )
+ .with_context(|| "failed to transform category transfer")
+}
+
+fn transform_category_transfer_without_context(
+ actualbudget_parent_category_transfer: &ActualbudgetTransaction,
+ actualbudget_child_category_transfers: &[ActualbudgetTransaction],
+) -> Result<Vec<BucketTransaction>> {
+ let actualbudget_child_category_transfers = actualbudget_child_category_transfers
+ .iter()
+ .filter(|t| {
+ t.parent_id
+ .clone()
+ .is_some_and(|parent_id| parent_id == actualbudget_parent_category_transfer.id)
+ })
+ .map(ActualbudgetTransaction::clone);
+
+ let withdrawals: Vec<ActualbudgetTransaction> = actualbudget_child_category_transfers
+ .clone()
+ .filter(|t| t.amount < 0)
+ .collect();
+ if withdrawals.len() != 1 {
+ bail!(
+ "actualbudget parent category transfer {} had {} withdrawals",
+ actualbudget_parent_category_transfer.id,
+ withdrawals.len(),
+ );
+ }
+ let withdrawal = withdrawals.get(0).unwrap();
+ println!("withdrawal: {:?}", withdrawal);
+
+ if withdrawal.category_id.is_none() {
+ bail!(
+ "actualbudget child withdrawal {} had no category ID",
+ withdrawal.id
+ );
+ }
+ let withdrawal_bucket_id: i32 = transform_id(&withdrawal.category_id.clone().unwrap());
+
+ let deposits: Vec<ActualbudgetTransaction> = actualbudget_child_category_transfers
+ .clone()
+ .filter(|t| t.amount > 0)
+ .collect();
+ if deposits.len() != 1 {
+ bail!(
+ "actualbudget parent category transfer {} had {} deposits",
+ actualbudget_parent_category_transfer.id,
+ deposits.len(),
+ );
+ }
+ let deposit = deposits.get(0).unwrap();
+ println!("deposit: {:?}", deposit);
+
+ if deposit.category_id.is_none() {
+ bail!(
+ "actualbudget child deposit {} had no category ID",
+ deposit.id
+ );
+ }
+ let deposit_id: i32 = transform_id(&deposit.category_id.clone().unwrap());
+
+ if withdrawal.amount != -deposit.amount {
+ bail!("actualbudget category transfer amounts did not sum to zero");
+ }
+
+ let amount: i32 = deposit.amount.try_into().unwrap();
+ let date = transform_date(&actualbudget_parent_category_transfer.date)?;
+
+ Ok(vec![
+ BucketTransaction {
+ id: transform_id(&deposit.id),
+ description: actualbudget_parent_category_transfer
+ .notes
+ .clone()
+ .unwrap_or_else(String::new),
+ amount: -amount,
+ date,
+ bucket_id: withdrawal_bucket_id,
+ },
+ BucketTransaction {
+ id: transform_id(&withdrawal.id),
+ description: actualbudget_parent_category_transfer
+ .notes
+ .clone()
+ .unwrap_or_else(String::new),
+ amount,
+ date,
+ bucket_id: deposit_id,
+ },
+ ])
+}
+
+#[cfg(test)]
+mod test {
+ use actualbudget_fakes::actualbudget_transaction::make_fake_actualbudget_transaction_builder;
+ use actualbudget_models::{
+ ActualbudgetDate, ActualbudgetTransaction, ActualbudgetTransactionBuilder,
+ };
+ use schist_models::{BucketTransaction, DateUtc};
+
+ use crate::transform_state::transform_id;
+
+ use super::transform_bucket_transactions_from_category_transfers;
+
+ #[test]
+ fn when_one_category_transfer_then_transforms() {
+ let actualbudget_transactions = [
+ make_fake_parent_transaction_builder("0")
+ .date(ActualbudgetDate::from_ymd(2026, 01, 16))
+ .build()
+ .unwrap(),
+ make_fake_child_withdrawal("1", "0"),
+ make_fake_child_deposit("2", "0"),
+ ];
+
+ let result =
+ transform_bucket_transactions_from_category_transfers(&actualbudget_transactions);
+
+ assert!(result.is_ok());
+ let bucket_transactions = result.unwrap();
+ assert_eq!(2, bucket_transactions.len());
+
+ let bucket_withdrawal = bucket_transactions.iter().find(|bt| bt.amount < 0).unwrap();
+ assert_eq!(-100, bucket_withdrawal.amount);
+ assert_eq!("I was a parent transaction", bucket_withdrawal.description);
+ assert_eq!(transform_id("0"), bucket_withdrawal.bucket_id);
+ assert_eq!(
+ DateUtc::from_ymd(2026, 1, 16).unwrap(),
+ bucket_withdrawal.date
+ );
+
+ let bucket_deposit = bucket_transactions.iter().find(|bt| bt.amount > 0).unwrap();
+ assert_eq!(100, bucket_deposit.amount);
+ assert_eq!("I was a parent transaction", bucket_deposit.description);
+ assert_eq!(transform_id("1"), bucket_deposit.bucket_id);
+ assert_eq!(DateUtc::from_ymd(2026, 1, 16).unwrap(), bucket_deposit.date);
+ }
+
+ #[test]
+ fn when_many_transactions_then_transforms() {
+ let actualbudget_transactions = [
+ make_fake_parent_transaction_builder("0")
+ .date(ActualbudgetDate::from_ymd(2026, 1, 16))
+ .build()
+ .unwrap(),
+ make_fake_child_withdrawal_builder("1", 100, "0")
+ .category_id(Some(String::from("0")))
+ .build()
+ .unwrap(),
+ make_fake_child_deposit_builder("2", 100, "0")
+ .category_id(Some(String::from("1")))
+ .build()
+ .unwrap(),
+ make_fake_parent_transaction_builder("3")
+ .date(ActualbudgetDate::from_ymd(2026, 1, 21))
+ .build()
+ .unwrap(),
+ make_fake_child_withdrawal_builder("4", 200, "3")
+ .category_id(Some(String::from("1")))
+ .build()
+ .unwrap(),
+ make_fake_child_deposit_builder("5", 200, "3")
+ .category_id(Some(String::from("0")))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("6")
+ .amount(300)
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("7")
+ .amount(-400)
+ .build()
+ .unwrap(),
+ ];
+
+ let result =
+ transform_bucket_transactions_from_category_transfers(&actualbudget_transactions);
+
+ assert!(result.is_ok());
+ let bucket_transactions = result.unwrap();
+ assert_eq!(4, bucket_transactions.len());
+
+ let bucket_withdrawal_0: Vec<&BucketTransaction> = bucket_transactions
+ .iter()
+ .filter(|ct| ct.amount == -100)
+ .collect();
+ assert_eq!(1, bucket_withdrawal_0.len());
+ let bucket_withdrawal_0 = bucket_withdrawal_0.get(0).unwrap();
+ assert_eq!(transform_id("0"), bucket_withdrawal_0.bucket_id);
+ assert_eq!(
+ DateUtc::from_ymd(2026, 1, 16).unwrap(),
+ bucket_withdrawal_0.date
+ );
+
+ let bucket_deposit_0: Vec<&BucketTransaction> = bucket_transactions
+ .iter()
+ .filter(|ct| ct.amount == 100)
+ .collect();
+ assert_eq!(1, bucket_deposit_0.len());
+ let bucket_deposit_0 = bucket_deposit_0.get(0).unwrap();
+ assert_eq!(transform_id("1"), bucket_deposit_0.bucket_id);
+ assert_eq!(
+ DateUtc::from_ymd(2026, 1, 16).unwrap(),
+ bucket_deposit_0.date
+ );
+
+ let bucket_withdrawal_1: Vec<&BucketTransaction> = bucket_transactions
+ .iter()
+ .filter(|ct| ct.amount == -200)
+ .collect();
+ assert_eq!(1, bucket_withdrawal_1.len());
+ let bucket_withdrawal_1 = bucket_withdrawal_1.get(0).unwrap();
+ assert_eq!(transform_id("1"), bucket_withdrawal_1.bucket_id);
+ assert_eq!(
+ DateUtc::from_ymd(2026, 1, 21).unwrap(),
+ bucket_withdrawal_1.date
+ );
+
+ let bucket_deposit_1: Vec<&BucketTransaction> = bucket_transactions
+ .iter()
+ .filter(|ct| ct.amount == 200)
+ .collect();
+ assert_eq!(1, bucket_deposit_1.len());
+ let bucket_deposit_1 = bucket_deposit_1.get(0).unwrap();
+ assert_eq!(transform_id("0"), bucket_deposit_1.bucket_id);
+ assert_eq!(
+ DateUtc::from_ymd(2026, 1, 21).unwrap(),
+ bucket_deposit_1.date
+ );
+ }
+
+ #[test]
+ fn when_multiple_child_withdrawals_then_err() {
+ let actualbudget_transactions = [
+ make_fake_parent_transaction("0"),
+ make_fake_child_withdrawal_builder("1", 50, "0")
+ .build()
+ .unwrap(),
+ make_fake_child_withdrawal_builder("2", 50, "0")
+ .build()
+ .unwrap(),
+ make_fake_child_deposit_builder("3", 100, "0")
+ .build()
+ .unwrap(),
+ ];
+
+ let result =
+ transform_bucket_transactions_from_category_transfers(&actualbudget_transactions);
+
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn when_multiple_child_deposits_then_err() {
+ let actualbudget_transactions = [
+ make_fake_parent_transaction("0"),
+ make_fake_child_withdrawal_builder("1", 100, "0")
+ .build()
+ .unwrap(),
+ make_fake_child_deposit_builder("2", 50, "0")
+ .build()
+ .unwrap(),
+ make_fake_child_deposit_builder("3", 50, "0")
+ .build()
+ .unwrap(),
+ ];
+
+ let result =
+ transform_bucket_transactions_from_category_transfers(&actualbudget_transactions);
+
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn when_no_child_withdrawals_then_return_empty() {
+ let actualbudget_transactions = [
+ make_fake_parent_transaction("0"),
+ make_fake_child_deposit("1", "0"),
+ ];
+
+ let result =
+ transform_bucket_transactions_from_category_transfers(&actualbudget_transactions);
+
+ assert!(result.is_ok());
+ assert_eq!(0, result.unwrap().len());
+ }
+
+ #[test]
+ fn when_no_child_deposits_then_return_empty() {
+ let actualbudget_transactions = [
+ make_fake_parent_transaction("0"),
+ make_fake_child_withdrawal("1", "0"),
+ ];
+
+ let result =
+ transform_bucket_transactions_from_category_transfers(&actualbudget_transactions);
+
+ assert!(result.is_ok());
+ assert_eq!(0, result.unwrap().len());
+ }
+
+ #[test]
+ fn when_child_amounts_dont_add_up_to_zero_then_return_empty() {
+ let actualbudget_transactions = [
+ make_fake_parent_transaction("0"),
+ make_fake_child_withdrawal_builder("1", 50, "0")
+ .build()
+ .unwrap(),
+ make_fake_child_deposit_builder("2", 100, "0")
+ .build()
+ .unwrap(),
+ ];
+
+ let result =
+ transform_bucket_transactions_from_category_transfers(&actualbudget_transactions);
+
+ assert!(result.is_ok());
+ assert_eq!(0, result.unwrap().len());
+ }
+
+ #[test]
+ fn when_child_category_transfer_has_no_category_then_err() {
+ let actualbudget_transactions_0 = [
+ make_fake_parent_transaction("0"),
+ make_fake_child_withdrawal_builder("1", 100, "0")
+ .category_id(None)
+ .build()
+ .unwrap(),
+ make_fake_child_deposit_builder("2", 100, "0")
+ .build()
+ .unwrap(),
+ ];
+ let actualbudget_transactions_1 = [
+ make_fake_parent_transaction("0"),
+ make_fake_child_withdrawal_builder("1", 100, "0")
+ .build()
+ .unwrap(),
+ make_fake_child_deposit_builder("2", 100, "0")
+ .category_id(None)
+ .build()
+ .unwrap(),
+ ];
+
+ let result_0 =
+ transform_bucket_transactions_from_category_transfers(&actualbudget_transactions_0);
+ let result_1 =
+ transform_bucket_transactions_from_category_transfers(&actualbudget_transactions_1);
+
+ assert!(result_0.is_err());
+ assert!(result_1.is_err());
+ }
+
+ fn make_fake_parent_transaction(id: &str) -> ActualbudgetTransaction {
+ make_fake_parent_transaction_builder(id).build().unwrap()
+ }
+
+ fn make_fake_parent_transaction_builder(id: &str) -> ActualbudgetTransactionBuilder {
+ let mut builder = make_fake_actualbudget_transaction_builder(id);
+ builder
+ .account_id(String::from("0"))
+ .notes(Some(String::from("I was a parent transaction")))
+ .is_parent(true);
+ builder
+ }
+
+ fn make_fake_child_withdrawal(id: &str, parent_id: &str) -> ActualbudgetTransaction {
+ make_fake_child_withdrawal_builder(id, 100, parent_id)
+ .build()
+ .unwrap()
+ }
+
+ fn make_fake_child_withdrawal_builder(
+ id: &str,
+ amount: i32,
+ parent_id: &str,
+ ) -> ActualbudgetTransactionBuilder {
+ let mut builder = make_fake_actualbudget_transaction_builder(id);
+ builder
+ .account_id(String::from("0"))
+ .category_id(Some(String::from("0")))
+ .amount(-amount)
+ .is_child(true)
+ .notes(Some(String::from("I was a child withdrawal")))
+ .parent_id(Some(String::from(parent_id)));
+ builder
+ }
+
+ fn make_fake_child_deposit(id: &str, parent_id: &str) -> ActualbudgetTransaction {
+ make_fake_child_deposit_builder(id, 100, parent_id)
+ .build()
+ .unwrap()
+ }
+
+ fn make_fake_child_deposit_builder(
+ id: &str,
+ amount: i32,
+ parent_id: &str,
+ ) -> ActualbudgetTransactionBuilder {
+ let mut builder = make_fake_actualbudget_transaction_builder(id);
+ builder
+ .account_id(String::from("0"))
+ .category_id(Some(String::from("1")))
+ .amount(amount)
+ .is_child(true)
+ .notes(Some(String::from("I was a child deposit")))
+ .parent_id(Some(String::from(parent_id)));
+ builder
+ }
+}
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs
index 835ceff..144913a 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs
@@ -1,12 +1,16 @@
use anyhow::{Context, Result};
-use schist_models::{Account, Bucket, BucketTransfer, Pipe, Transaction};
+use schist_models::{Account, Bucket, BucketTransaction, Pipe, Transaction};
-use crate::{actualbudget_state::ActualbudgetState, schist_state::SchistState};
+use crate::{
+ actualbudget_state::ActualbudgetState, schist_state::SchistState,
+ transform_state::transform_bucket_transactions_from_budgets_not_carried_over,
+};
use super::{
- transform_account_transfers, transform_accounts, transform_categories_to_buckets,
- transform_date, transform_transactions, transform_transactions_to_bucket_transfers,
- transform_zero_budgets_to_drips::transform_budget_drips, transform_zero_budgets_to_pipes,
+ transform_account_transfers, transform_accounts,
+ transform_bucket_transactions_from_category_transfers, transform_categories_to_buckets,
+ transform_date, transform_transactions, transform_zero_budgets_to_drips,
+ transform_zero_budgets_to_pipes,
};
pub fn transform_state(actualbudget_state: ActualbudgetState) -> Result<SchistState> {
@@ -27,7 +31,8 @@ fn transform_state_without_context(actualbudget_state: ActualbudgetState) -> Res
&actualbudget_state.actualbudget_transactions,
&first_activity_date,
);
- let budget_drips = transform_budget_drips(&actualbudget_state.actualbudget_zero_budgets)?;
+ let budget_drips =
+ transform_zero_budgets_to_drips(&actualbudget_state.actualbudget_zero_budgets)?;
let buckets: Vec<Bucket> = transform_categories_to_buckets(
&actualbudget_state.actualbudget_categories,
&actualbudget_state.actualbudget_category_groups,
@@ -36,8 +41,16 @@ fn transform_state_without_context(actualbudget_state: ActualbudgetState) -> Res
);
let pipes: Vec<Pipe> =
transform_zero_budgets_to_pipes(&actualbudget_state.actualbudget_zero_budgets)?;
- let bucket_transfers: Vec<BucketTransfer> =
- transform_transactions_to_bucket_transfers(&remaining_transactions)?;
+ let bucket_transactions: Vec<BucketTransaction> = vec![
+ transform_bucket_transactions_from_category_transfers(&remaining_transactions)?,
+ transform_bucket_transactions_from_budgets_not_carried_over(
+ &actualbudget_state.actualbudget_zero_budgets,
+ &actualbudget_state.actualbudget_transactions,
+ )?,
+ ]
+ .into_iter()
+ .flatten()
+ .collect();
Ok(SchistState {
first_transaction_date: first_activity_date,
@@ -47,6 +60,6 @@ fn transform_state_without_context(actualbudget_state: ActualbudgetState) -> Res
budget_drips,
pipes: pipes,
transactions,
- bucket_transfers,
+ bucket_transactions,
})
}
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_transactions.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_transactions.rs
index 11765be..dce07af 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_transactions.rs
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_transactions.rs
@@ -2,7 +2,7 @@ use actualbudget_models::{ActualbudgetPayee, ActualbudgetTransaction};
use anyhow::{Context, Result};
use schist_models::Transaction;
-use super::{is_not_bucket_transfer, transform_date, transform_id};
+use super::{is_not_category_transfer, transform_date, transform_id};
pub fn transform_transactions(
actualbudget_transactions: &[ActualbudgetTransaction],
@@ -21,7 +21,7 @@ fn transform_transactions_without_context(
let actualbudget_transactions = actualbudget_transactions
.iter()
.filter(|t| !t.is_starting_balance())
- .filter(|t| is_not_bucket_transfer(t, actualbudget_transactions));
+ .filter(|t| is_not_category_transfer(t, actualbudget_transactions));
let parent_actualbudget_transactions: Vec<&ActualbudgetTransaction> = actualbudget_transactions
.clone()
.filter(|t| t.is_parent)
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_transactions_to_bucket_transfers.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_transactions_to_bucket_transfers.rs
deleted file mode 100644
index 51545c3..0000000
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_transactions_to_bucket_transfers.rs
+++ /dev/null
@@ -1,383 +0,0 @@
-use actualbudget_models::ActualbudgetTransaction;
-use anyhow::{bail, Context, Result};
-use itertools::Itertools;
-use schist_models::BucketTransfer;
-
-use super::transform_id;
-
-pub fn transform_transactions_to_bucket_transfers(
- actualbudget_transactions: &[ActualbudgetTransaction],
-) -> Result<Vec<BucketTransfer>> {
- let children: Vec<&ActualbudgetTransaction> = actualbudget_transactions
- .into_iter()
- .filter(|&t| t.is_child)
- .collect();
- let parent_children_pairs: Vec<(&ActualbudgetTransaction, Vec<&ActualbudgetTransaction>)> =
- actualbudget_transactions
- .iter()
- .filter(|t| t.is_parent)
- .map(|parent| {
- (
- parent,
- children
- .clone()
- .into_iter()
- .filter(|&c| c.parent_id.clone().is_some_and(|id| id == parent.id))
- .collect::<Vec<&ActualbudgetTransaction>>(),
- )
- })
- .map(|(parent, children)| {
- let children_total_amount: i32 = children.iter().map(|child| child.amount).sum();
- if parent.amount != children_total_amount {
- bail!("parent transaction {} has amount: {}, but its {} children add up to a total amount of: {}",
- parent.id,
- parent.amount,
- children.len(),
- children_total_amount);
- }
- Ok((parent, children))
- })
- .filter(|result|
- match result {
- Ok((parent, _children)) => parent.amount == 0,
- Err(_) => true,
- })
- .try_collect()?;
-
- Ok(parent_children_pairs
- .iter()
- .map(|(parent, children)| transform_category_transfer(parent, children))
- .filter_ok(Option::is_some)
- .map_ok(Option::unwrap)
- .collect::<Result<Vec<BucketTransfer>>>()
- .with_context(|| "failed to transform category transfers")?)
-}
-
-fn transform_category_transfer(
- actualbudget_parent_category_transfer: &ActualbudgetTransaction,
- actualbudget_child_category_transfers: &[&ActualbudgetTransaction],
-) -> Result<Option<BucketTransfer>> {
- transform_category_transfer_without_context(
- actualbudget_parent_category_transfer,
- actualbudget_child_category_transfers,
- )
- .with_context(|| "failed to transform category transfer")
-}
-
-fn transform_category_transfer_without_context(
- actualbudget_parent_category_transfer: &ActualbudgetTransaction,
- actualbudget_child_category_transfers: &[&ActualbudgetTransaction],
-) -> Result<Option<BucketTransfer>> {
- let from_transfers: Vec<&ActualbudgetTransaction> = actualbudget_child_category_transfers
- .into_iter()
- .cloned()
- .filter(|&t| t.amount < 0)
- .collect();
- if from_transfers.len() != 1 {
- bail!(
- "actualbudget parent category transfer {} had {} from child transfers",
- actualbudget_parent_category_transfer.id,
- from_transfers.len(),
- );
- }
- let from_transfer = from_transfers.get(0).unwrap();
-
- if from_transfer.category_id.is_none() {
- bail!(
- "actualbudget child from transfer {} had no category ID",
- from_transfer.id
- );
- }
- let from_category_id: i32 = transform_id(&from_transfer.category_id.clone().unwrap());
-
- let to_transfers: Vec<&ActualbudgetTransaction> = actualbudget_child_category_transfers
- .iter()
- .filter(|&t| t.amount > 0)
- .cloned()
- .collect();
- if to_transfers.len() != 1 {
- bail!(
- "actualbudget parent category transfer {} had {} to child transfers",
- actualbudget_parent_category_transfer.id,
- to_transfers.len(),
- );
- }
- let to_transfer = to_transfers.get(0).unwrap();
-
- if to_transfer.category_id.is_none() {
- bail!(
- "actualbudget child to transfer {} had no category ID",
- to_transfer.id
- );
- }
- let to_category_id: i32 = transform_id(&to_transfer.category_id.clone().unwrap());
-
- if from_transfer.amount != -to_transfer.amount {
- bail!("actualbudget category transfer amounts did not sum to zero");
- }
-
- Ok(Some(BucketTransfer {
- id: transform_id(&actualbudget_parent_category_transfer.id),
- description: actualbudget_parent_category_transfer
- .notes
- .clone()
- .unwrap_or_else(String::new),
- amount: to_transfer.amount.try_into().unwrap(),
- from_bucket_id: from_category_id,
- to_bucket_id: to_category_id,
- }))
-}
-
-#[cfg(test)]
-mod test {
- use actualbudget_fakes::actualbudget_transaction::make_fake_actualbudget_transaction_builder;
- use actualbudget_models::{ActualbudgetTransaction, ActualbudgetTransactionBuilder};
- use schist_models::BucketTransfer;
-
- use crate::transform_state::transform_id::transform_id;
-
- use super::transform_transactions_to_bucket_transfers;
-
- #[test]
- fn when_one_category_transfer_then_transforms() {
- let actualbudget_transactions = [
- make_fake_parent_transaction("0"),
- make_fake_from_child_transaction("1", "0"),
- make_fake_to_child_transaction("2", "0"),
- ];
-
- let result = transform_transactions_to_bucket_transfers(&actualbudget_transactions);
-
- assert!(result.is_ok());
- let bucket_transfers = result.unwrap();
- assert_eq!(1, bucket_transfers.len());
- let bucket_transfer = bucket_transfers.get(0).unwrap();
- assert_eq!(100, bucket_transfer.amount);
- assert_eq!("I was a parent transaction", bucket_transfer.description);
- assert_eq!(transform_id("0"), bucket_transfer.from_bucket_id);
- assert_eq!(transform_id("1"), bucket_transfer.to_bucket_id);
- }
-
- #[test]
- fn when_many_transactions_then_transforms() {
- let actualbudget_transactions = [
- make_fake_parent_transaction("0"),
- make_fake_from_child_transaction_builder("1", 100, "0")
- .category_id(Some(String::from("0")))
- .build()
- .unwrap(),
- make_fake_to_child_transaction_builder("2", 100, "0")
- .category_id(Some(String::from("1")))
- .build()
- .unwrap(),
- make_fake_parent_transaction("3"),
- make_fake_from_child_transaction_builder("4", 200, "3")
- .category_id(Some(String::from("1")))
- .build()
- .unwrap(),
- make_fake_to_child_transaction_builder("5", 200, "3")
- .category_id(Some(String::from("0")))
- .build()
- .unwrap(),
- make_fake_actualbudget_transaction_builder("6")
- .amount(300)
- .build()
- .unwrap(),
- make_fake_actualbudget_transaction_builder("7")
- .amount(-400)
- .build()
- .unwrap(),
- ];
-
- let result = transform_transactions_to_bucket_transfers(&actualbudget_transactions);
-
- assert!(result.is_ok());
- let bucket_transfers = result.unwrap();
- assert_eq!(2, bucket_transfers.len());
-
- let bucket_transfer_0: Vec<&BucketTransfer> = bucket_transfers
- .iter()
- .filter(|ct| ct.amount == 100)
- .collect();
- assert_eq!(1, bucket_transfer_0.len());
- let bucket_transfer_0 = bucket_transfer_0.get(0).unwrap();
- assert_eq!(transform_id("0"), bucket_transfer_0.from_bucket_id);
- assert_eq!(transform_id("1"), bucket_transfer_0.to_bucket_id);
-
- let bucket_transfer_1: Vec<&BucketTransfer> = bucket_transfers
- .iter()
- .filter(|ct| ct.amount == 200)
- .collect();
- assert_eq!(1, bucket_transfer_1.len());
- let bucket_transfer_1 = bucket_transfer_1.get(0).unwrap();
- assert_eq!(transform_id("1"), bucket_transfer_1.from_bucket_id);
- assert_eq!(transform_id("0"), bucket_transfer_1.to_bucket_id);
- }
-
- #[test]
- fn when_multiple_child_from_transactions_then_err() {
- let actualbudget_transactions = [
- make_fake_parent_transaction("0"),
- make_fake_from_child_transaction_builder("1", 50, "0")
- .build()
- .unwrap(),
- make_fake_from_child_transaction_builder("2", 50, "0")
- .build()
- .unwrap(),
- make_fake_to_child_transaction_builder("3", 100, "0")
- .build()
- .unwrap(),
- ];
-
- let result = transform_transactions_to_bucket_transfers(&actualbudget_transactions);
-
- assert!(result.is_err());
- }
-
- #[test]
- fn when_multiple_child_to_transactions_then_err() {
- let actualbudget_transactions = [
- make_fake_parent_transaction("0"),
- make_fake_from_child_transaction_builder("1", 100, "0")
- .build()
- .unwrap(),
- make_fake_to_child_transaction_builder("2", 50, "0")
- .build()
- .unwrap(),
- make_fake_to_child_transaction_builder("3", 50, "0")
- .build()
- .unwrap(),
- ];
-
- let result = transform_transactions_to_bucket_transfers(&actualbudget_transactions);
-
- assert!(result.is_err());
- }
-
- #[test]
- fn when_no_child_from_transactions_then_err() {
- let actualbudget_transactions = [
- make_fake_parent_transaction("0"),
- make_fake_to_child_transaction("1", "0"),
- ];
-
- let result = transform_transactions_to_bucket_transfers(&actualbudget_transactions);
-
- assert!(result.is_err());
- }
-
- #[test]
- fn when_no_child_to_transactions_then_err() {
- let actualbudget_transactions = [
- make_fake_parent_transaction("0"),
- make_fake_from_child_transaction("1", "0"),
- ];
-
- let result = transform_transactions_to_bucket_transfers(&actualbudget_transactions);
-
- assert!(result.is_err());
- }
-
- #[test]
- fn when_child_amounts_dont_add_up_to_zero_then_err() {
- let actualbudget_transactions = [
- make_fake_parent_transaction("0"),
- make_fake_from_child_transaction_builder("1", 50, "0")
- .build()
- .unwrap(),
- make_fake_to_child_transaction_builder("2", 100, "0")
- .build()
- .unwrap(),
- ];
-
- let result = transform_transactions_to_bucket_transfers(&actualbudget_transactions);
-
- assert!(result.is_err());
- }
-
- #[test]
- fn when_child_category_transfer_has_no_category_then_err() {
- let actualbudget_transactions_0 = [
- make_fake_parent_transaction("0"),
- make_fake_from_child_transaction_builder("1", 100, "0")
- .category_id(None)
- .build()
- .unwrap(),
- make_fake_to_child_transaction_builder("2", 100, "0")
- .build()
- .unwrap(),
- ];
- let actualbudget_transactions_1 = [
- make_fake_parent_transaction("0"),
- make_fake_from_child_transaction_builder("1", 100, "0")
- .build()
- .unwrap(),
- make_fake_to_child_transaction_builder("2", 100, "0")
- .category_id(None)
- .build()
- .unwrap(),
- ];
-
- let result_0 = transform_transactions_to_bucket_transfers(&actualbudget_transactions_0);
- let result_1 = transform_transactions_to_bucket_transfers(&actualbudget_transactions_1);
-
- assert!(result_0.is_err());
- assert!(result_1.is_err());
- }
-
- fn make_fake_parent_transaction(id: &str) -> ActualbudgetTransaction {
- make_fake_parent_transaction_builder(id).build().unwrap()
- }
-
- fn make_fake_parent_transaction_builder(id: &str) -> ActualbudgetTransactionBuilder {
- let mut builder = make_fake_actualbudget_transaction_builder(id);
- builder
- .account_id(String::from("0"))
- .notes(Some(String::from("I was a parent transaction")))
- .is_parent(true);
- builder
- }
-
- fn make_fake_from_child_transaction(id: &str, parent_id: &str) -> ActualbudgetTransaction {
- make_fake_from_child_transaction_builder(id, 100, parent_id)
- .build()
- .unwrap()
- }
- fn make_fake_from_child_transaction_builder(
- id: &str,
- amount: i32,
- parent_id: &str,
- ) -> ActualbudgetTransactionBuilder {
- let mut builder = make_fake_actualbudget_transaction_builder(id);
- builder
- .account_id(String::from("0"))
- .category_id(Some(String::from("0")))
- .amount(-amount)
- .is_child(true)
- .notes(Some(String::from("I was a from child transaction")))
- .parent_id(Some(String::from(parent_id)));
- builder
- }
-
- fn make_fake_to_child_transaction(id: &str, parent_id: &str) -> ActualbudgetTransaction {
- make_fake_to_child_transaction_builder(id, 100, parent_id)
- .build()
- .unwrap()
- }
-
- fn make_fake_to_child_transaction_builder(
- id: &str,
- amount: i32,
- parent_id: &str,
- ) -> ActualbudgetTransactionBuilder {
- let mut builder = make_fake_actualbudget_transaction_builder(id);
- builder
- .account_id(String::from("0"))
- .category_id(Some(String::from("1")))
- .amount(amount)
- .is_child(true)
- .notes(Some(String::from("I was a to child transaction")))
- .parent_id(Some(String::from(parent_id)));
- builder
- }
-}
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets_to_drips.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets_to_drips.rs
index bfaef31..b5e6734 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets_to_drips.rs
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets_to_drips.rs
@@ -8,7 +8,7 @@ use schist_traits::Dateable;
use super::{transform_date::transform_date, transform_id::transform_id};
-pub fn transform_budget_drips(
+pub fn transform_zero_budgets_to_drips(
actualbudget_zero_budgets: &[ActualbudgetZeroBudget],
) -> Result<Vec<Drip>> {
let non_zero_actualbudget_zero_budgets: Vec<ActualbudgetZeroBudget> = actualbudget_zero_budgets
@@ -133,7 +133,7 @@ mod test {
use crate::transform_state::transform_id::transform_id;
- use super::transform_budget_drips;
+ use super::transform_zero_budgets_to_drips;
mod map_actualbudget_zero_budget_to_budget_drips {
use actualbudget_fakes::actualbudget_zero_budget::make_fake_actualbudget_zero_budget_builder;
@@ -195,7 +195,7 @@ mod test {
#[test]
fn when_no_zero_budgets_then_no_budget_drips() {
let actualbudget_zero_budgets = [];
- let result = transform_budget_drips(&actualbudget_zero_budgets);
+ let result = transform_zero_budgets_to_drips(&actualbudget_zero_budgets);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 0);
}
@@ -209,7 +209,7 @@ mod test {
.build()
.unwrap()];
- let result = transform_budget_drips(&actualbudget_zero_budgets);
+ let result = transform_zero_budgets_to_drips(&actualbudget_zero_budgets);
assert!(result.is_ok());
let mut budget_drips = result.unwrap();
@@ -233,7 +233,7 @@ mod test {
.build()
.unwrap()];
- let result = transform_budget_drips(&actualbudget_zero_budgets);
+ let result = transform_zero_budgets_to_drips(&actualbudget_zero_budgets);
assert!(result.is_ok());
let budget_drips = result.unwrap();
@@ -278,7 +278,7 @@ mod test {
.unwrap(),
];
- let result = transform_budget_drips(&actualbudget_zero_budgets);
+ let result = transform_zero_budgets_to_drips(&actualbudget_zero_budgets);
let category_0_id = transform_id("0");
let category_1_id = transform_id("1");
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_transaction_matchers.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_transaction_matchers.rs
new file mode 100644
index 0000000..95bb54f
--- /dev/null
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_transaction_matchers.rs
@@ -0,0 +1,66 @@
+use schist_models::{Bucket, BucketTransaction};
+use schist_traits::Dateable;
+
+use super::bucket_names::{BUCKET_NAME_GENERAL, BUCKET_NAME_SAVINGS};
+
+pub fn get_bucket_transaction_matchers() -> Vec<Box<dyn Fn(&BucketTransaction, &[Bucket]) -> bool>>
+{
+ vec![
+ Box::new(|ct, c| matches_withdrawal_120_savings_23_oct_2024(ct, c)),
+ Box::new(|ct, c| matches_deposit_120_general_23_oct_2024(ct, c)),
+ ]
+}
+
+fn matches_withdrawal_120_savings_23_oct_2024(
+ bucket_transactions: &BucketTransaction,
+ buckets: &[Bucket],
+) -> bool {
+ let savings_bucket = buckets
+ .iter()
+ .find(|c| c.name == BUCKET_NAME_SAVINGS)
+ .expect("failed to find savings bucket");
+
+ match bucket_transactions {
+ BucketTransaction {
+ id: _id,
+ description,
+ amount: -120_00,
+ date,
+ bucket_id,
+ } => {
+ *bucket_id == savings_bucket.id
+ && description.as_str() == ""
+ && date.year() == 2024
+ && date.month() == 10
+ && date.day() == 23
+ }
+ _ => false,
+ }
+}
+
+fn matches_deposit_120_general_23_oct_2024(
+ bucket_transaction: &BucketTransaction,
+ buckets: &[Bucket],
+) -> bool {
+ let general_bucket = buckets
+ .iter()
+ .find(|c| c.name == BUCKET_NAME_GENERAL)
+ .expect("failed to find general bucket");
+
+ match bucket_transaction {
+ BucketTransaction {
+ id: _id,
+ description,
+ amount: 120_00,
+ date,
+ bucket_id,
+ } => {
+ *bucket_id == general_bucket.id
+ && description.as_str() == ""
+ && date.year() == 2024
+ && date.month() == 10
+ && date.day() == 23
+ }
+ _ => false,
+ }
+}
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_transfer_matchers.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_transfer_matchers.rs
deleted file mode 100644
index c31b9bc..0000000
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_transfer_matchers.rs
+++ /dev/null
@@ -1,39 +0,0 @@
-use schist_models::{Bucket, BucketTransfer};
-
-use super::bucket_names::{BUCKET_NAME_GENERAL, BUCKET_NAME_SAVINGS};
-
-pub fn get_bucket_transfer_matchers() -> Vec<Box<dyn Fn(&BucketTransfer, &[Bucket]) -> bool>> {
- vec![Box::new(|ct, c| {
- matches_120_savings_to_general_23_oct_2024(ct, c)
- })]
-}
-
-fn matches_120_savings_to_general_23_oct_2024(
- bucket_transfer: &BucketTransfer,
- buckets: &[Bucket],
-) -> bool {
- let savings_bucket = buckets
- .iter()
- .find(|c| c.name == BUCKET_NAME_SAVINGS)
- .expect("failed to find savings bucket");
-
- let general_bucket = buckets
- .iter()
- .find(|c| c.name == BUCKET_NAME_GENERAL)
- .expect("failed to find general bucket");
-
- match bucket_transfer {
- BucketTransfer {
- id: _id,
- description,
- amount: 120_00,
- from_bucket_id,
- to_bucket_id,
- } => {
- *from_bucket_id == savings_bucket.id
- && *to_bucket_id == general_bucket.id
- && description.as_str() == ""
- }
- _ => false,
- }
-}
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/drip_matchers.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/drip_matchers.rs
index 629a04b..ccb1715 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/drip_matchers.rs
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/drip_matchers.rs
@@ -33,7 +33,7 @@ pub fn assert_drip_matches(drip: &Drip, buckets: &[Bucket]) {
.as_str(),
);
- let expected_quantity = match (bucket_name.as_str(), drip.date.month(), drip.date.day()) {
+ let expected_amount = match (bucket_name.as_str(), drip.date.month(), drip.date.day()) {
(BUCKET_NAME_FOOD, 8 | 10, 1) => 6_50,
(BUCKET_NAME_FOOD, 8 | 10, _) => 6_45,
(BUCKET_NAME_FOOD, 9 | 11, 1) => 6_86,
@@ -70,8 +70,8 @@ pub fn assert_drip_matches(drip: &Drip, buckets: &[Bucket]) {
};
assert_eq!(
- expected_quantity, drip.amount,
- "drip {:?} had unexpected quantity",
+ expected_amount, drip.amount,
+ "drip {:?} had unexpected amount",
drip,
);
}
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/mod.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/mod.rs
index 82d7870..1dd718c 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/mod.rs
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/mod.rs
@@ -3,7 +3,7 @@ mod account_names;
mod account_transfer_matchers;
mod bucket_matchers;
mod bucket_names;
-mod bucket_transfer_matchers;
+mod bucket_transaction_matchers;
mod db_url;
mod drip_matchers;
mod pipe_matchers;
@@ -15,7 +15,7 @@ pub use account_names::ACCOUNT_NAMES_DICT;
pub use account_transfer_matchers::get_account_transfer_matchers;
pub use bucket_matchers::get_bucket_matchers;
pub use bucket_names::BUCKET_NAMES_DICT;
-pub use bucket_transfer_matchers::get_bucket_transfer_matchers;
+pub use bucket_transaction_matchers::get_bucket_transaction_matchers;
pub use db_url::TEST_IN_DB_URL;
pub use db_url::TEST_OUT_DB_URL;
pub use drip_matchers::assert_drip_matches;
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/run.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/run.rs
index 04c21ca..1232016 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/run.rs
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/run.rs
@@ -3,15 +3,15 @@ pub mod common;
use actualbudget_to_schist_transformer::run::run;
use common::{
assert_drip_matches, get_account_matchers, get_account_transfer_matchers, get_bucket_matchers,
- get_bucket_transfer_matchers, get_num_expected_drips, get_transaction_matchers, TestContext,
+ get_bucket_transaction_matchers, get_num_expected_drips, get_transaction_matchers, TestContext,
TEST_IN_DB_URL, TEST_OUT_DB_URL,
};
use diesel::{Connection, SqliteConnection};
use schist_queries::{
account_transfers::get_all_account_transfers, accounts::get_all_accounts,
- bucket_transfers::get_all_bucket_transfers, buckets::get_all_buckets, drips::get_all_drips,
- pipes::get_all_pipes, transactions::get_all_transactions,
+ bucket_transactions::get_all_bucket_transactions, buckets::get_all_buckets,
+ drips::get_all_drips, pipes::get_all_pipes, transactions::get_all_transactions,
};
use crate::common::get_pipe_matchers;
@@ -25,7 +25,7 @@ fn given_test_database_when_run_then_exports_expected_schist_state() {
let account_transfers = get_all_account_transfers(connection).unwrap();
let drips = get_all_drips(connection).unwrap();
let buckets = get_all_buckets(connection).unwrap();
- let bucket_transfers = get_all_bucket_transfers(connection).unwrap();
+ let bucket_transactions = get_all_bucket_transactions(connection).unwrap();
let pipes = get_all_pipes(connection).unwrap();
let transactions = get_all_transactions(connection).unwrap();
@@ -66,15 +66,15 @@ fn given_test_database_when_run_then_exports_expected_schist_state() {
);
}
- let bucket_transfer_matchers = get_bucket_transfer_matchers();
- assert_eq!(bucket_transfer_matchers.len(), bucket_transfers.len());
- for bucket_transfer in bucket_transfers {
+ let bucket_transaction_matchers = get_bucket_transaction_matchers();
+ assert_eq!(bucket_transaction_matchers.len(), bucket_transactions.len());
+ for bucket_transaction in bucket_transactions {
assert!(
- bucket_transfer_matchers
+ bucket_transaction_matchers
.iter()
- .any(|m| m(&bucket_transfer, &buckets)),
- "no match found for bucket_transfer {:?}",
- bucket_transfer,
+ .any(|m| m(&bucket_transaction, &buckets)),
+ "no match found for bucket_transaction {:?}",
+ bucket_transaction,
);
}