summaryrefslogtreecommitdiff
path: root/rust
diff options
context:
space:
mode:
Diffstat (limited to 'rust')
-rw-r--r--rust/actualbudget_models/src/actualbudget_transaction.rs2
-rw-r--r--rust/actualbudget_to_schist_transformer/src/transform_state/transform_category_transfers.rs377
-rw-r--r--rust/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs2
3 files changed, 327 insertions, 54 deletions
diff --git a/rust/actualbudget_models/src/actualbudget_transaction.rs b/rust/actualbudget_models/src/actualbudget_transaction.rs
index 950ea07..dc64de0 100644
--- a/rust/actualbudget_models/src/actualbudget_transaction.rs
+++ b/rust/actualbudget_models/src/actualbudget_transaction.rs
@@ -3,7 +3,7 @@ use diesel::prelude::{Identifiable, Queryable, QueryableByName, Selectable};
use crate::actualbudget_date::ActualbudgetDate;
-#[derive(Builder, Identifiable, QueryableByName, Queryable, Selectable, Clone)]
+#[derive(Builder, Debug, Identifiable, QueryableByName, Queryable, Selectable, Clone)]
#[diesel(table_name = crate::actualbudget_schema::v_transactions)]
pub struct ActualbudgetTransaction {
pub id: String,
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
index 14b34f2..2bcd0e0 100644
--- 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
@@ -2,72 +2,345 @@ use actualbudget_models::{
ActualbudgetAccount,
ActualbudgetTransaction,
};
+use anyhow::{bail, Context, Result};
+use itertools::Itertools;
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| {
+) -> Result<Vec<CategoryTransfer>> {
+ let actualbudget_category_transfers = actualbudget_transactions
+ .iter()
+ .filter(|transaction|
+ is_category_transfer(transaction, actualbudget_category_transfers_account)
+ );
+ 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();
+
+ Ok(actualbudget_parent_category_transfers
+ .iter()
+ .map(|pct| transform_category_transfer(pct, &actualbudget_child_category_transfers))
+ .filter_ok(Option::is_some)
+ .map_ok(Option::unwrap)
+ .collect::<Result<Vec<CategoryTransfer>>>()
+ .with_context(|| "failed to transform category transfers")?
+ )
+}
+
+fn transform_category_transfer(
+ actualbudget_parent_category_transfer: &ActualbudgetTransaction,
+ actualbudget_child_category_transfers: &[ActualbudgetTransaction],
+) -> Result<Option<CategoryTransfer>> {
+ 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<CategoryTransfer>> {
+ 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)
- });
-
- 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
+ ).map(ActualbudgetTransaction::clone);
+
+ let from_transfers: Vec<ActualbudgetTransaction> = actualbudget_child_category_transfers
+ .clone()
+ .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
+ .clone()
+ .filter(|t| t.amount < 0)
+ .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(CategoryTransfer {
+ id: transform_id(&actualbudget_parent_category_transfer.id),
+ description: actualbudget_parent_category_transfer
+ .notes
+ .clone()
+ .unwrap_or_else(String::new),
+ quantity: from_transfer.amount.try_into().unwrap(),
+ from_category_id,
+ to_category_id,
+ }))
}
#[cfg(test)]
mod test {
+ use actualbudget_fakes::{actualbudget_account::make_fake_actualbudget_account, actualbudget_transaction::make_fake_actualbudget_transaction_builder};
+ use actualbudget_models::{ActualbudgetTransaction, ActualbudgetTransactionBuilder};
+ use schist_models::category_transfer::CategoryTransfer;
+
+ use crate::transform_state::transform_id::transform_id;
+
+ use super::transform_category_transfers;
+
#[test]
- fn todo() {
- todo!()
+ fn when_one_category_transfer_then_transforms() {
+ let actualbudget_category_transfers_account = make_fake_actualbudget_account("0");
+ 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_category_transfers(
+ &actualbudget_transactions,
+ &Some(actualbudget_category_transfers_account),
+ );
+
+ assert!(result.is_ok());
+ let category_transfers = result.unwrap();
+ assert_eq!(1, category_transfers.len());
+ let category_transfer = category_transfers.get(0).unwrap();
+ assert_eq!(100, category_transfer.quantity);
+ assert_eq!("I was a parent transaction", category_transfer.description);
+ assert_eq!(transform_id("0"), category_transfer.from_category_id);
+ assert_eq!(transform_id("1"), category_transfer.to_category_id);
+ }
+
+ #[test]
+ fn when_many_transactions_then_transforms() {
+ let actualbudget_category_transfers_account = make_fake_actualbudget_account("0");
+ let actualbudget_transactions = [
+ make_fake_parent_transaction("0"),
+ make_fake_from_child_transaction_builder("1", "0").amount(100).category_id(Some(String::from("0"))).build().unwrap(),
+ make_fake_to_child_transaction_builder("2", "0").amount(-100).category_id(Some(String::from("1"))).build().unwrap(),
+ make_fake_parent_transaction("3"),
+ make_fake_from_child_transaction_builder("4", "3").amount(200).category_id(Some(String::from("1"))).build().unwrap(),
+ make_fake_to_child_transaction_builder("5", "3").amount(-200).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_category_transfers(
+ &actualbudget_transactions,
+ &Some(actualbudget_category_transfers_account),
+ );
+
+ assert!(result.is_ok());
+ let category_transfers = result.unwrap();
+ assert_eq!(2, category_transfers.len());
+
+ let category_transfer_0: Vec<&CategoryTransfer> = category_transfers
+ .iter()
+ .filter(|ct| ct.quantity == 100)
+ .collect();
+ assert_eq!(1, category_transfer_0.len());
+ let category_transfer_0 = category_transfer_0.get(0).unwrap();
+ assert_eq!(transform_id("0"), category_transfer_0.from_category_id);
+ assert_eq!(transform_id("1"), category_transfer_0.to_category_id);
+
+ let category_transfer_1: Vec<&CategoryTransfer> = category_transfers
+ .iter()
+ .filter(|ct| ct.quantity == 200)
+ .collect();
+ assert_eq!(1, category_transfer_1.len());
+ let category_transfer_1 = category_transfer_1.get(0).unwrap();
+ assert_eq!(transform_id("1"), category_transfer_1.from_category_id);
+ assert_eq!(transform_id("0"), category_transfer_1.to_category_id);
+ }
+
+ #[test]
+ fn when_multiple_child_from_transactions_then_err() {
+ let actualbudget_category_transfers_account = make_fake_actualbudget_account("0");
+ let actualbudget_transactions = [
+ make_fake_parent_transaction("0"),
+ make_fake_from_child_transaction_builder("1", "0").amount(50).build().unwrap(),
+ make_fake_from_child_transaction_builder("2", "0").amount(50).build().unwrap(),
+ make_fake_to_child_transaction_builder("3", "0").amount(-100).build().unwrap(),
+ ];
+
+ let result = transform_category_transfers(
+ &actualbudget_transactions,
+ &Some(actualbudget_category_transfers_account),
+ );
+
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn when_multiple_child_to_transactions_then_err() {
+ let actualbudget_category_transfers_account = make_fake_actualbudget_account("0");
+ let actualbudget_transactions = [
+ make_fake_parent_transaction("0"),
+ make_fake_from_child_transaction_builder("1", "0").amount(100).build().unwrap(),
+ make_fake_to_child_transaction_builder("2", "0").amount(-50).build().unwrap(),
+ make_fake_to_child_transaction_builder("3", "0").amount(-50).build().unwrap(),
+ ];
+
+ let result = transform_category_transfers(
+ &actualbudget_transactions,
+ &Some(actualbudget_category_transfers_account),
+ );
+
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn when_no_child_from_transactions_then_err() {
+ let actualbudget_category_transfers_account = make_fake_actualbudget_account("0");
+ let actualbudget_transactions = [
+ make_fake_parent_transaction("0"),
+ make_fake_to_child_transaction("1", "0"),
+ ];
+
+ let result = transform_category_transfers(
+ &actualbudget_transactions,
+ &Some(actualbudget_category_transfers_account),
+ );
+
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn when_no_child_to_transactions_then_err() {
+ let actualbudget_category_transfers_account = make_fake_actualbudget_account("0");
+ let actualbudget_transactions = [
+ make_fake_parent_transaction("0"),
+ make_fake_from_child_transaction("1", "0"),
+ ];
+
+ let result = transform_category_transfers(
+ &actualbudget_transactions,
+ &Some(actualbudget_category_transfers_account),
+ );
+
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn when_child_amounts_dont_add_up_to_zero_then_err() {
+ let actualbudget_category_transfers_account = make_fake_actualbudget_account("0");
+ let actualbudget_transactions = [
+ make_fake_parent_transaction("0"),
+ make_fake_from_child_transaction_builder("1", "0").amount(50).build().unwrap(),
+ make_fake_to_child_transaction_builder("2", "0").amount(-100).build().unwrap(),
+ ];
+
+ let result = transform_category_transfers(
+ &actualbudget_transactions,
+ &Some(actualbudget_category_transfers_account),
+ );
+
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn when_child_category_transfer_has_no_category_then_err() {
+ let actualbudget_category_transfers_account = make_fake_actualbudget_account("0");
+ let actualbudget_transactions_0 = [
+ make_fake_parent_transaction("0"),
+ make_fake_from_child_transaction_builder("1", "0").category_id(None).build().unwrap(),
+ make_fake_to_child_transaction_builder("2", "0").build().unwrap(),
+ ];
+ let actualbudget_transactions_1 = [
+ make_fake_parent_transaction("0"),
+ make_fake_from_child_transaction_builder("1", "0").build().unwrap(),
+ make_fake_to_child_transaction_builder("2", "0").category_id(None).build().unwrap(),
+ ];
+
+ let result_0 = transform_category_transfers(
+ &actualbudget_transactions_0,
+ &Some(actualbudget_category_transfers_account.clone()),
+ );
+ let result_1 = transform_category_transfers(
+ &actualbudget_transactions_1,
+ &Some(actualbudget_category_transfers_account),
+ );
+
+ 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, parent_id).build().unwrap()
+ }
+ fn make_fake_from_child_transaction_builder(id: &str, 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(100)
+ .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, parent_id).build().unwrap()
+ }
+
+ fn make_fake_to_child_transaction_builder(id: &str, 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(-100)
+ .is_child(true)
+ .notes(Some(String::from("I was a to child transaction")))
+ .parent_id(Some(String::from(parent_id)));
+ builder
}
}
diff --git a/rust/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs
index cc5dfe1..9fae7a1 100644
--- a/rust/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs
+++ b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs
@@ -25,7 +25,7 @@ pub fn transform_state(actualbudget_state: ActualbudgetState) -> Result<SchistSt
let category_transfers: Vec<CategoryTransfer> = transform_category_transfers(
&actualbudget_state.actualbudget_transactions,
&actualbudget_state.actualbudget_category_transfers_account,
- );
+ )?;
Ok(SchistState {
first_transaction_date,