summaryrefslogtreecommitdiff
path: root/rust/actualbudget_to_schist_transformer/src/transform_state
diff options
context:
space:
mode:
authorJoe Carstairs <me@joeac.net>2024-11-25 08:21:29 +0000
committerJoe Carstairs <me@joeac.net>2024-11-25 13:05:10 +0000
commit5bd223ee9c6853502d509627678f696c6ea5fcdc (patch)
tree0bb22e02e4c4dd359bbadef597d207556698d9e2 /rust/actualbudget_to_schist_transformer/src/transform_state
parent06770a965f75416c22ef7115004a250388cebcbf (diff)
Rename AB transformer
Diffstat (limited to 'rust/actualbudget_to_schist_transformer/src/transform_state')
-rw-r--r--rust/actualbudget_to_schist_transformer/src/transform_state/is_category_transfer.rs18
-rw-r--r--rust/actualbudget_to_schist_transformer/src/transform_state/mod.rs20
-rw-r--r--rust/actualbudget_to_schist_transformer/src/transform_state/transform_accounts.rs27
-rw-r--r--rust/actualbudget_to_schist_transformer/src/transform_state/transform_budget_drips.rs283
-rw-r--r--rust/actualbudget_to_schist_transformer/src/transform_state/transform_categories.rs38
-rw-r--r--rust/actualbudget_to_schist_transformer/src/transform_state/transform_category_transfers.rs73
-rw-r--r--rust/actualbudget_to_schist_transformer/src/transform_state/transform_date.rs15
-rw-r--r--rust/actualbudget_to_schist_transformer/src/transform_state/transform_id.rs17
-rw-r--r--rust/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs35
-rw-r--r--rust/actualbudget_to_schist_transformer/src/transform_state/transform_transactions.rs93
10 files changed, 619 insertions, 0 deletions
diff --git a/rust/actualbudget_to_schist_transformer/src/transform_state/is_category_transfer.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/is_category_transfer.rs
new file mode 100644
index 0000000..c82387c
--- /dev/null
+++ b/rust/actualbudget_to_schist_transformer/src/transform_state/is_category_transfer.rs
@@ -0,0 +1,18 @@
+use actualbudget_models::{ActualbudgetAccount, ActualbudgetTransaction};
+
+pub fn is_category_transfer(
+ transaction: &ActualbudgetTransaction,
+ actualbudget_category_transfers_account: &Option<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/actualbudget_to_schist_transformer/src/transform_state/mod.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/mod.rs
new file mode 100644
index 0000000..916894a
--- /dev/null
+++ b/rust/actualbudget_to_schist_transformer/src/transform_state/mod.rs
@@ -0,0 +1,20 @@
+mod is_category_transfer;
+mod transform_accounts;
+mod transform_budget_drips;
+mod transform_categories;
+mod transform_category_transfers;
+mod transform_date;
+mod transform_id;
+mod transform_state;
+mod transform_transactions;
+
+use is_category_transfer::is_category_transfer;
+use is_category_transfer::is_not_category_transfer;
+use transform_accounts::transform_accounts;
+use transform_categories::transform_categories;
+use transform_category_transfers::transform_category_transfers;
+use transform_date::transform_date;
+use transform_id::transform_id;
+use transform_transactions::transform_transactions;
+
+pub use transform_state::*;
diff --git a/rust/actualbudget_to_schist_transformer/src/transform_state/transform_accounts.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_accounts.rs
new file mode 100644
index 0000000..917c62d
--- /dev/null
+++ b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_accounts.rs
@@ -0,0 +1,27 @@
+use actualbudget_models::ActualbudgetAccount;
+use schist_models::{account::Account, datetime_utc::DatetimeUtc};
+
+use super::transform_id;
+
+pub fn transform_accounts(
+ actualbudget_accounts: &[ActualbudgetAccount],
+ first_transaction_date: &DatetimeUtc,
+) -> Vec<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/actualbudget_to_schist_transformer/src/transform_state/transform_budget_drips.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_budget_drips.rs
new file mode 100644
index 0000000..2e9125b
--- /dev/null
+++ b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_budget_drips.rs
@@ -0,0 +1,283 @@
+use actualbudget_models::ActualbudgetZeroBudget;
+use anyhow::{Context, Result};
+use itertools::Itertools;
+use schist_models::{budget_drip::BudgetDrip, date_utc::DateUtc};
+use schist_traits::dateable::Dateable;
+
+use super::{transform_date::transform_date, transform_id::transform_id};
+
+pub fn transform_budget_drips(actualbudget_zero_budgets: &[ActualbudgetZeroBudget]) -> Result<Vec<BudgetDrip>> {
+ let budget_drips = group_actualbudget_zero_budgets_by_category_id(actualbudget_zero_budgets)
+ .iter()
+ .map(sort_actualbudget_zero_budgets_by_month)
+ .flat_map(|sorted_actualbudget_zero_budgets| sorted_actualbudget_zero_budgets
+ .iter()
+ .map(map_actualbudget_zero_budget_to_budget_drips)
+ .collect::<Result<Vec<Vec<BudgetDrip>>>>()
+ ).flatten()
+ .flatten()
+ .collect();
+ Ok(budget_drips)
+}
+
+fn map_actualbudget_zero_budget_to_budget_drips(
+ actualbudget_zero_budget: &ActualbudgetZeroBudget,
+) -> Result<Vec<BudgetDrip>> {
+ let context = || "failed to map actualbudget zero budgets to budget drips";
+
+ let first_day_of_month: DateUtc = Into::<DateUtc>::into(transform_date(&actualbudget_zero_budget.month).with_context(context)?);
+ let month = first_day_of_month.month();
+ let days_in_month: Vec<DateUtc> = (0..31)
+ .map(|i| first_day_of_month.add_days(i))
+ .take_while(|d| match d {
+ Err(_) => true,
+ Ok(d) => d.month() == month,
+ })
+ .filter(Result::is_ok)
+ .map(Result::unwrap)
+ .collect();
+
+ // unwrap() can panic, but days_in_month is guaranteed to have between 0 and
+ // 31 elements, so we should be grand
+ let num_days_in_month: i32 = days_in_month.len().try_into().unwrap();
+
+ let quantity = actualbudget_zero_budget.amount / num_days_in_month;
+ let remainder_quantity = actualbudget_zero_budget.amount - (quantity * num_days_in_month);
+
+ let budget_drips: Vec<BudgetDrip> = days_in_month.iter().map(|&date|
+ BudgetDrip {
+ id: transform_id(&actualbudget_zero_budget.id),
+ category_id: transform_id(&actualbudget_zero_budget.category_id),
+ date,
+ quantity: if date == first_day_of_month {
+ quantity + remainder_quantity
+ } else {
+ quantity
+ }
+ }
+ ).collect();
+
+ Ok(budget_drips)
+}
+
+fn sort_actualbudget_zero_budgets_by_month(
+ actualbudget_zero_budgets_by_category_id: &Vec<ActualbudgetZeroBudget>,
+) -> Vec<ActualbudgetZeroBudget> {
+ let mut actualbudget_zero_budgets = actualbudget_zero_budgets_by_category_id.clone();
+ actualbudget_zero_budgets.sort_by_key(|zb| zb.month.clone());
+ actualbudget_zero_budgets
+}
+
+fn group_actualbudget_zero_budgets_by_category_id(
+ actualbudget_zero_budgets: &[ActualbudgetZeroBudget],
+) -> Vec<Vec<ActualbudgetZeroBudget>> {
+ let category_ids = actualbudget_zero_budgets
+ .iter()
+ .map(|zb| zb.category_id.clone())
+ .unique();
+
+ category_ids
+ .map(|category_id| actualbudget_zero_budgets
+ .iter()
+ .filter(|zb| zb.category_id == category_id)
+ .map(ActualbudgetZeroBudget::clone)
+ .collect()
+ ).collect()
+}
+
+#[cfg(test)]
+mod test {
+ use actualbudget_fakes::actualbudget_zero_budget::make_fake_actualbudget_zero_budget_builder;
+ use actualbudget_models::ActualbudgetDate;
+ use schist_models::budget_drip::BudgetDrip;
+ use schist_traits::dateable::Dateable;
+
+ use crate::transform_state::transform_id::transform_id;
+
+ use super::transform_budget_drips;
+
+ mod map_actualbudget_zero_budget_to_budget_drips {
+ use actualbudget_fakes::actualbudget_zero_budget::make_fake_actualbudget_zero_budget_builder;
+ use actualbudget_models::ActualbudgetDate;
+ use schist_models::budget_drip::BudgetDrip;
+ use schist_traits::dateable::Dateable;
+
+ use crate::transform_state::transform_budget_drips::map_actualbudget_zero_budget_to_budget_drips;
+
+ #[test]
+ fn when_zero_budget_with_integer_budget_per_day_then_evenly_divided_budget_drips() {
+ let actualbudget_zero_budget = make_fake_actualbudget_zero_budget_builder("0")
+ .amount(31 * 100)
+ .category_id(String::from("0"))
+ .month(ActualbudgetDate::from_i32(202401))
+ .build()
+ .unwrap();
+
+ let result = map_actualbudget_zero_budget_to_budget_drips(&actualbudget_zero_budget);
+
+ assert!(result.is_ok());
+ let mut budget_drips = result.unwrap();
+ assert_eq!(31, budget_drips.len());
+ assert!(budget_drips.iter().all(|bd| bd.quantity == 100));
+ budget_drips.sort_by_key(|bd| bd.date);
+ for i in 1..31 {
+ assert_eq!(Ok(i), budget_drips.get(i - 1).unwrap().date.day().try_into());
+ }
+ }
+
+ #[test]
+ fn when_zero_budget_with_non_integer_budget_per_day_then_remainder_on_one_day() {
+ let actualbudget_zero_budget = make_fake_actualbudget_zero_budget_builder("0")
+ .amount(31 * 100 + 30)
+ .category_id(String::from("0"))
+ .month(ActualbudgetDate::from_i32(202401))
+ .build()
+ .unwrap();
+
+ let result = map_actualbudget_zero_budget_to_budget_drips(&actualbudget_zero_budget);
+
+ assert!(result.is_ok());
+ let budget_drips = result.unwrap();
+ assert_eq!(31, budget_drips.len());
+
+ let budget_drips_quantity_not_floored: Vec<&BudgetDrip> = budget_drips
+ .iter()
+ .filter(|bd| bd.quantity != 100)
+ .collect();
+ assert_eq!(1, budget_drips_quantity_not_floored.len());
+
+ let budget_drips_floored: Vec<&BudgetDrip> = budget_drips
+ .iter()
+ .filter(|bd| bd.quantity == 100)
+ .collect();
+ assert_eq!(30, budget_drips_floored.len());
+ }
+ }
+
+ #[test]
+ fn when_no_zero_budgets_then_no_budget_drips() {
+ let actualbudget_zero_budgets = [];
+ let result = transform_budget_drips(&actualbudget_zero_budgets);
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap().len(), 0);
+ }
+
+ #[test]
+ fn when_one_zero_budget_with_integer_budget_per_day_then_evenly_divided_budget_drips() {
+ let actualbudget_zero_budgets = [
+ make_fake_actualbudget_zero_budget_builder("0")
+ .amount(31 * 100)
+ .category_id(String::from("0"))
+ .month(ActualbudgetDate::from_i32(202401))
+ .build()
+ .unwrap()
+ ];
+
+ let result = transform_budget_drips(&actualbudget_zero_budgets);
+
+ assert!(result.is_ok());
+ let mut budget_drips = result.unwrap();
+ assert_eq!(31, budget_drips.len());
+ assert!(budget_drips.iter().all(|bd| bd.quantity == 100));
+ budget_drips.sort_by_key(|bd| bd.date);
+ for i in 1..31 {
+ assert_eq!(Ok(i), budget_drips.get(i - 1).unwrap().date.day().try_into());
+ }
+ }
+
+ #[test]
+ fn when_one_zero_budgets_with_non_integer_budget_per_day_then_remainder_on_one_day() {
+ let actualbudget_zero_budgets = [
+ make_fake_actualbudget_zero_budget_builder("0")
+ .amount(31 * 100 + 30)
+ .category_id(String::from("0"))
+ .month(ActualbudgetDate::from_i32(202401))
+ .build()
+ .unwrap()
+ ];
+
+ let result = transform_budget_drips(&actualbudget_zero_budgets);
+
+ assert!(result.is_ok());
+ let budget_drips = result.unwrap();
+ assert_eq!(31, budget_drips.len());
+
+ let budget_drips_quantity_not_floored: Vec<&BudgetDrip> = budget_drips
+ .iter()
+ .filter(|bd| bd.quantity != 100)
+ .collect();
+ assert_eq!(1, budget_drips_quantity_not_floored.len());
+
+ let budget_drips_floored: Vec<&BudgetDrip> = budget_drips
+ .iter()
+ .filter(|bd| bd.quantity == 100)
+ .collect();
+ assert_eq!(30, budget_drips_floored.len());
+ }
+
+ #[test]
+ fn when_multiple_zero_budgets_with_integer_budget_per_day_across_multiple_categories_then_evenly_divided_budget_drips() {
+ let actualbudget_zero_budgets = [
+ make_fake_actualbudget_zero_budget_builder("0")
+ .amount(31 * 100)
+ .category_id(String::from("0"))
+ .month(ActualbudgetDate::from_i32(202401))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_zero_budget_builder("1")
+ .amount(31 * 200)
+ .category_id(String::from("1"))
+ .month(ActualbudgetDate::from_i32(202401))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_zero_budget_builder("2")
+ .amount(29 * 110)
+ .category_id(String::from("0"))
+ .month(ActualbudgetDate::from_i32(202402))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_zero_budget_builder("3")
+ .amount(29 * 210)
+ .category_id(String::from("1"))
+ .month(ActualbudgetDate::from_i32(202402))
+ .build()
+ .unwrap(),
+ ];
+
+ let result = transform_budget_drips(&actualbudget_zero_budgets);
+ let category_0_id = transform_id("0");
+ let category_1_id = transform_id("1");
+
+ assert!(result.is_ok());
+ let budget_drips = result.unwrap();
+ assert_eq!(31 + 31 + 29 + 29, budget_drips.len());
+
+ let budget_drips_jan_category_0: Vec<&BudgetDrip> = budget_drips
+ .iter()
+ .filter(|bd| bd.category_id == category_0_id && bd.date.month() == 1)
+ .collect();
+ assert_eq!(31, budget_drips_jan_category_0.len());
+ assert!(budget_drips_jan_category_0.iter().all(|bd| bd.quantity == 100));
+
+ let budget_drips_jan_category_1: Vec<&BudgetDrip> = budget_drips
+ .iter()
+ .filter(|bd| bd.category_id == category_1_id && bd.date.month() == 1)
+ .collect();
+ assert_eq!(31, budget_drips_jan_category_1.len());
+ assert!(budget_drips_jan_category_1.iter().all(|bd| bd.quantity == 200));
+
+ let budget_drips_feb_category_0: Vec<&BudgetDrip> = budget_drips
+ .iter()
+ .filter(|bd| bd.category_id == category_0_id && bd.date.month() == 2)
+ .collect();
+ assert_eq!(29, budget_drips_feb_category_0.len());
+ assert!(budget_drips_feb_category_0.iter().all(|bd| bd.quantity == 110));
+
+ let budget_drips_feb_category_1: Vec<&BudgetDrip> = budget_drips
+ .iter()
+ .filter(|bd| bd.category_id == category_1_id && bd.date.month() == 2)
+ .collect();
+ assert_eq!(29, budget_drips_feb_category_1.len());
+ assert!(budget_drips_feb_category_1.iter().all(|bd| bd.quantity == 210));
+ }
+}
diff --git a/rust/actualbudget_to_schist_transformer/src/transform_state/transform_categories.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_categories.rs
new file mode 100644
index 0000000..e0fc687
--- /dev/null
+++ b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_categories.rs
@@ -0,0 +1,38 @@
+use actualbudget_models::ActualbudgetCategory;
+use schist_models::{budget_period_unit::BudgetPeriodUnit, category::Category};
+
+use super::transform_id;
+
+pub fn transform_categories(
+ actualbudget_categories: &[ActualbudgetCategory],
+) -> Vec<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(),
+ balance: 0,
+ balance_date: get_balance_date(),
+ budget_period: 1,
+ budget_period_unit: BudgetPeriodUnit::Month,
+ budget_quantity: get_budget_quantity(),
+ })
+ }
+ categories
+}
+
+fn get_balance_date() -> schist_models::date_utc::DateUtc {
+ todo!()
+}
+
+fn get_budget_quantity() -> i32 {
+ todo!()
+}
+
+#[cfg(test)]
+mod test {
+ #[test]
+ fn todo() {
+ todo!()
+ }
+}
diff --git a/rust/actualbudget_to_schist_transformer/src/transform_state/transform_category_transfers.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_category_transfers.rs
new file mode 100644
index 0000000..14b34f2
--- /dev/null
+++ b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_category_transfers.rs
@@ -0,0 +1,73 @@
+use actualbudget_models::{
+ ActualbudgetAccount,
+ ActualbudgetTransaction,
+};
+use schist_models::category_transfer::CategoryTransfer;
+use core::panic;
+
+use super::{is_category_transfer, transform_id};
+
+pub fn transform_category_transfers(
+ actualbudget_transactions: &[ActualbudgetTransaction],
+ actualbudget_category_transfers_account: &Option<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
+}
+
+#[cfg(test)]
+mod test {
+ #[test]
+ fn todo() {
+ todo!()
+ }
+}
diff --git a/rust/actualbudget_to_schist_transformer/src/transform_state/transform_date.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_date.rs
new file mode 100644
index 0000000..d45b8bb
--- /dev/null
+++ b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_date.rs
@@ -0,0 +1,15 @@
+use anyhow::Result;
+use actualbudget_models::ActualbudgetDate;
+use schist_models::datetime_utc::DatetimeUtc;
+
+pub fn transform_date(actualbudget_date: &ActualbudgetDate) -> Result<DatetimeUtc> {
+ actualbudget_date.to_iso().parse()
+}
+
+#[cfg(test)]
+mod test {
+ #[test]
+ fn todo() {
+ todo!()
+ }
+}
diff --git a/rust/actualbudget_to_schist_transformer/src/transform_state/transform_id.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_id.rs
new file mode 100644
index 0000000..0b2102b
--- /dev/null
+++ b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_id.rs
@@ -0,0 +1,17 @@
+use std::hash::{DefaultHasher, Hash, Hasher};
+
+pub fn transform_id(actualbudget_id: &str) -> i32 {
+ let mut hasher = DefaultHasher::new();
+ actualbudget_id.hash(&mut hasher);
+ (hasher.finish() % std::convert::TryInto::<u64>::try_into(i32::MAX).unwrap())
+ .try_into()
+ .unwrap()
+}
+
+#[cfg(test)]
+mod test {
+ #[test]
+ fn todo() {
+ todo!()
+ }
+}
diff --git a/rust/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs
new file mode 100644
index 0000000..2285276
--- /dev/null
+++ b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs
@@ -0,0 +1,35 @@
+use anyhow::Result;
+use schist_models::{account::Account, category::Category, category_transfer::CategoryTransfer, transaction::Transaction, transaction_categorisation::TransactionCategorisation};
+
+use crate::{actualbudget_state::ActualbudgetState, schist_state::SchistState};
+
+use super::{transform_accounts, transform_budget_drips::transform_budget_drips, transform_categories, transform_category_transfers, transform_date, transform_transactions};
+
+pub fn transform_state(actualbudget_state: ActualbudgetState) -> Result<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 budget_drips = transform_budget_drips(&actualbudget_state.actualbudget_zero_budgets)?;
+ let categories: Vec<Category> =
+ transform_categories(&actualbudget_state.actualbudget_categories);
+ 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_drips,
+ transactions,
+ transaction_categorisations,
+ category_transfers,
+ })
+}
diff --git a/rust/actualbudget_to_schist_transformer/src/transform_state/transform_transactions.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_transactions.rs
new file mode 100644
index 0000000..3fbcb0f
--- /dev/null
+++ b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_transactions.rs
@@ -0,0 +1,93 @@
+use anyhow::Result;
+use actualbudget_models::{ActualbudgetAccount, ActualbudgetTransaction};
+use schist_models::{transaction::Transaction, transaction_categorisation::TransactionCategorisation};
+
+use super::{is_not_category_transfer, transform_date, transform_id};
+
+pub fn transform_transactions(
+ actualbudget_transactions: &[ActualbudgetTransaction],
+ actualbudget_category_transfers_account: &Option<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))
+}
+
+#[cfg(test)]
+mod test {
+ #[test]
+ fn todo() {
+ todo!()
+ }
+}