From 86487b8ff6e691f2b261a93733df8f4cc5dc7320 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Mon, 25 Aug 2025 18:22:00 +0100 Subject: refactor navigation --- .../src/gui/components/bucket_name_and_balance.rs | 15 +- schist_desktop_gui/src/gui/components/button.rs | 7 +- .../src/gui/components/navigation.rs | 86 +++++--- .../gui/components/navigation/new_navigation.rs | 72 ------- .../gui/components/navigation/update_navigation.rs | 225 +++++++++++---------- .../gui/components/navigation/view_navigation.rs | 37 ++-- .../navigation/view_navigation_button.rs | 32 +-- schist_desktop_gui/src/gui/components/text.rs | 68 ++++--- .../src/gui/screens/main_screen/view.rs | 6 +- 9 files changed, 265 insertions(+), 283 deletions(-) delete mode 100644 schist_desktop_gui/src/gui/components/navigation/new_navigation.rs diff --git a/schist_desktop_gui/src/gui/components/bucket_name_and_balance.rs b/schist_desktop_gui/src/gui/components/bucket_name_and_balance.rs index 24a3952..b08729c 100644 --- a/schist_desktop_gui/src/gui/components/bucket_name_and_balance.rs +++ b/schist_desktop_gui/src/gui/components/bucket_name_and_balance.rs @@ -14,11 +14,18 @@ impl BucketNameAndBalance { } } -impl<'a> Into>> for BucketNameAndBalance { - fn into(self) -> Element<'a, navigation::Message> { +impl<'a> From<&'a BucketNameAndBalance> for Element<'a, navigation::Message> { + fn from(bucket_name_and_balance: &'a BucketNameAndBalance) -> Self { row![ - Text::new(&self.bucket.name).as_element(), - Text::new(&self.bucket.balance.unwrap_or(0).to_string()).as_element(), + Text::new(&bucket_name_and_balance.bucket.name).as_element(), + Text::new( + &bucket_name_and_balance + .bucket + .balance + .unwrap_or(0) + .to_string() + ) + .as_element(), ] .into() } diff --git a/schist_desktop_gui/src/gui/components/button.rs b/schist_desktop_gui/src/gui/components/button.rs index e071f33..6882273 100644 --- a/schist_desktop_gui/src/gui/components/button.rs +++ b/schist_desktop_gui/src/gui/components/button.rs @@ -2,9 +2,10 @@ use iced::{widget::Button, Element}; use crate::style::*; -pub fn active_button<'a, Content: Into>, Message>( - content: Content, -) -> Button<'a, Message> { +pub fn active_button<'a, Content, Message>(content: &'a Content) -> Button<'a, Message> +where + Element<'a, Message>: From<&'a Content>, +{ iced::widget::button(content) .style(|theme: &iced::Theme, _status| iced::widget::button::Style { background: Some(iced::Background::Color( diff --git a/schist_desktop_gui/src/gui/components/navigation.rs b/schist_desktop_gui/src/gui/components/navigation.rs index bed3221..6dbe622 100644 --- a/schist_desktop_gui/src/gui/components/navigation.rs +++ b/schist_desktop_gui/src/gui/components/navigation.rs @@ -1,19 +1,9 @@ -mod new_navigation; mod update_navigation; mod view_navigation; mod view_navigation_button; -use new_navigation::new_navigation; -use update_navigation::update_navigation; -use view_navigation::view_navigation; -use view_navigation_button::view_navigation_button; - use std::fmt::Debug; -use iced::Element; - -use crate::traits::{Component, Viewable}; - #[derive(Clone, Debug)] pub struct Navigation { options_before_active: Vec, @@ -36,33 +26,67 @@ pub enum Action { impl Navigation { pub fn new(active_option: Option, options: Vec) -> Self { - new_navigation(active_option, options) + let active_option_index = active_option.clone().map_or(Option::None, |active_option| { + options.iter().position(|option| option.eq(&active_option)) + }); + if let Some(active_option_index) = active_option_index { + let split = options.split_at(active_option_index); + Navigation { + options_before_active: split.0.to_vec(), + active_option: active_option, + options_after_active: split.1.split_at(1).1.to_vec(), + } + } else { + Navigation { + options_before_active: options, + active_option: active_option, + options_after_active: vec![], + } + } } } -impl<'a, TOption> Navigation -where - TOption: Clone + PartialEq + Into>>, -{ - pub fn button(&'a self, option: &'a TOption) -> Element<'a, Message> { - view_navigation_button(self, option) +#[cfg(test)] +mod test { + use super::Navigation; + + #[test] + fn when_active_option_at_start_of_options_then_finds_active_option() { + let navigation = Navigation::new(Some(1), vec![1, 2, 3, 4, 5]); + assert_eq!(Vec::::new(), navigation.options_before_active); + assert_eq!(Some(1), navigation.active_option); + assert_eq!(vec![2, 3, 4, 5], navigation.options_after_active); } -} -impl<'a, TOption> Viewable<'a, Message> for Navigation -where - TOption: Clone + PartialEq + Into>>, -{ - fn view(&'a self) -> iced::Element<'a, Message> { - view_navigation(self) + #[test] + fn when_active_option_in_middle_of_options_then_finds_active_option() { + let navigation = Navigation::new(Some(3), vec![1, 2, 3, 4, 5]); + assert_eq!(vec![1, 2], navigation.options_before_active); + assert_eq!(Some(3), navigation.active_option); + assert_eq!(vec![4, 5], navigation.options_after_active); + } + + #[test] + fn when_active_option_at_end_of_options_then_finds_active_option() { + let navigation = Navigation::new(Some(5), vec![1, 2, 3, 4, 5]); + assert_eq!(vec![1, 2, 3, 4], navigation.options_before_active); + assert_eq!(Some(5), navigation.active_option); + assert_eq!(Vec::::new(), navigation.options_after_active); + } + + #[test] + fn when_active_option_not_in_options_then_appends_active_option() { + let navigation = Navigation::new(Some(6), vec![1, 2, 3, 4, 5]); + assert_eq!(vec![1, 2, 3, 4, 5], navigation.options_before_active); + assert_eq!(Some(6), navigation.active_option); + assert_eq!(Vec::::new(), navigation.options_after_active); } -} -impl<'a, TOption> Component<'a, Message, Action> for Navigation -where - TOption: Clone + PartialEq + Into>>, -{ - fn update(&mut self, message: Message) -> Action { - update_navigation(self, message) + #[test] + fn when_no_active_option_then_all_options_are_before_active_option() { + let navigation = Navigation::new(None, vec![1, 2, 3, 4, 5]); + assert_eq!(vec![1, 2, 3, 4, 5], navigation.options_before_active); + assert_eq!(None, navigation.active_option); + assert_eq!(Vec::::new(), navigation.options_after_active); } } diff --git a/schist_desktop_gui/src/gui/components/navigation/new_navigation.rs b/schist_desktop_gui/src/gui/components/navigation/new_navigation.rs deleted file mode 100644 index b5c0c19..0000000 --- a/schist_desktop_gui/src/gui/components/navigation/new_navigation.rs +++ /dev/null @@ -1,72 +0,0 @@ -use super::Navigation; - -pub fn new_navigation( - active_option: Option, - options: Vec, -) -> Navigation -where - TOption: Clone + PartialEq, -{ - let active_option_index = active_option.clone().map_or(Option::None, |active_option| { - options.iter().position(|option| option.eq(&active_option)) - }); - if let Some(active_option_index) = active_option_index { - let split = options.split_at(active_option_index); - Navigation { - options_before_active: split.0.to_vec(), - active_option, - options_after_active: split.1.split_at(1).1.to_vec(), - } - } else { - Navigation { - options_before_active: options, - active_option, - options_after_active: vec![], - } - } -} - -#[cfg(test)] -mod test { - use super::Navigation; - - #[test] - fn when_active_option_at_start_of_options_then_finds_active_option() { - let navigation = Navigation::new(Some(1), vec![1, 2, 3, 4, 5]); - assert_eq!(Vec::::new(), navigation.options_before_active); - assert_eq!(Some(1), navigation.active_option); - assert_eq!(vec![2, 3, 4, 5], navigation.options_after_active); - } - - #[test] - fn when_active_option_in_middle_of_options_then_finds_active_option() { - let navigation = Navigation::new(Some(3), vec![1, 2, 3, 4, 5]); - assert_eq!(vec![1, 2], navigation.options_before_active); - assert_eq!(Some(3), navigation.active_option); - assert_eq!(vec![4, 5], navigation.options_after_active); - } - - #[test] - fn when_active_option_at_end_of_options_then_finds_active_option() { - let navigation = Navigation::new(Some(5), vec![1, 2, 3, 4, 5]); - assert_eq!(vec![1, 2, 3, 4], navigation.options_before_active); - assert_eq!(Some(5), navigation.active_option); - assert_eq!(Vec::::new(), navigation.options_after_active); - } - - #[test] - fn when_active_option_not_in_options_then_appends_active_option() { - let navigation = Navigation::new(Some(6), vec![1, 2, 3, 4, 5]); - assert_eq!(vec![1, 2, 3, 4, 5], navigation.options_before_active); - assert_eq!(Some(6), navigation.active_option); - assert_eq!(Vec::::new(), navigation.options_after_active); - } - - #[test] - fn when_no_active_option_then_all_options_are_before_active_option() { - let navigation = Navigation::new(None, vec![1, 2, 3, 4, 5]); - assert_eq!(vec![1, 2, 3, 4, 5], navigation.options_before_active); - assert_eq!(None, navigation.active_option); - assert_eq!(Vec::::new(), navigation.options_after_active); - } -} diff --git a/schist_desktop_gui/src/gui/components/navigation/update_navigation.rs b/schist_desktop_gui/src/gui/components/navigation/update_navigation.rs index f86867f..c44ddd8 100644 --- a/schist_desktop_gui/src/gui/components/navigation/update_navigation.rs +++ b/schist_desktop_gui/src/gui/components/navigation/update_navigation.rs @@ -1,127 +1,130 @@ use iced::Element; +use crate::traits::Component; + use super::{Action, Message, Navigation}; -pub fn update_navigation<'a, TOption>( - navigation: &mut Navigation, - message: Message, -) -> Action +impl<'a, TOption> Component<'a, Message, Action> for Navigation where - TOption: Clone + PartialEq + Into>>, + TOption: Clone + PartialEq + 'a, + Element<'a, Message>: From<&'a TOption>, { - match message { - Message::SelectOption(option) => { - if navigation - .active_option - .as_ref() - .is_some_and(|active_option| active_option.clone() == option) - { - Action::None - } else if let Some(index) = navigation - .options_before_active - .iter() - .position(|o| o.clone() == option) - { - let split = navigation.options_before_active.split_at(index); - navigation.options_after_active = vec![ + fn update(&mut self, message: Message) -> Action { + let navigation: &mut Navigation = self; + match message { + Message::SelectOption(option) => { + if navigation + .active_option + .as_ref() + .is_some_and(|active_option| TOption::eq(active_option, &option)) + { + Action::None + } else if let Some(index) = navigation + .options_before_active + .iter() + .position(|o| TOption::eq(o, &option)) + { + let split = navigation.options_before_active.split_at(index); + navigation.options_after_active = vec![ + navigation + .active_option + .clone() + .map_or(vec![], |active_option| vec![active_option]), + split.1.split_at(1).1.to_vec(), + navigation.options_after_active.clone(), + ] + .concat(); + navigation.options_before_active = split.0.to_vec(); + navigation.active_option = Some(option.clone()); + Action::SelectOption(option) + } else if let Some(index) = navigation + .options_after_active + .iter() + .position(|o| TOption::eq(o, &option)) + { + let split = navigation.options_after_active.split_at(index); + navigation.options_before_active = vec![ + navigation.options_before_active.clone(), + split.0.to_vec(), + navigation + .active_option + .clone() + .map_or(vec![], |active_option| vec![active_option]), + ] + .concat(); + navigation.active_option = Some(option.clone()); + navigation.options_after_active = split.1.split_at(1).1.to_vec(); + Action::SelectOption(option) + } else { + if let Some(active_option) = &navigation.active_option { + navigation.options_before_active.push(active_option.clone()); + } navigation - .active_option - .clone() - .map_or(vec![], |active_option| vec![active_option]), - split.1.split_at(1).1.to_vec(), - navigation.options_after_active.clone(), - ] - .concat(); - navigation.options_before_active = split.0.to_vec(); - navigation.active_option = Some(option.clone()); - Action::SelectOption(option) - } else if let Some(index) = navigation - .options_after_active - .iter() - .position(|o| o.clone() == option) - { - let split = navigation.options_after_active.split_at(index); - navigation.options_before_active = vec![ - navigation.options_before_active.clone(), - split.0.to_vec(), + .options_before_active + .append(&mut navigation.options_after_active); + navigation.active_option = Some(option.clone()); + navigation.options_after_active = vec![]; + Action::SelectOption(option) + } + } + Message::SetOptions(options) => { + let active_option_index = navigation .active_option .clone() - .map_or(vec![], |active_option| vec![active_option]), - ] - .concat(); - navigation.active_option = Some(option.clone()); - navigation.options_after_active = split.1.split_at(1).1.to_vec(); - Action::SelectOption(option) - } else { - if let Some(active_option) = &navigation.active_option { - navigation.options_before_active.push(active_option.clone()); + .map_or(Option::None, |active_option| { + options + .iter() + .position(|option| active_option == option.clone()) + }); + if let Some(active_option_index) = active_option_index { + let split = options.split_at(active_option_index); + navigation.options_before_active = split.0.to_vec(); + navigation.options_after_active = split.1.split_at(1).1.to_vec(); + Action::None + } else { + navigation.options_before_active = options + .split_last() + .map_or_else(Vec::new, |(_last, rest)| rest.to_vec()); + navigation.active_option = options.last().cloned(); + options + .last() + .cloned() + .map_or(Action::None, Action::SelectOption) } - navigation - .options_before_active - .append(&mut navigation.options_after_active); - navigation.active_option = Some(option.clone()); - navigation.options_after_active = vec![]; - Action::SelectOption(option) } - } - Message::SetOptions(options) => { - let active_option_index = - navigation - .active_option - .clone() - .map_or(Option::None, |active_option| { - options - .iter() - .position(|option| active_option == option.clone()) - }); - if let Some(active_option_index) = active_option_index { - let split = options.split_at(active_option_index); - navigation.options_before_active = split.0.to_vec(); - navigation.options_after_active = split.1.split_at(1).1.to_vec(); - Action::None - } else { - navigation.options_before_active = options - .split_last() - .map_or_else(Vec::new, |(_last, rest)| rest.to_vec()); - navigation.active_option = options.last().cloned(); - options - .last() - .cloned() - .map_or(Action::None, Action::SelectOption) - } - } - Message::SelectNext => { - if let Some(next) = navigation.options_after_active.clone().first() { - navigation.options_before_active = vec![ - navigation.options_before_active.clone(), - navigation.active_option.clone().map_or(vec![], |o| vec![o]), - ] - .concat(); - navigation.active_option = Some(next.clone()); - navigation.options_after_active = - navigation.options_after_active.split_at(1).1.to_vec(); - Action::SelectOption(next.clone()) - } else { - Action::None + Message::SelectNext => { + if let Some(next) = navigation.options_after_active.clone().first() { + navigation.options_before_active = vec![ + navigation.options_before_active.clone(), + navigation.active_option.clone().map_or(vec![], |o| vec![o]), + ] + .concat(); + navigation.active_option = Some(next.clone()); + navigation.options_after_active = + navigation.options_after_active.split_at(1).1.to_vec(); + Action::SelectOption(next.clone()) + } else { + Action::None + } } - } - Message::SelectPrev => { - if let Some(prev) = navigation.options_before_active.clone().last() { - navigation.options_after_active = vec![ - navigation.active_option.clone().map_or(vec![], |o| vec![o]), - navigation.options_after_active.clone(), - ] - .concat(); - navigation.active_option = Some(prev.clone()); - navigation.options_before_active = navigation - .options_before_active - .split_last() - .map(|split| split.1.to_vec()) - .unwrap_or_else(Vec::new); - Action::SelectOption(prev.clone()) - } else { - Action::None + Message::SelectPrev => { + if let Some(prev) = navigation.options_before_active.clone().last() { + navigation.options_after_active = vec![ + navigation.active_option.clone().map_or(vec![], |o| vec![o]), + navigation.options_after_active.clone(), + ] + .concat(); + navigation.active_option = Some(prev.clone()); + navigation.options_before_active = navigation + .options_before_active + .split_last() + .map(|split| split.1.to_vec()) + .unwrap_or_else(Vec::new); + Action::SelectOption(prev.clone()) + } else { + Action::None + } } } } diff --git a/schist_desktop_gui/src/gui/components/navigation/view_navigation.rs b/schist_desktop_gui/src/gui/components/navigation/view_navigation.rs index beec818..3f1da18 100644 --- a/schist_desktop_gui/src/gui/components/navigation/view_navigation.rs +++ b/schist_desktop_gui/src/gui/components/navigation/view_navigation.rs @@ -1,26 +1,27 @@ use iced::{widget::column, Element}; +use crate::traits::Viewable; + use super::{Message, Navigation}; -pub fn view_navigation<'a, TOption>( - navigation: &'a Navigation, -) -> Element<'a, Message> +impl<'a, TOption> Viewable<'a, Message> for Navigation where - TOption: Clone + PartialEq + Into>>, + TOption: Clone + PartialEq + 'a, + Element<'a, Message>: From<&'a TOption>, { - let mut options = Vec::new(); - navigation - .options_before_active - .iter() - .map(|option| navigation.button(&option)) - .for_each(|option| options.push(option)); - if let Some(active_option) = navigation.active_option.as_ref() { - options.push(navigation.button(&active_option)); + fn view(&'a self) -> iced::Element<'a, Message> { + let mut options = Vec::new(); + self.options_before_active + .iter() + .map(|option| self.view_navigation_button(&option)) + .for_each(|option| options.push(option)); + if let Some(active_option) = self.active_option.as_ref() { + options.push(self.view_navigation_button(&active_option)); + } + self.options_after_active + .iter() + .map(|option| self.view_navigation_button(&option)) + .for_each(|option| options.push(option)); + column(options).into() } - navigation - .options_after_active - .iter() - .map(|option| navigation.button(&option)) - .for_each(|option| options.push(option)); - column(options).into() } diff --git a/schist_desktop_gui/src/gui/components/navigation/view_navigation_button.rs b/schist_desktop_gui/src/gui/components/navigation/view_navigation_button.rs index 3132bf1..d65328c 100644 --- a/schist_desktop_gui/src/gui/components/navigation/view_navigation_button.rs +++ b/schist_desktop_gui/src/gui/components/navigation/view_navigation_button.rs @@ -4,24 +4,24 @@ use crate::gui::components::{active_button, inactive_button}; use super::{Message, Navigation}; -pub fn view_navigation_button<'a, TOption>( - navigation: &'a Navigation, - option: &'a TOption, -) -> Element<'a, Message> +impl<'a, TOption> Navigation where - TOption: Clone + PartialEq + Into>>, + TOption: Clone + PartialEq + 'a, + Element<'a, Message>: From<&'a TOption>, { - if navigation - .active_option - .as_ref() - .is_some_and(|o| *o == *option) - { - active_button(Into::>>::into(option.clone())).into() - } else { - inactive_button( - Into::>>::into(option.clone()), - Message::SelectOption(option.clone()), - ) + pub(super) fn view_navigation_button( + &'a self, + option: &'a TOption, + ) -> Element<'a, Message> { + if self + .active_option + .as_ref() + .is_some_and(|o| TOption::eq(o, option)) + { + active_button(option) + } else { + inactive_button(option, Message::SelectOption(option.clone())) + } .into() } } diff --git a/schist_desktop_gui/src/gui/components/text.rs b/schist_desktop_gui/src/gui/components/text.rs index 90cd7bc..a3093ac 100644 --- a/schist_desktop_gui/src/gui/components/text.rs +++ b/schist_desktop_gui/src/gui/components/text.rs @@ -1,3 +1,5 @@ +use std::borrow::Borrow; + use crate::style::*; #[derive(Clone, Debug, PartialEq)] @@ -71,30 +73,46 @@ impl Text { } } -impl<'a, Message> Into> for Text { - fn into(self) -> iced::Element<'a, Message> { - iced::widget::text(self.content.clone()) - .size(match self.size { - Some(Size::Small) => TEXT_SIZE_SM, - Some(Size::Base) | None => TEXT_SIZE_BASE, - }) - .style(move |theme: &iced::Theme| iced::widget::text::Style { - color: match (&self.colour, &self.strength) { - (Some(Colour::Danger), Some(Strength::Base) | None) => { - Some(theme.extended_palette().danger.base.text) - } - (Some(Colour::Danger), Some(Strength::Weak)) => { - Some(theme.extended_palette().danger.weak.text) - } - (Some(Colour::Primary), Some(Strength::Base) | None) => { - Some(theme.extended_palette().primary.base.text) - } - (Some(Colour::Primary), Some(Strength::Weak)) => { - Some(theme.extended_palette().primary.weak.text) - } - (None, _) => None, - }, - }) - .into() +impl<'a, Message> From for iced::Element<'a, Message> { + fn from(text: Text) -> Self { + text_as_element(text) + } +} + +impl<'a, Message> From<&'a Text> for iced::Element<'a, Message> { + fn from(text: &'a Text) -> Self { + text_as_element(text) } } + +fn text_as_element<'a, T, Message>(text: T) -> iced::Element<'a, Message> +where + T: Borrow + 'a, +{ + iced::widget::text(text.borrow().content.clone()) + .size(match text.borrow().size { + Some(Size::Small) => TEXT_SIZE_SM, + Some(Size::Base) | None => TEXT_SIZE_BASE, + }) + .style(move |theme: &iced::Theme| iced::widget::text::Style { + color: match ( + &text.borrow().borrow().colour, + &text.borrow().borrow().strength, + ) { + (Some(Colour::Danger), Some(Strength::Base) | None) => { + Some(theme.extended_palette().danger.base.text) + } + (Some(Colour::Danger), Some(Strength::Weak)) => { + Some(theme.extended_palette().danger.weak.text) + } + (Some(Colour::Primary), Some(Strength::Base) | None) => { + Some(theme.extended_palette().primary.base.text) + } + (Some(Colour::Primary), Some(Strength::Weak)) => { + Some(theme.extended_palette().primary.weak.text) + } + (None, _) => None, + }, + }) + .into() +} diff --git a/schist_desktop_gui/src/gui/screens/main_screen/view.rs b/schist_desktop_gui/src/gui/screens/main_screen/view.rs index 2be427b..5f2df4e 100644 --- a/schist_desktop_gui/src/gui/screens/main_screen/view.rs +++ b/schist_desktop_gui/src/gui/screens/main_screen/view.rs @@ -41,8 +41,8 @@ impl View { } } -impl<'a> Into>> for View { - fn into(self) -> Element<'a, navigation::Message> { - Text::new(self.name()).into() +impl<'a> From<&'a View> for Element<'a, navigation::Message> { + fn from(val: &'a View) -> Self { + Text::new(val.name()).into() } } -- cgit v1.2.3 From f4a9ff3471db5c0bd3e68b6ee517fd6887138a85 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Mon, 25 Aug 2025 18:29:50 +0100 Subject: refactor balances_view --- .../src/gui/components/balances_view.rs | 26 ++-------- .../balances_view/update_balances_view.rs | 60 +++++++++++++--------- .../components/balances_view/view_balances_view.rs | 47 +++++++++-------- 3 files changed, 66 insertions(+), 67 deletions(-) diff --git a/schist_desktop_gui/src/gui/components/balances_view.rs b/schist_desktop_gui/src/gui/components/balances_view.rs index 3ca37be..a1c8db3 100644 --- a/schist_desktop_gui/src/gui/components/balances_view.rs +++ b/schist_desktop_gui/src/gui/components/balances_view.rs @@ -3,19 +3,13 @@ mod update_balances_view; mod view_balances_view; mod view_group_names; -use self::{ - update_balances_view::update_balances_view, view_balances_view::view_balances_view, - view_group_names::view_group_names, -}; +use self::view_group_names::view_group_names; use schist_models::Bucket; -use crate::{ - gui::components::{ - balances_view::make_balances_view_greeting::make_balances_view_greeting, navigation, - Navigation, Text, - }, - traits::{Component, Viewable}, +use crate::gui::components::{ + balances_view::make_balances_view_greeting::make_balances_view_greeting, navigation, + Navigation, Text, }; #[derive(Clone, Debug)] @@ -49,15 +43,3 @@ impl BalancesView { } } } - -impl<'a> Viewable<'a, Message> for BalancesView { - fn view(&'a self) -> iced::Element<'a, Message> { - view_balances_view(self) - } -} - -impl<'a> Component<'a, Message, Action> for BalancesView { - fn update(&mut self, message: Message) -> Action { - update_balances_view(self, message) - } -} diff --git a/schist_desktop_gui/src/gui/components/balances_view/update_balances_view.rs b/schist_desktop_gui/src/gui/components/balances_view/update_balances_view.rs index e7b32c3..1dd5c6f 100644 --- a/schist_desktop_gui/src/gui/components/balances_view/update_balances_view.rs +++ b/schist_desktop_gui/src/gui/components/balances_view/update_balances_view.rs @@ -7,42 +7,57 @@ use crate::{ traits::Component, }; -pub fn update_balances_view(balances_view: &mut BalancesView, message: Message) -> Action { - match message { - Message::NavigationMessage(message) => update_navigation(balances_view, message), - Message::SetBuckets(buckets) => set_buckets(balances_view, buckets), - Message::SelectNextBucket => select_next_bucket(balances_view), - Message::SelectPrevBucket => select_prev_bucket(balances_view), +impl<'a> Component<'a, Message, Action> for BalancesView { + fn update(&mut self, message: Message) -> Action { + match message { + Message::NavigationMessage(message) => { + update_navigation(self, message); + Action::None + } + + Message::SetBuckets(buckets) => { + set_buckets(self, buckets); + Action::None + } + + Message::SelectNextBucket => { + select_next_bucket(self); + Action::None + } + + Message::SelectPrevBucket => { + select_prev_bucket(self); + Action::None + } + } } } -fn select_prev_bucket(balances_view: &mut BalancesView) -> Action { +fn select_prev_bucket(balances_view: &mut BalancesView) -> () { match balances_view .navigation .update(navigation::Message::SelectPrev) { navigation::Action::SelectOption(option) => { balances_view.greeting = make_balances_view_greeting(Some(option)); - Action::None } - navigation::Action::None => Action::None, - } + navigation::Action::None => {} + }; } -fn select_next_bucket(balances_view: &mut BalancesView) -> Action { +fn select_next_bucket(balances_view: &mut BalancesView) -> () { match balances_view .navigation .update(navigation::Message::SelectNext) { navigation::Action::SelectOption(option) => { balances_view.greeting = make_balances_view_greeting(Some(option)); - Action::None } - navigation::Action::None => Action::None, - } + navigation::Action::None => {} + }; } -fn set_buckets(balances_view: &mut BalancesView, buckets: Vec) -> Action { +fn set_buckets(balances_view: &mut BalancesView, buckets: Vec) -> () { balances_view.buckets = buckets.clone(); match balances_view .navigation @@ -50,23 +65,18 @@ fn set_buckets(balances_view: &mut BalancesView, buckets: Vec { balances_view.greeting = make_balances_view_greeting(Some(option)); - Action::None } - navigation::Action::None => Action::None, - } + navigation::Action::None => {} + }; } -fn update_navigation( - balances_view: &mut BalancesView, - message: navigation::Message, -) -> Action { +fn update_navigation(balances_view: &mut BalancesView, message: navigation::Message) -> () { match balances_view.navigation.update(message) { navigation::Action::SelectOption(group_name) => { balances_view.greeting = make_balances_view_greeting(Some(group_name)); - Action::None } - navigation::Action::None => Action::None, - } + navigation::Action::None => {} + }; } #[cfg(test)] diff --git a/schist_desktop_gui/src/gui/components/balances_view/view_balances_view.rs b/schist_desktop_gui/src/gui/components/balances_view/view_balances_view.rs index 6a64508..22f22b6 100644 --- a/schist_desktop_gui/src/gui/components/balances_view/view_balances_view.rs +++ b/schist_desktop_gui/src/gui/components/balances_view/view_balances_view.rs @@ -1,23 +1,30 @@ -use iced::widget::{column, row, Container}; -use iced::{alignment, Element, Length}; +use iced::{ + alignment, + widget::{column, row, Container}, + Length, +}; -use crate::gui::components::Text; -use crate::gui::components::{balances_view::Message, BalancesView}; -use crate::style::SPACING_LG; -use crate::traits::Viewable; +use crate::{ + gui::components::{balances_view::Message, BalancesView, Text}, + style::SPACING_LG, + traits::Viewable, +}; -pub fn view_balances_view<'a>(balances_view: &'a BalancesView) -> Element<'a, Message> { - let navigation = balances_view - .navigation - .view() - .map(Message::NavigationMessage); - let main_content = column![Text::default(&balances_view.greeting).as_element()]; - row![ - Container::new(navigation).width(Length::FillPortion(1)), - Container::new(main_content).width(Length::FillPortion(3)), - ] - .align_y(alignment::Vertical::Center) - .height(Length::Fill) - .spacing(SPACING_LG) - .into() +impl<'a> Viewable<'a, Message> for BalancesView { + fn view(&'a self) -> iced::Element<'a, Message> { + let balances_view: &'a BalancesView = self; + let navigation = balances_view + .navigation + .view() + .map(Message::NavigationMessage); + let main_content = column![Text::default(&balances_view.greeting).as_element()]; + row![ + Container::new(navigation).width(Length::FillPortion(1)), + Container::new(main_content).width(Length::FillPortion(3)), + ] + .align_y(alignment::Vertical::Center) + .height(Length::Fill) + .spacing(SPACING_LG) + .into() + } } -- cgit v1.2.3 From 43e4dd1bd0258bfc5164ac8e0d83c12840f23719 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sat, 17 Jan 2026 09:32:06 +0000 Subject: Marks task-073 as done --- requirements/tasks/task-073.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/tasks/task-073.md b/requirements/tasks/task-073.md index cfb4c2f..74e39f4 100644 --- a/requirements/tasks/task-073.md +++ b/requirements/tasks/task-073.md @@ -9,7 +9,7 @@ - All the columns must be properly spaced out - For this ticket, the main content can be empty or contain a placeholder -Status: doing +Status: done Epic: epic-000 -- cgit v1.2.3 From 1056421dd45b7e9af6f63a95026cb069cfa34e29 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Fri, 9 Jan 2026 18:13:36 +0000 Subject: improves balance format/style --- schist_core/schist_models/src/bucket.rs | 13 ++++++++++ .../src/gui/components/bucket_name_and_balance.rs | 28 +++++++++++++--------- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/schist_core/schist_models/src/bucket.rs b/schist_core/schist_models/src/bucket.rs index a28aa8f..67ed31e 100644 --- a/schist_core/schist_models/src/bucket.rs +++ b/schist_core/schist_models/src/bucket.rs @@ -1,3 +1,5 @@ +use std::ops::Div; + use derive_builder::Builder; use diesel::prelude::*; use serde::{Deserialize, Serialize}; @@ -34,3 +36,14 @@ impl Bucket { .all(|t| t.bucket_id.is_none_or(|id| id != self.id)) } } + +impl Bucket { + pub fn pretty_balance_per_100(&self) -> String { + let abs_balance_per_100 = self.balance.unwrap_or(0).div(100).abs(); + if self.balance.is_none_or(|balance| balance >= 0) { + abs_balance_per_100.to_string() + } else { + format!("({abs_balance_per_100})") + } + } +} diff --git a/schist_desktop_gui/src/gui/components/bucket_name_and_balance.rs b/schist_desktop_gui/src/gui/components/bucket_name_and_balance.rs index b08729c..8a5a16e 100644 --- a/schist_desktop_gui/src/gui/components/bucket_name_and_balance.rs +++ b/schist_desktop_gui/src/gui/components/bucket_name_and_balance.rs @@ -1,7 +1,10 @@ -use iced::{widget::row, Element}; +use iced::{ + widget::{row, text}, + Element, Length, +}; use schist_models::Bucket; -use crate::gui::components::{navigation, Text}; +use crate::gui::components::navigation; #[derive(Clone, Debug, PartialEq)] pub struct BucketNameAndBalance { @@ -16,16 +19,19 @@ impl BucketNameAndBalance { impl<'a> From<&'a BucketNameAndBalance> for Element<'a, navigation::Message> { fn from(bucket_name_and_balance: &'a BucketNameAndBalance) -> Self { + let balance_style = if bucket_name_and_balance + .bucket + .balance + .is_none_or(|balance| balance >= 0) + { + text::default + } else { + text::danger + }; + row![ - Text::new(&bucket_name_and_balance.bucket.name).as_element(), - Text::new( - &bucket_name_and_balance - .bucket - .balance - .unwrap_or(0) - .to_string() - ) - .as_element(), + text(bucket_name_and_balance.bucket.name.clone()).width(Length::Fill), + text(bucket_name_and_balance.bucket.pretty_balance_per_100()).style(balance_style), ] .into() } -- cgit v1.2.3 From 2809e6a2ef3ea636f3de5873f6c874923b410400 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Thu, 15 Jan 2026 22:35:39 +0000 Subject: renames categories -> buckets, refactors category_transfers -> bucket_transactions --- schist_core/schist_fakes/src/bucket_transaction.rs | 15 + schist_core/schist_fakes/src/bucket_transfer.rs | 16 - schist_core/schist_fakes/src/lib.rs | 2 +- .../2024-08-31-084439_initial_setup/down.sql | 2 +- .../2024-08-31-084439_initial_setup/up.sql | 11 +- schist_core/schist_models/src/bucket.rs | 2 - .../schist_models/src/bucket_transaction.rs | 11 + schist_core/schist_models/src/bucket_transfer.rs | 12 - schist_core/schist_models/src/lib.rs | 4 +- schist_core/schist_models/src/schema.rs | 8 +- schist_core/schist_models/user_data.sqlite | Bin 45056 -> 49152 bytes .../schist_queries/src/bucket_transactions.rs | 65 ++++ schist_core/schist_queries/src/bucket_transfers.rs | 82 ----- schist_core/schist_queries/src/clear.rs | 6 +- schist_core/schist_queries/src/lib.rs | 2 +- .../schist_queries/tests/bucket_transactions.rs | 160 ++++++++ schist_core/schist_queries/tests/buckets.rs | 83 +++++ .../schist_queries/tests/category_transfers.rs | 201 ---------- .../src/actualbudget_accounts.rs | 21 +- .../tests/actualbudget_accounts.rs | 15 +- .../src/actualbudget_state.rs | 16 +- .../src/schist_state.rs | 8 +- .../src/transform_state.rs | 10 +- .../src/transform_state/is_bucket_transfer.rs | 46 --- .../src/transform_state/is_category_transfer.rs | 46 +++ .../src/transform_state/transform_accounts.rs | 4 +- .../transform_bucket_transactions.rs | 406 +++++++++++++++++++++ .../src/transform_state/transform_state.rs | 12 +- .../src/transform_state/transform_transactions.rs | 4 +- .../transform_transactions_to_bucket_transfers.rs | 383 ------------------- .../tests/common/bucket_transaction_matchers.rs | 51 +++ .../tests/common/bucket_transfer_matchers.rs | 8 +- .../tests/common/mod.rs | 4 +- .../tests/run.rs | 22 +- 34 files changed, 907 insertions(+), 831 deletions(-) create mode 100644 schist_core/schist_fakes/src/bucket_transaction.rs delete mode 100644 schist_core/schist_fakes/src/bucket_transfer.rs create mode 100644 schist_core/schist_models/src/bucket_transaction.rs delete mode 100644 schist_core/schist_models/src/bucket_transfer.rs create mode 100644 schist_core/schist_queries/src/bucket_transactions.rs delete mode 100644 schist_core/schist_queries/src/bucket_transfers.rs create mode 100644 schist_core/schist_queries/tests/bucket_transactions.rs create mode 100644 schist_core/schist_queries/tests/buckets.rs delete mode 100644 schist_core/schist_queries/tests/category_transfers.rs delete mode 100644 transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_bucket_transfer.rs create mode 100644 transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_category_transfer.rs create mode 100644 transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_bucket_transactions.rs delete mode 100644 transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_transactions_to_bucket_transfers.rs create mode 100644 transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_transaction_matchers.rs diff --git a/schist_core/schist_fakes/src/bucket_transaction.rs b/schist_core/schist_fakes/src/bucket_transaction.rs new file mode 100644 index 0000000..3dd0a6f --- /dev/null +++ b/schist_core/schist_fakes/src/bucket_transaction.rs @@ -0,0 +1,15 @@ +use schist_models::{BucketTransaction, BucketTransactionBuilder}; + +pub fn make_fake_bucket_transaction(id: i32) -> BucketTransaction { + make_fake_bucket_transaction_builder(id).build().unwrap() +} + +pub fn make_fake_bucket_transaction_builder(id: i32) -> BucketTransactionBuilder { + let mut builder = BucketTransactionBuilder::default(); + builder + .id(id) + .description(String::from("Fake bucket transaction")) + .amount(0) + .bucket_id(1); + builder +} diff --git a/schist_core/schist_fakes/src/bucket_transfer.rs b/schist_core/schist_fakes/src/bucket_transfer.rs deleted file mode 100644 index 1342ca2..0000000 --- a/schist_core/schist_fakes/src/bucket_transfer.rs +++ /dev/null @@ -1,16 +0,0 @@ -use schist_models::{BucketTransfer, BucketTransferBuilder}; - -pub fn make_fake_bucket_transfer(id: i32) -> BucketTransfer { - make_fake_bucket_transfer_builder(id).build().unwrap() -} - -pub fn make_fake_bucket_transfer_builder(id: i32) -> BucketTransferBuilder { - let mut builder = BucketTransferBuilder::default(); - builder - .id(id) - .description(String::from("Fake bucket transfer")) - .amount(0) - .from_bucket_id(0) - .to_bucket_id(1); - builder -} diff --git a/schist_core/schist_fakes/src/lib.rs b/schist_core/schist_fakes/src/lib.rs index 0e4cbb4..7ac98fa 100644 --- a/schist_core/schist_fakes/src/lib.rs +++ b/schist_core/schist_fakes/src/lib.rs @@ -1,7 +1,7 @@ pub mod account; pub mod account_transfer; pub mod bucket; -pub mod bucket_transfer; +pub mod bucket_transaction; pub mod drip; pub mod pipe; pub mod transaction; diff --git a/schist_core/schist_models/migrations/2024-08-31-084439_initial_setup/down.sql b/schist_core/schist_models/migrations/2024-08-31-084439_initial_setup/down.sql index 3b2c73e..f6ead07 100644 --- a/schist_core/schist_models/migrations/2024-08-31-084439_initial_setup/down.sql +++ b/schist_core/schist_models/migrations/2024-08-31-084439_initial_setup/down.sql @@ -1,7 +1,7 @@ DROP TABLE accounts; DROP TABLE account_transfers; DROP TABLE buckets; -DROP TABLE bucket_transfers; +DROP TABLE bucket_transactions; DROP TABLE drips; DROP TABLE pipes; DROP TABLE transactions; diff --git a/schist_core/schist_models/migrations/2024-08-31-084439_initial_setup/up.sql b/schist_core/schist_models/migrations/2024-08-31-084439_initial_setup/up.sql index b37c626..e645d43 100644 --- a/schist_core/schist_models/migrations/2024-08-31-084439_initial_setup/up.sql +++ b/schist_core/schist_models/migrations/2024-08-31-084439_initial_setup/up.sql @@ -32,17 +32,12 @@ CREATE TABLE buckets( name TEXT NOT NULL ); -CREATE TABLE bucket_transfers( +CREATE TABLE bucket_transactions( id INTEGER NOT NULL PRIMARY KEY, amount INTEGER NOT NULL, + bucket_id INTEGER NOT NULL, description TEXT NOT NULL, - from_bucket_id INTEGER NOT NULL, - to_bucket_id INTEGER NOT NULL, - FOREIGN KEY (from_bucket_id) - REFERENCES buckets (id) - ON UPDATE CASCADE - ON DELETE RESTRICT, - FOREIGN KEY (to_bucket_id) + FOREIGN KEY (bucket_id) REFERENCES buckets (id) ON UPDATE CASCADE ON DELETE RESTRICT diff --git a/schist_core/schist_models/src/bucket.rs b/schist_core/schist_models/src/bucket.rs index 67ed31e..debbab7 100644 --- a/schist_core/schist_models/src/bucket.rs +++ b/schist_core/schist_models/src/bucket.rs @@ -35,9 +35,7 @@ impl Bucket { .iter() .all(|t| t.bucket_id.is_none_or(|id| id != self.id)) } -} -impl Bucket { pub fn pretty_balance_per_100(&self) -> String { let abs_balance_per_100 = self.balance.unwrap_or(0).div(100).abs(); if self.balance.is_none_or(|balance| balance >= 0) { diff --git a/schist_core/schist_models/src/bucket_transaction.rs b/schist_core/schist_models/src/bucket_transaction.rs new file mode 100644 index 0000000..2aeae8a --- /dev/null +++ b/schist_core/schist_models/src/bucket_transaction.rs @@ -0,0 +1,11 @@ +use derive_builder::Builder; +use diesel::prelude::*; + +#[derive(Builder, Queryable, Identifiable, Selectable, Debug, PartialEq, Insertable)] +#[diesel(table_name = crate::schema::bucket_transactions)] +pub struct BucketTransaction { + pub id: i32, + pub amount: i32, + pub bucket_id: i32, + pub description: String, +} diff --git a/schist_core/schist_models/src/bucket_transfer.rs b/schist_core/schist_models/src/bucket_transfer.rs deleted file mode 100644 index 7c3a9d2..0000000 --- a/schist_core/schist_models/src/bucket_transfer.rs +++ /dev/null @@ -1,12 +0,0 @@ -use derive_builder::Builder; -use diesel::prelude::*; - -#[derive(Builder, Queryable, Identifiable, Selectable, Debug, PartialEq, Insertable)] -#[diesel(table_name = crate::schema::bucket_transfers)] -pub struct BucketTransfer { - pub id: i32, - pub amount: i32, - pub description: String, - pub from_bucket_id: i32, - pub to_bucket_id: i32, -} diff --git a/schist_core/schist_models/src/lib.rs b/schist_core/schist_models/src/lib.rs index 3aa4733..2a119cb 100644 --- a/schist_core/schist_models/src/lib.rs +++ b/schist_core/schist_models/src/lib.rs @@ -1,7 +1,7 @@ mod account; mod account_transfer; mod bucket; -mod bucket_transfer; +mod bucket_transaction; mod budget_period_unit; mod date_utc; mod datetime_utc; @@ -15,7 +15,7 @@ pub mod schema; pub use account::Account; pub use account_transfer::{AccountTransfer, AccountTransferBuilder}; pub use bucket::{Bucket, BucketBuilder}; -pub use bucket_transfer::{BucketTransfer, BucketTransferBuilder}; +pub use bucket_transaction::{BucketTransaction, BucketTransactionBuilder}; pub use budget_period_unit::BudgetPeriodUnit; pub use date_utc::DateUtc; pub use datetime_utc::DatetimeUtc; diff --git a/schist_core/schist_models/src/schema.rs b/schist_core/schist_models/src/schema.rs index d16e636..8744696 100644 --- a/schist_core/schist_models/src/schema.rs +++ b/schist_core/schist_models/src/schema.rs @@ -21,12 +21,11 @@ diesel::table! { } diesel::table! { - bucket_transfers (id) { + bucket_transactions (id) { id -> Integer, amount -> Integer, + bucket_id -> Integer, description -> Text, - from_bucket_id -> Integer, - to_bucket_id -> Integer, } } @@ -74,6 +73,7 @@ diesel::table! { } } +diesel::joinable!(bucket_transactions -> buckets (bucket_id)); diesel::joinable!(drips -> buckets (bucket_id)); diesel::joinable!(pipes -> buckets (bucket_id)); diesel::joinable!(transactions -> accounts (account_id)); @@ -81,7 +81,7 @@ diesel::joinable!(transactions -> accounts (account_id)); diesel::allow_tables_to_appear_in_same_query!( account_transfers, accounts, - bucket_transfers, + bucket_transactions, buckets, drips, pipes, diff --git a/schist_core/schist_models/user_data.sqlite b/schist_core/schist_models/user_data.sqlite index 175ce60..81fd25c 100644 Binary files a/schist_core/schist_models/user_data.sqlite and b/schist_core/schist_models/user_data.sqlite differ diff --git a/schist_core/schist_queries/src/bucket_transactions.rs b/schist_core/schist_queries/src/bucket_transactions.rs new file mode 100644 index 0000000..da06341 --- /dev/null +++ b/schist_core/schist_queries/src/bucket_transactions.rs @@ -0,0 +1,65 @@ +use anyhow::{Context, Result}; +use diesel::{dsl::sum, QueryDsl, RunQueryDsl, SelectableHelper, SqliteConnection}; +use schist_models::BucketTransaction; +use schist_schema::schema::bucket_transactions::{ + self as bucket_transactions_schema, dsl::bucket_transactions as bucket_transactions_table, +}; + +pub fn delete_all_bucket_transactions(connection: &mut SqliteConnection) -> Result { + let num_rows_deleted = diesel::delete(bucket_transactions_table) + .execute(connection) + .with_context(|| "failed to delete all bucket transactions")?; + Ok(num_rows_deleted) +} + +pub fn get_all_bucket_transactions( + connection: &mut SqliteConnection, +) -> Result> { + let all_bucket_transactions = bucket_transactions_table + .select(BucketTransaction::as_select()) + .load(connection) + .with_context(|| "failed to get all bucket transactions")?; + Ok(all_bucket_transactions) +} + +pub fn insert_bucket_transactions( + bucket_transactions: &[BucketTransaction], + connection: &mut SqliteConnection, +) -> Result { + let num_rows_inserted = diesel::insert_into(bucket_transactions_table) + .values(bucket_transactions) + .execute(connection) + .with_context(|| insert_err_msg(&bucket_transactions))?; + Ok(num_rows_inserted) +} + +fn insert_err_msg(bucket_transactions: &[BucketTransaction]) -> String { + format!( + "failed to insert bucket transactions: [{}]", + bucket_transactions + .iter() + .map(|ct| ct.id.to_string()) + .collect::>() + .join(", ") + ) +} + +pub fn sum_bucket_transaction_amount_per_bucket_id( + connection: &mut SqliteConnection, +) -> Result> { + let sum = bucket_transactions_table + .group_by(bucket_transactions_schema::bucket_id) + .select(( + bucket_transactions_schema::bucket_id, + sum(bucket_transactions_schema::amount), + )) + .load::<(i32, Option)>(connection) + .map(|result| { + result + .iter() + .map(|sum| (sum.0, sum.1.unwrap_or(0))) + .collect() + }) + .with_context(|| "failed to sum bucket transaction amount per bucket ID")?; + Ok(sum) +} diff --git a/schist_core/schist_queries/src/bucket_transfers.rs b/schist_core/schist_queries/src/bucket_transfers.rs deleted file mode 100644 index 652c8e0..0000000 --- a/schist_core/schist_queries/src/bucket_transfers.rs +++ /dev/null @@ -1,82 +0,0 @@ -use anyhow::{Context, Result}; -use diesel::{dsl::sum, QueryDsl, RunQueryDsl, SelectableHelper, SqliteConnection}; -use schist_models::BucketTransfer; -use schist_schema::schema::bucket_transfers::{ - self as bucket_transfers_schema, dsl::bucket_transfers as bucket_transfers_table, -}; - -pub fn delete_all_bucket_transfers(connection: &mut SqliteConnection) -> Result { - let num_rows_deleted = diesel::delete(bucket_transfers_table) - .execute(connection) - .with_context(|| "failed to delete all bucket transfers")?; - Ok(num_rows_deleted) -} - -pub fn get_all_bucket_transfers(connection: &mut SqliteConnection) -> Result> { - let all_bucket_transfers = bucket_transfers_table - .select(BucketTransfer::as_select()) - .load(connection) - .with_context(|| "failed to get all bucket transfers")?; - Ok(all_bucket_transfers) -} - -pub fn insert_bucket_transfers( - bucket_transfers: &[BucketTransfer], - connection: &mut SqliteConnection, -) -> Result { - let num_rows_inserted = diesel::insert_into(bucket_transfers_table) - .values(bucket_transfers) - .execute(connection) - .with_context(|| insert_err_msg(&bucket_transfers))?; - Ok(num_rows_inserted) -} - -fn insert_err_msg(bucket_transfers: &[BucketTransfer]) -> String { - format!( - "failed to insert bucket transfers: [{}]", - bucket_transfers - .iter() - .map(|ct| ct.id.to_string()) - .collect::>() - .join(", ") - ) -} - -pub fn sum_bucket_transfer_quantity_per_from_bucket_id( - connection: &mut SqliteConnection, -) -> Result> { - let sum = bucket_transfers_table - .group_by(bucket_transfers_schema::from_bucket_id) - .select(( - bucket_transfers_schema::from_bucket_id, - sum(bucket_transfers_schema::amount), - )) - .load::<(i32, Option)>(connection) - .map(|result| { - result - .iter() - .map(|sum| (sum.0, sum.1.unwrap_or(0))) - .collect() - }) - .with_context(|| "failed to sum bucket transfer quantity per from bucket ID")?; - Ok(sum) -} - -pub fn sum_bucket_transfer_quantity_per_to_bucket_id( - connection: &mut SqliteConnection, -) -> Result> { - let sum = bucket_transfers_table - .group_by(bucket_transfers_schema::to_bucket_id) - .select(( - bucket_transfers_schema::to_bucket_id, - sum(bucket_transfers_schema::amount), - )) - .load::<(i32, Option)>(connection) - .map(|vec| { - vec.iter() - .map(|(id, sum)| (*id, sum.unwrap_or(0))) - .collect() - }) - .with_context(|| "failed to sum bucket transfer quantity per to bucket ID")?; - Ok(sum) -} diff --git a/schist_core/schist_queries/src/clear.rs b/schist_core/schist_queries/src/clear.rs index a5a82f3..845e109 100644 --- a/schist_core/schist_queries/src/clear.rs +++ b/schist_core/schist_queries/src/clear.rs @@ -2,7 +2,9 @@ use anyhow::Result; use diesel::SqliteConnection; use crate::{ - account_transfers::delete_all_account_transfers, accounts::delete_all_accounts, bucket_transfers::delete_all_bucket_transfers, buckets::delete_all_buckets, drips::delete_all_drips, pipes::delete_all_pipes, transactions::delete_all_transactions + account_transfers::delete_all_account_transfers, accounts::delete_all_accounts, + bucket_transactions::delete_all_bucket_transactions, buckets::delete_all_buckets, + drips::delete_all_drips, pipes::delete_all_pipes, transactions::delete_all_transactions, }; pub fn clear(connection: &mut SqliteConnection) -> Result<()> { @@ -11,7 +13,7 @@ pub fn clear(connection: &mut SqliteConnection) -> Result<()> { delete_all_pipes(connection)?; delete_all_transactions(connection)?; delete_all_accounts(connection)?; - delete_all_bucket_transfers(connection)?; + delete_all_bucket_transactions(connection)?; delete_all_buckets(connection)?; Ok(()) } diff --git a/schist_core/schist_queries/src/lib.rs b/schist_core/schist_queries/src/lib.rs index ccd558d..ee74d6b 100644 --- a/schist_core/schist_queries/src/lib.rs +++ b/schist_core/schist_queries/src/lib.rs @@ -1,6 +1,6 @@ pub mod account_transfers; pub mod accounts; -pub mod bucket_transfers; +pub mod bucket_transactions; pub mod buckets; pub mod clear; pub mod drips; diff --git a/schist_core/schist_queries/tests/bucket_transactions.rs b/schist_core/schist_queries/tests/bucket_transactions.rs new file mode 100644 index 0000000..9c877af --- /dev/null +++ b/schist_core/schist_queries/tests/bucket_transactions.rs @@ -0,0 +1,160 @@ +mod common; + +use common::test_context::TestContext; +use diesel::{Connection, SqliteConnection}; +use schist_fakes::{ + bucket::make_fake_bucket, bucket_transaction::make_fake_bucket_transaction_builder, +}; +use schist_queries as queries; + +#[test] +fn when_insert_bucket_transactions_then_returns_num_bucket_transactions() { + let context = TestContext::new(); + let connection = &mut SqliteConnection::establish(&context.db_url).unwrap(); + let buckets = [make_fake_bucket(0), make_fake_bucket(1)]; + let bucket_transactions = [ + make_fake_bucket_transaction_builder(0) + .bucket_id(0) + .build() + .unwrap(), + make_fake_bucket_transaction_builder(1) + .bucket_id(1) + .build() + .unwrap(), + ]; + let _ = queries::buckets::insert_buckets(&buckets, connection); + + let num_bucket_transactions_inserted = + queries::bucket_transactions::insert_bucket_transactions(&bucket_transactions, connection) + .unwrap(); + + assert_eq!(num_bucket_transactions_inserted, 2); +} + +#[test] +fn given_bucket_transactions_when_get_all_bucket_transactions_then_return_bucket_transactions() { + let context = TestContext::new(); + let connection = &mut SqliteConnection::establish(&context.db_url).unwrap(); + let buckets = [make_fake_bucket(0), make_fake_bucket(1)]; + let bucket_transactions = [ + make_fake_bucket_transaction_builder(0) + .bucket_id(0) + .build() + .unwrap(), + make_fake_bucket_transaction_builder(1) + .bucket_id(1) + .build() + .unwrap(), + ]; + let _ = queries::buckets::insert_buckets(&buckets, connection); + let _ = + queries::bucket_transactions::insert_bucket_transactions(&bucket_transactions, connection) + .unwrap(); + + let bucket_transactions_in_db = + queries::bucket_transactions::get_all_bucket_transactions(connection).unwrap(); + + assert_eq!(bucket_transactions_in_db.len(), bucket_transactions.len()); + for bucket_transaction in &bucket_transactions { + assert!(bucket_transactions_in_db + .iter() + .any(|bucket_transaction_in_db| bucket_transaction_in_db.eq(&bucket_transaction))); + } + for bucket_transaction_in_db in bucket_transactions_in_db { + assert!(bucket_transactions + .iter() + .any(|bucket_transaction| bucket_transaction.eq(&bucket_transaction_in_db))); + } +} + +#[test] +fn given_bucket_transactions_when_delete_all_bucket_transactions_then_return_num_of_bucket_transactions( +) { + let context = TestContext::new(); + let connection = &mut SqliteConnection::establish(&context.db_url).unwrap(); + let buckets = [make_fake_bucket(0), make_fake_bucket(1)]; + let bucket_transactions = [ + make_fake_bucket_transaction_builder(0) + .bucket_id(0) + .build() + .unwrap(), + make_fake_bucket_transaction_builder(1) + .bucket_id(1) + .build() + .unwrap(), + ]; + let _ = queries::buckets::insert_buckets(&buckets, connection); + let _ = + queries::bucket_transactions::insert_bucket_transactions(&bucket_transactions, connection) + .unwrap(); + + let num_bucket_transactions_deleted = + queries::bucket_transactions::delete_all_bucket_transactions(connection).unwrap(); + + assert_eq!(num_bucket_transactions_deleted, bucket_transactions.len()); +} + +#[test] +fn given_deleted_bucket_transactions_when_get_all_bucket_transactions_then_return_empty() { + let context = TestContext::new(); + let connection = &mut SqliteConnection::establish(&context.db_url).unwrap(); + let buckets = [make_fake_bucket(0), make_fake_bucket(1)]; + let bucket_transactions = [ + make_fake_bucket_transaction_builder(0) + .bucket_id(0) + .build() + .unwrap(), + make_fake_bucket_transaction_builder(1) + .bucket_id(1) + .build() + .unwrap(), + ]; + let _ = queries::buckets::insert_buckets(&buckets, connection); + let _ = + queries::bucket_transactions::insert_bucket_transactions(&bucket_transactions, connection) + .unwrap(); + let _ = queries::bucket_transactions::delete_all_bucket_transactions(connection).unwrap(); + + let bucket_transactions_returned = + queries::bucket_transactions::get_all_bucket_transactions(connection).unwrap(); + + assert_eq!(bucket_transactions_returned.len(), 0); +} + +#[test] +fn given_bucket_transactions_when_sum_quantity_per_bucket_id_then_returns_sum_per_bucket_id() { + let context = TestContext::new(); + let connection = &mut SqliteConnection::establish(&context.db_url).unwrap(); + let buckets = [make_fake_bucket(0), make_fake_bucket(1)]; + let bucket_transactions = [ + make_fake_bucket_transaction_builder(0) + .bucket_id(0) + .amount(1) + .build() + .unwrap(), + make_fake_bucket_transaction_builder(1) + .bucket_id(0) + .amount(2) + .build() + .unwrap(), + make_fake_bucket_transaction_builder(2) + .bucket_id(1) + .amount(4) + .build() + .unwrap(), + ]; + let _ = queries::buckets::insert_buckets(&buckets, connection); + let _ = + queries::bucket_transactions::insert_bucket_transactions(&bucket_transactions, connection) + .unwrap(); + + let sums = + queries::bucket_transactions::sum_bucket_transaction_amount_per_bucket_id(connection); + + assert!(sums.is_ok()); + let mut sums = sums.unwrap(); + sums.sort_by_key(|sum| sum.0); + assert_eq!(buckets.len(), sums.len()); + assert_eq!(3, sums[0].1); + assert_eq!(4, sums[1].1); +} diff --git a/schist_core/schist_queries/tests/buckets.rs b/schist_core/schist_queries/tests/buckets.rs new file mode 100644 index 0000000..72dd4c7 --- /dev/null +++ b/schist_core/schist_queries/tests/buckets.rs @@ -0,0 +1,83 @@ +mod common; + +use common::test_context::TestContext; +use diesel::{Connection, SqliteConnection}; +use schist_fakes::bucket::make_fake_bucket; +use schist_models::Bucket; +use schist_queries as queries; + +fn given_buckets(connection: &mut SqliteConnection) -> Vec { + let buckets = vec![make_fake_bucket(0), make_fake_bucket(1)]; + let _ = queries::buckets::insert_buckets(&buckets, connection).unwrap(); + buckets +} + +#[test] +fn when_insert_buckets_then_returns_num_buckets() { + let context = TestContext::new(); + let connection = &mut SqliteConnection::establish(&context.db_url).unwrap(); + let buckets = [make_fake_bucket(0), make_fake_bucket(1)]; + + let num_buckets_inserted = queries::buckets::insert_buckets(&buckets, connection).unwrap(); + + assert_eq!(num_buckets_inserted, 2); +} + +#[test] +fn given_buckets_when_get_all_buckets_then_return_buckets() { + let context = TestContext::new(); + let connection = &mut SqliteConnection::establish(&context.db_url).unwrap(); + let buckets = given_buckets(connection); + + let buckets_in_db = queries::buckets::get_all_buckets(connection).unwrap(); + + assert_eq!(buckets_in_db.len(), buckets.len()); + for bucket in &buckets { + assert!(buckets_in_db + .iter() + .any(|bucket_in_db| bucket_in_db.eq(&bucket))); + } + for bucket_in_db in buckets_in_db { + assert!(buckets.iter().any(|bucket| bucket.eq(&bucket_in_db))); + } +} + +#[test] +fn given_buckets_when_delete_all_buckets_then_return_num_of_buckets() { + let context = TestContext::new(); + let connection = &mut SqliteConnection::establish(&context.db_url).unwrap(); + let buckets = given_buckets(connection); + + let num_buckets_deleted = queries::buckets::delete_all_buckets(connection).unwrap(); + + assert_eq!(num_buckets_deleted, buckets.len()); +} + +#[test] +fn given_deleted_buckets_when_get_all_buckets_then_return_empty() { + let context = TestContext::new(); + let connection = &mut SqliteConnection::establish(&context.db_url).unwrap(); + given_buckets(connection); + let _ = queries::buckets::delete_all_buckets(connection).unwrap(); + + let buckets_returned = queries::buckets::get_all_buckets(connection).unwrap(); + + assert_eq!(buckets_returned.len(), 0); +} + +#[test] +fn given_buckets_when_get_all_ids_then_return_all_ids() { + let context = TestContext::new(); + let connection = &mut SqliteConnection::establish(&context.db_url).unwrap(); + let buckets = given_buckets(connection); + + let ids = queries::buckets::get_all_bucket_ids(connection).unwrap(); + + assert_eq!(buckets.len(), ids.len()); + for bucket in &buckets { + assert!(ids.iter().any(|id| bucket.id == *id)); + } + for id in ids { + assert!(buckets.iter().any(|bucket| bucket.id == id)); + } +} diff --git a/schist_core/schist_queries/tests/category_transfers.rs b/schist_core/schist_queries/tests/category_transfers.rs deleted file mode 100644 index f7c75a7..0000000 --- a/schist_core/schist_queries/tests/category_transfers.rs +++ /dev/null @@ -1,201 +0,0 @@ -mod common; - -use common::test_context::TestContext; -use diesel::{Connection, SqliteConnection}; -use schist_fakes::{bucket::make_fake_bucket, bucket_transfer::make_fake_bucket_transfer_builder}; -use schist_queries as queries; - -#[test] -fn when_insert_bucket_transfers_then_returns_num_bucket_transfers() { - let context = TestContext::new(); - let connection = &mut SqliteConnection::establish(&context.db_url).unwrap(); - let buckets = [make_fake_bucket(0), make_fake_bucket(1)]; - let bucket_transfers = [ - make_fake_bucket_transfer_builder(0) - .from_bucket_id(0) - .to_bucket_id(1) - .build() - .unwrap(), - make_fake_bucket_transfer_builder(1) - .from_bucket_id(0) - .to_bucket_id(1) - .build() - .unwrap(), - ]; - let _ = queries::buckets::insert_buckets(&buckets, connection); - - let num_bucket_transfers_inserted = - queries::bucket_transfers::insert_bucket_transfers(&bucket_transfers, connection).unwrap(); - - assert_eq!(num_bucket_transfers_inserted, 2); -} - -#[test] -fn given_bucket_transfers_when_get_all_bucket_transfers_then_return_bucket_transfers() { - let context = TestContext::new(); - let connection = &mut SqliteConnection::establish(&context.db_url).unwrap(); - let buckets = [make_fake_bucket(0), make_fake_bucket(1)]; - let bucket_transfers = [ - make_fake_bucket_transfer_builder(0) - .from_bucket_id(0) - .to_bucket_id(1) - .build() - .unwrap(), - make_fake_bucket_transfer_builder(1) - .from_bucket_id(0) - .to_bucket_id(1) - .build() - .unwrap(), - ]; - let _ = queries::buckets::insert_buckets(&buckets, connection); - let _ = - queries::bucket_transfers::insert_bucket_transfers(&bucket_transfers, connection).unwrap(); - - let bucket_transfers_in_db = - queries::bucket_transfers::get_all_bucket_transfers(connection).unwrap(); - - assert_eq!(bucket_transfers_in_db.len(), bucket_transfers.len()); - for bucket_transfer in &bucket_transfers { - assert!(bucket_transfers_in_db - .iter() - .any(|bucket_transfer_in_db| bucket_transfer_in_db.eq(&bucket_transfer))); - } - for bucket_transfer_in_db in bucket_transfers_in_db { - assert!(bucket_transfers - .iter() - .any(|bucket_transfer| bucket_transfer.eq(&bucket_transfer_in_db))); - } -} - -#[test] -fn given_bucket_transfers_when_delete_all_bucket_transfers_then_return_num_of_bucket_transfers() { - let context = TestContext::new(); - let connection = &mut SqliteConnection::establish(&context.db_url).unwrap(); - let buckets = [make_fake_bucket(0), make_fake_bucket(1)]; - let bucket_transfers = [ - make_fake_bucket_transfer_builder(0) - .from_bucket_id(0) - .to_bucket_id(1) - .build() - .unwrap(), - make_fake_bucket_transfer_builder(1) - .from_bucket_id(0) - .to_bucket_id(1) - .build() - .unwrap(), - ]; - let _ = queries::buckets::insert_buckets(&buckets, connection); - let _ = - queries::bucket_transfers::insert_bucket_transfers(&bucket_transfers, connection).unwrap(); - - let num_bucket_transfers_deleted = - queries::bucket_transfers::delete_all_bucket_transfers(connection).unwrap(); - - assert_eq!(num_bucket_transfers_deleted, bucket_transfers.len()); -} - -#[test] -fn given_deleted_bucket_transfers_when_get_all_bucket_transfers_then_return_empty() { - let context = TestContext::new(); - let connection = &mut SqliteConnection::establish(&context.db_url).unwrap(); - let buckets = [make_fake_bucket(0), make_fake_bucket(1)]; - let bucket_transfers = [ - make_fake_bucket_transfer_builder(0) - .from_bucket_id(0) - .to_bucket_id(1) - .build() - .unwrap(), - make_fake_bucket_transfer_builder(1) - .from_bucket_id(0) - .to_bucket_id(1) - .build() - .unwrap(), - ]; - let _ = queries::buckets::insert_buckets(&buckets, connection); - let _ = - queries::bucket_transfers::insert_bucket_transfers(&bucket_transfers, connection).unwrap(); - let _ = queries::bucket_transfers::delete_all_bucket_transfers(connection).unwrap(); - - let bucket_transfers_returned = - queries::bucket_transfers::get_all_bucket_transfers(connection).unwrap(); - - assert_eq!(bucket_transfers_returned.len(), 0); -} - -#[test] -fn given_bucket_transfers_when_sum_quantity_per_from_bucket_id_then_returns_sum_per_from_bucket_id() -{ - let context = TestContext::new(); - let connection = &mut SqliteConnection::establish(&context.db_url).unwrap(); - let buckets = [make_fake_bucket(0), make_fake_bucket(1)]; - let bucket_transfers = [ - make_fake_bucket_transfer_builder(0) - .from_bucket_id(0) - .to_bucket_id(1) - .amount(1) - .build() - .unwrap(), - make_fake_bucket_transfer_builder(1) - .from_bucket_id(0) - .to_bucket_id(1) - .amount(2) - .build() - .unwrap(), - make_fake_bucket_transfer_builder(2) - .from_bucket_id(1) - .to_bucket_id(0) - .amount(4) - .build() - .unwrap(), - ]; - let _ = queries::buckets::insert_buckets(&buckets, connection); - let _ = - queries::bucket_transfers::insert_bucket_transfers(&bucket_transfers, connection).unwrap(); - - let sums = - queries::bucket_transfers::sum_bucket_transfer_quantity_per_from_bucket_id(connection); - - assert!(sums.is_ok()); - let mut sums = sums.unwrap(); - sums.sort_by_key(|sum| sum.0); - assert_eq!(buckets.len(), sums.len()); - assert_eq!(3, sums[0].1); - assert_eq!(4, sums[1].1); -} - -#[test] -fn given_bucket_transfers_when_sum_quantity_per_to_bucket_id_then_returns_sum_per_to_bucket_id() { - let context = TestContext::new(); - let connection = &mut SqliteConnection::establish(&context.db_url).unwrap(); - let buckets = [make_fake_bucket(0), make_fake_bucket(1)]; - let bucket_transfers = [ - make_fake_bucket_transfer_builder(0) - .from_bucket_id(0) - .to_bucket_id(1) - .amount(1) - .build() - .unwrap(), - make_fake_bucket_transfer_builder(1) - .from_bucket_id(0) - .to_bucket_id(1) - .amount(2) - .build() - .unwrap(), - make_fake_bucket_transfer_builder(2) - .from_bucket_id(1) - .to_bucket_id(0) - .amount(4) - .build() - .unwrap(), - ]; - let _ = queries::buckets::insert_buckets(&buckets, connection); - let _ = - queries::bucket_transfers::insert_bucket_transfers(&bucket_transfers, connection).unwrap(); - - let sums = queries::bucket_transfers::sum_bucket_transfer_quantity_per_to_bucket_id(connection); - - assert!(sums.is_ok()); - let mut sums = sums.unwrap(); - sums.sort_by_key(|sum| sum.0); - assert_eq!([(0, 4), (1, 3)], sums.as_slice()); -} 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> { +pub fn get_all_actualbudget_accounts( + connection: &mut SqliteConnection, +) -> Result> { 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> { - 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, pub transactions: Vec, pub pipes: Vec, - pub bucket_transfers: Vec, + pub bucket_transactions: Vec, } 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..1f5ead3 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,27 @@ mod is_account_transfer; -mod is_bucket_transfer; +mod is_category_transfer; mod transform_account_transfers; mod transform_accounts; +mod transform_bucket_transactions; 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::transform_bucket_transactions; 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_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_bucket_transfer.rs deleted file mode 100644 index 188697e..0000000 --- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_bucket_transfer.rs +++ /dev/null @@ -1,46 +0,0 @@ -use actualbudget_models::ActualbudgetTransaction; - -pub fn is_bucket_transfer( - transaction: &ActualbudgetTransaction, - transactions: &[ActualbudgetTransaction], -) -> bool { - is_parent_bucket_transfer(transaction, transactions) - || is_child_bucket_transfer(transaction, transactions) -} - -fn is_parent_bucket_transfer( - transaction: &ActualbudgetTransaction, - transactions: &[ActualbudgetTransaction], -) -> bool { - if !transaction.is_parent { - return false; - } - - let sum_of_children_amounts: i32 = transactions - .iter() - .filter(|t| is_parent_and_child(transaction, t)) - .map(|t| t.amount) - .sum(); - sum_of_children_amounts == 0 -} - -fn is_child_bucket_transfer( - transaction: &ActualbudgetTransaction, - transactions: &[ActualbudgetTransaction], -) -> bool { - transaction.is_child - && transactions - .iter() - .any(|t| is_parent_and_child(t, transaction)) -} - -fn is_parent_and_child(parent: &ActualbudgetTransaction, child: &ActualbudgetTransaction) -> bool { - parent.is_parent && child.is_child && child.parent_id.clone().is_some_and(|id| id == parent.id) -} - -pub fn is_not_bucket_transfer( - transaction: &ActualbudgetTransaction, - transactions: &[ActualbudgetTransaction], -) -> bool { - !is_bucket_transfer(transaction, transactions) -} diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_category_transfer.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_category_transfer.rs new file mode 100644 index 0000000..75d48b7 --- /dev/null +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_category_transfer.rs @@ -0,0 +1,46 @@ +use actualbudget_models::ActualbudgetTransaction; + +pub fn is_category_transfer( + transaction: &ActualbudgetTransaction, + transactions: &[ActualbudgetTransaction], +) -> bool { + is_parent_category_transfer(transaction, transactions) + || is_child_category_transfer(transaction, transactions) +} + +fn is_parent_category_transfer( + transaction: &ActualbudgetTransaction, + transactions: &[ActualbudgetTransaction], +) -> bool { + if !transaction.is_parent { + return false; + } + + let sum_of_children_amounts: i32 = transactions + .iter() + .filter(|t| is_parent_and_child(transaction, t)) + .map(|t| t.amount) + .sum(); + sum_of_children_amounts == 0 +} + +fn is_child_category_transfer( + transaction: &ActualbudgetTransaction, + transactions: &[ActualbudgetTransaction], +) -> bool { + transaction.is_child + && transactions + .iter() + .any(|t| is_parent_and_child(t, transaction)) +} + +fn is_parent_and_child(parent: &ActualbudgetTransaction, child: &ActualbudgetTransaction) -> bool { + parent.is_parent && child.is_child && child.parent_id.clone().is_some_and(|id| id == parent.id) +} + +pub fn is_not_category_transfer( + transaction: &ActualbudgetTransaction, + transactions: &[ActualbudgetTransaction], +) -> bool { + !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.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_bucket_transactions.rs new file mode 100644 index 0000000..22ee95e --- /dev/null +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_bucket_transactions.rs @@ -0,0 +1,406 @@ +use actualbudget_models::ActualbudgetTransaction; +use anyhow::{bail, Context, Result}; +use schist_models::BucketTransaction; + +use super::{is_category_transfer, transform_id}; + +pub fn transform_bucket_transactions( + actualbudget_transactions: &[ActualbudgetTransaction], +) -> Result> { + let actualbudget_category_transfers = actualbudget_transactions + .iter() + .filter(|transaction| is_category_transfer(transaction, actualbudget_transactions)); + let actualbudget_parent_category_transfers: Vec = + actualbudget_category_transfers + .clone() + .filter(|t| t.is_parent) + .map(ActualbudgetTransaction::clone) + .collect(); + let actualbudget_child_category_transfers: Vec = + 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> { + 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> { + 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 = 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 = 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 quantity: i32 = deposit.amount.try_into().unwrap(); + + Ok(vec![ + BucketTransaction { + id: transform_id(&deposit.id), + description: actualbudget_parent_category_transfer + .notes + .clone() + .unwrap_or_else(String::new), + amount: -quantity, + bucket_id: withdrawal_bucket_id, + }, + BucketTransaction { + id: transform_id(&withdrawal.id), + description: actualbudget_parent_category_transfer + .notes + .clone() + .unwrap_or_else(String::new), + amount: quantity, + bucket_id: deposit_id, + }, + ]) +} + +#[cfg(test)] +mod test { + use actualbudget_fakes::actualbudget_transaction::make_fake_actualbudget_transaction_builder; + use actualbudget_models::{ActualbudgetTransaction, ActualbudgetTransactionBuilder}; + use schist_models::BucketTransaction; + + use crate::transform_state::transform_id::transform_id; + + use super::transform_bucket_transactions; + + #[test] + fn when_one_category_transfer_then_transforms() { + let actualbudget_transactions = [ + make_fake_parent_transaction("0"), + make_fake_child_withdrawal("1", "0"), + make_fake_child_deposit("2", "0"), + ]; + + let result = transform_bucket_transactions(&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); + + 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); + } + + #[test] + fn when_many_transactions_then_transforms() { + let actualbudget_transactions = [ + make_fake_parent_transaction("0"), + 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("3"), + 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(&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); + + 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); + + 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); + + 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); + } + + #[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(&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(&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(&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(&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(&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(&actualbudget_transactions_0); + let result_1 = transform_bucket_transactions(&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..8e71a47 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,11 +1,11 @@ use anyhow::{Context, Result}; -use schist_models::{Account, Bucket, BucketTransfer, Pipe, Transaction}; +use schist_models::{Account, Bucket, Pipe, Transaction}; use crate::{actualbudget_state::ActualbudgetState, schist_state::SchistState}; use super::{ - transform_account_transfers, transform_accounts, transform_categories_to_buckets, - transform_date, transform_transactions, transform_transactions_to_bucket_transfers, + transform_account_transfers, transform_accounts, transform_bucket_transactions, + transform_categories_to_buckets, transform_date, transform_transactions, transform_zero_budgets_to_drips::transform_budget_drips, transform_zero_budgets_to_pipes, }; @@ -36,8 +36,8 @@ fn transform_state_without_context(actualbudget_state: ActualbudgetState) -> Res ); let pipes: Vec = transform_zero_budgets_to_pipes(&actualbudget_state.actualbudget_zero_budgets)?; - let bucket_transfers: Vec = - transform_transactions_to_bucket_transfers(&remaining_transactions)?; + let bucket_transactions = + transform_bucket_transactions(&actualbudget_state.actualbudget_transactions)?; Ok(SchistState { first_transaction_date: first_activity_date, @@ -47,6 +47,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> { - 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::>(), - ) - }) - .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::>>() - .with_context(|| "failed to transform category transfers")?) -} - -fn transform_category_transfer( - actualbudget_parent_category_transfer: &ActualbudgetTransaction, - actualbudget_child_category_transfers: &[&ActualbudgetTransaction], -) -> Result> { - 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> { - 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/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..decc189 --- /dev/null +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_transaction_matchers.rs @@ -0,0 +1,51 @@ +use schist_models::{Bucket, BucketTransaction}; + +use super::bucket_names::{BUCKET_NAME_GENERAL, BUCKET_NAME_SAVINGS}; + +pub fn get_bucket_transaction_matchers() -> Vec 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, + bucket_id, + } => *bucket_id == savings_bucket.id && description.as_str() == "", + _ => 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, + bucket_id, + } => *bucket_id == general_bucket.id && description.as_str() == "", + _ => 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 index c31b9bc..a093cce 100644 --- 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 @@ -1,15 +1,15 @@ -use schist_models::{Bucket, BucketTransfer}; +use schist_models::{Bucket, BucketTransaction}; use super::bucket_names::{BUCKET_NAME_GENERAL, BUCKET_NAME_SAVINGS}; -pub fn get_bucket_transfer_matchers() -> Vec bool>> { +pub fn get_bucket_transfer_matchers() -> Vec 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, + bucket_transfer: &BucketTransaction, buckets: &[Bucket], ) -> bool { let savings_bucket = buckets @@ -23,7 +23,7 @@ fn matches_120_savings_to_general_23_oct_2024( .expect("failed to find general bucket"); match bucket_transfer { - BucketTransfer { + BucketTransaction { id: _id, description, amount: 120_00, 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, ); } -- cgit v1.2.3 From bcc165c7493e6bf993c762e48392badae3cbaf30 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sat, 17 Jan 2026 07:46:59 +0000 Subject: ActualbudgetDate is i64 internally --- .../actualbudget_models/src/actualbudget_date.rs | 23 ++++++++++++++++------ 1 file changed, 17 insertions(+), 6 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..622e22b 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)] #[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,7 @@ impl ActualbudgetDate { } pub fn from_i32(i: i32) -> Self { - ActualbudgetDate(i) + ActualbudgetDate(i.into()) } fn date_part(&self) -> DatePart { @@ -51,7 +62,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 +85,6 @@ impl PartialOrd for ActualbudgetDate { impl FromSql for ActualbudgetDate { fn from_sql(bytes: ::RawValue<'_>) -> diesel::deserialize::Result { let int: i32 = FromSql::::from_sql(bytes)?; - Ok(ActualbudgetDate(int)) + Ok(ActualbudgetDate(int.into())) } } -- cgit v1.2.3 From 6679a7ab6a52df9bca907f154cc6b069ebc83a6d Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sat, 17 Jan 2026 07:47:18 +0000 Subject: ActualbudgetDate implements Copy --- .../actualbudget_models/src/actualbudget_date.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_date.rs b/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_date.rs index 622e22b..aaab5e8 100644 --- a/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_date.rs +++ b/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_date.rs @@ -6,7 +6,7 @@ use diesel::{ 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(i64); -- cgit v1.2.3 From 70b326ddbe47fbec3d6516fa223ecde41988a5d2 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sat, 17 Jan 2026 07:47:36 +0000 Subject: ActualbudgetDate::from_ymd() --- .../actualbudget_models/src/actualbudget_date.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_date.rs b/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_date.rs index aaab5e8..08b648e 100644 --- a/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_date.rs +++ b/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_date.rs @@ -54,6 +54,15 @@ impl ActualbudgetDate { ActualbudgetDate(i.into()) } + pub fn from_ymd(year: T, month: S, day: U) -> Self + where + T: Into, + S: Into, + U: Into, + { + Self(year.into() * 10_000 + month.into() * 100 + day.into()) + } + fn date_part(&self) -> DatePart { if self.0 < 10000000 { DatePart::MonthOfYear @@ -88,3 +97,16 @@ impl FromSql for ActualbudgetDate { 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"); + } +} -- cgit v1.2.3 From 76ba747912a391d235a9c4456e1d813dee9712f3 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sat, 17 Jan 2026 07:50:42 +0000 Subject: Publicise DateUtc::first_day_in_month and DateUtc::first_day_in_next_month --- schist_core/schist_models/src/date_utc.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/schist_core/schist_models/src/date_utc.rs b/schist_core/schist_models/src/date_utc.rs index 18a894b..927644c 100644 --- a/schist_core/schist_models/src/date_utc.rs +++ b/schist_core/schist_models/src/date_utc.rs @@ -105,7 +105,7 @@ impl DateUtc { return true; } - fn first_day_in_next_month(&self) -> Self { + pub fn first_day_in_next_month(&self) -> Self { let month = self.month(); let mut curr_guess: Option> = None; let mut curr_add = 1; @@ -120,7 +120,7 @@ impl DateUtc { }; } - fn first_day_in_month(&self) -> Self { + pub fn first_day_in_month(&self) -> Self { let month = self.month(); let mut prev_guess = self.clone(); let mut curr_guess = self.clone(); -- cgit v1.2.3 From fa1d9941e2d3969a1035d36c97f3363628a409c0 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sat, 17 Jan 2026 07:53:58 +0000 Subject: Refactor transform_bucket_transactions -> transform_bucket_transactions_from_category_transfers --- .../src/transform_state.rs | 5 +- .../transform_bucket_transactions.rs | 406 -------------------- ..._bucket_transactions_from_category_transfers.rs | 415 +++++++++++++++++++++ .../src/transform_state/transform_state.rs | 16 +- 4 files changed, 429 insertions(+), 413 deletions(-) delete mode 100644 transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_bucket_transactions.rs create mode 100644 transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_bucket_transactions_from_category_transfers.rs 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 1f5ead3..ae560c6 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 @@ -2,7 +2,7 @@ mod is_account_transfer; mod is_category_transfer; mod transform_account_transfers; mod transform_accounts; -mod transform_bucket_transactions; +mod transform_bucket_transactions_from_category_transfers; mod transform_categories_to_buckets; mod transform_date; mod transform_id; @@ -17,11 +17,12 @@ 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::transform_bucket_transactions; +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_zero_budgets_to_drips::transform_budget_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/transform_bucket_transactions.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_bucket_transactions.rs deleted file mode 100644 index 22ee95e..0000000 --- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_bucket_transactions.rs +++ /dev/null @@ -1,406 +0,0 @@ -use actualbudget_models::ActualbudgetTransaction; -use anyhow::{bail, Context, Result}; -use schist_models::BucketTransaction; - -use super::{is_category_transfer, transform_id}; - -pub fn transform_bucket_transactions( - actualbudget_transactions: &[ActualbudgetTransaction], -) -> Result> { - let actualbudget_category_transfers = actualbudget_transactions - .iter() - .filter(|transaction| is_category_transfer(transaction, actualbudget_transactions)); - let actualbudget_parent_category_transfers: Vec = - actualbudget_category_transfers - .clone() - .filter(|t| t.is_parent) - .map(ActualbudgetTransaction::clone) - .collect(); - let actualbudget_child_category_transfers: Vec = - 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> { - 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> { - 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 = 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 = 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 quantity: i32 = deposit.amount.try_into().unwrap(); - - Ok(vec![ - BucketTransaction { - id: transform_id(&deposit.id), - description: actualbudget_parent_category_transfer - .notes - .clone() - .unwrap_or_else(String::new), - amount: -quantity, - bucket_id: withdrawal_bucket_id, - }, - BucketTransaction { - id: transform_id(&withdrawal.id), - description: actualbudget_parent_category_transfer - .notes - .clone() - .unwrap_or_else(String::new), - amount: quantity, - bucket_id: deposit_id, - }, - ]) -} - -#[cfg(test)] -mod test { - use actualbudget_fakes::actualbudget_transaction::make_fake_actualbudget_transaction_builder; - use actualbudget_models::{ActualbudgetTransaction, ActualbudgetTransactionBuilder}; - use schist_models::BucketTransaction; - - use crate::transform_state::transform_id::transform_id; - - use super::transform_bucket_transactions; - - #[test] - fn when_one_category_transfer_then_transforms() { - let actualbudget_transactions = [ - make_fake_parent_transaction("0"), - make_fake_child_withdrawal("1", "0"), - make_fake_child_deposit("2", "0"), - ]; - - let result = transform_bucket_transactions(&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); - - 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); - } - - #[test] - fn when_many_transactions_then_transforms() { - let actualbudget_transactions = [ - make_fake_parent_transaction("0"), - 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("3"), - 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(&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); - - 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); - - 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); - - 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); - } - - #[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(&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(&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(&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(&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(&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(&actualbudget_transactions_0); - let result_1 = transform_bucket_transactions(&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_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..952405e --- /dev/null +++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_bucket_transactions_from_category_transfers.rs @@ -0,0 +1,415 @@ +use actualbudget_models::ActualbudgetTransaction; +use anyhow::{bail, Context, Result}; +use schist_models::BucketTransaction; + +use super::{is_category_transfer, transform_id}; + +pub fn transform_bucket_transactions_from_category_transfers( + actualbudget_transactions: &[ActualbudgetTransaction], +) -> Result> { + let actualbudget_category_transfers = actualbudget_transactions + .iter() + .filter(|transaction| is_category_transfer(transaction, actualbudget_transactions)); + let actualbudget_parent_category_transfers: Vec = + actualbudget_category_transfers + .clone() + .filter(|t| t.is_parent) + .map(ActualbudgetTransaction::clone) + .collect(); + let actualbudget_child_category_transfers: Vec = + 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> { + 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> { + 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 = 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 = 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 quantity: i32 = deposit.amount.try_into().unwrap(); + + Ok(vec![ + BucketTransaction { + id: transform_id(&deposit.id), + description: actualbudget_parent_category_transfer + .notes + .clone() + .unwrap_or_else(String::new), + amount: -quantity, + bucket_id: withdrawal_bucket_id, + }, + BucketTransaction { + id: transform_id(&withdrawal.id), + description: actualbudget_parent_category_transfer + .notes + .clone() + .unwrap_or_else(String::new), + amount: quantity, + bucket_id: deposit_id, + }, + ]) +} + +#[cfg(test)] +mod test { + use actualbudget_fakes::actualbudget_transaction::make_fake_actualbudget_transaction_builder; + use actualbudget_models::{ActualbudgetTransaction, ActualbudgetTransactionBuilder}; + use schist_models::BucketTransaction; + + use crate::transform_state::transform_id::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("0"), + 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); + + 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); + } + + #[test] + fn when_many_transactions_then_transforms() { + let actualbudget_transactions = [ + make_fake_parent_transaction("0"), + 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("3"), + 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); + + 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); + + 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); + + 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); + } + + #[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 8e71a47..d3ebee5 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,13 @@ use anyhow::{Context, Result}; -use schist_models::{Account, Bucket, Pipe, Transaction}; +use schist_models::{Account, Bucket, BucketTransaction, Pipe, Transaction}; use crate::{actualbudget_state::ActualbudgetState, schist_state::SchistState}; use super::{ - transform_account_transfers, transform_accounts, transform_bucket_transactions, + transform_account_transfers, transform_accounts, + transform_bucket_transactions_from_category_transfers, transform_budget_drips, transform_categories_to_buckets, transform_date, transform_transactions, - transform_zero_budgets_to_drips::transform_budget_drips, transform_zero_budgets_to_pipes, + transform_zero_budgets_to_pipes, }; pub fn transform_state(actualbudget_state: ActualbudgetState) -> Result { @@ -36,8 +37,13 @@ fn transform_state_without_context(actualbudget_state: ActualbudgetState) -> Res ); let pipes: Vec = transform_zero_budgets_to_pipes(&actualbudget_state.actualbudget_zero_budgets)?; - let bucket_transactions = - transform_bucket_transactions(&actualbudget_state.actualbudget_transactions)?; + let bucket_transactions: Vec = + vec![transform_bucket_transactions_from_category_transfers( + &remaining_transactions, + )?] + .into_iter() + .flatten() + .collect(); Ok(SchistState { first_transaction_date: first_activity_date, -- cgit v1.2.3 From 0971a7a4e847a5ae935c164d8d63c18d803ddc70 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sat, 17 Jan 2026 08:34:18 +0000 Subject: BucketTransaction has date field --- schist_core/schist_fakes/src/bucket_transaction.rs | 6 +- .../2024-08-31-084439_initial_setup/up.sql | 1 + .../schist_models/src/bucket_transaction.rs | 3 + schist_core/schist_models/src/schema.rs | 1 + schist_core/schist_models/user_data.sqlite | Bin 49152 -> 0 bytes ..._bucket_transactions_from_category_transfers.rs | 61 +++++++++++++++++++++ .../tests/common/bucket_transaction_matchers.rs | 19 ++++++- 7 files changed, 88 insertions(+), 3 deletions(-) delete mode 100644 schist_core/schist_models/user_data.sqlite diff --git a/schist_core/schist_fakes/src/bucket_transaction.rs b/schist_core/schist_fakes/src/bucket_transaction.rs index 3dd0a6f..aecc057 100644 --- a/schist_core/schist_fakes/src/bucket_transaction.rs +++ b/schist_core/schist_fakes/src/bucket_transaction.rs @@ -1,4 +1,7 @@ -use schist_models::{BucketTransaction, BucketTransactionBuilder}; +use schist_models::{ + bucket_transaction::{BucketTransaction, BucketTransactionBuilder}, + date_utc::DateUtc, +}; pub fn make_fake_bucket_transaction(id: i32) -> BucketTransaction { make_fake_bucket_transaction_builder(id).build().unwrap() @@ -10,6 +13,7 @@ pub fn make_fake_bucket_transaction_builder(id: i32) -> BucketTransactionBuilder .id(id) .description(String::from("Fake bucket transaction")) .amount(0) + .date(DateUtc::from_ymd(1970, 1, 1).unwrap()) .bucket_id(1); builder } diff --git a/schist_core/schist_models/migrations/2024-08-31-084439_initial_setup/up.sql b/schist_core/schist_models/migrations/2024-08-31-084439_initial_setup/up.sql index e645d43..da85b33 100644 --- a/schist_core/schist_models/migrations/2024-08-31-084439_initial_setup/up.sql +++ b/schist_core/schist_models/migrations/2024-08-31-084439_initial_setup/up.sql @@ -36,6 +36,7 @@ CREATE TABLE bucket_transactions( id INTEGER NOT NULL PRIMARY KEY, amount INTEGER NOT NULL, bucket_id INTEGER NOT NULL, + date TEXT NOT NULL, description TEXT NOT NULL, FOREIGN KEY (bucket_id) REFERENCES buckets (id) diff --git a/schist_core/schist_models/src/bucket_transaction.rs b/schist_core/schist_models/src/bucket_transaction.rs index 2aeae8a..a956d0b 100644 --- a/schist_core/schist_models/src/bucket_transaction.rs +++ b/schist_core/schist_models/src/bucket_transaction.rs @@ -1,11 +1,14 @@ use derive_builder::Builder; use diesel::prelude::*; +use crate::date_utc::DateUtc; + #[derive(Builder, Queryable, Identifiable, Selectable, Debug, PartialEq, Insertable)] #[diesel(table_name = crate::schema::bucket_transactions)] pub struct BucketTransaction { pub id: i32, pub amount: i32, pub bucket_id: i32, + pub date: DateUtc, pub description: String, } diff --git a/schist_core/schist_models/src/schema.rs b/schist_core/schist_models/src/schema.rs index 8744696..9579e5c 100644 --- a/schist_core/schist_models/src/schema.rs +++ b/schist_core/schist_models/src/schema.rs @@ -25,6 +25,7 @@ diesel::table! { id -> Integer, amount -> Integer, bucket_id -> Integer, + date -> Text, description -> Text, } } diff --git a/schist_core/schist_models/user_data.sqlite b/schist_core/schist_models/user_data.sqlite deleted file mode 100644 index 81fd25c..0000000 Binary files a/schist_core/schist_models/user_data.sqlite and /dev/null differ 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 index 952405e..c5a8cf6 100644 --- 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 @@ -2,6 +2,8 @@ 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( @@ -105,6 +107,7 @@ fn transform_category_transfer_without_context( } let quantity: i32 = deposit.amount.try_into().unwrap(); + let date = transform_date(&actualbudget_parent_category_transfer.date)?; Ok(vec![ BucketTransaction { @@ -123,6 +126,7 @@ fn transform_category_transfer_without_context( .clone() .unwrap_or_else(String::new), amount: quantity, + date, bucket_id: deposit_id, }, ]) @@ -130,9 +134,20 @@ fn transform_category_transfer_without_context( #[cfg(test)] mod test { +<<<<<<< HEAD use actualbudget_fakes::actualbudget_transaction::make_fake_actualbudget_transaction_builder; use actualbudget_models::{ActualbudgetTransaction, ActualbudgetTransactionBuilder}; use schist_models::BucketTransaction; +======= + use actualbudget_fakes::{ + actualbudget_account::make_fake_actualbudget_account, + actualbudget_transaction::make_fake_actualbudget_transaction_builder, + }; + use actualbudget_models::{ + ActualbudgetDate, ActualbudgetTransaction, ActualbudgetTransactionBuilder, + }; + use schist_models::{bucket_transaction::BucketTransaction, date_utc::DateUtc}; +>>>>>>> 44b35a0 (BucketTransaction has date field) use crate::transform_state::transform_id::transform_id; @@ -141,9 +156,18 @@ mod test { #[test] fn when_one_category_transfer_then_transforms() { let actualbudget_transactions = [ +<<<<<<< HEAD make_fake_parent_transaction("0"), make_fake_child_withdrawal("1", "0"), make_fake_child_deposit("2", "0"), +======= + make_fake_parent_transaction_builder("0") + .date(ActualbudgetDate::from_ymd(2026, 01, 16)) + .build() + .unwrap(), + make_fake_from_child_transaction("1", "0"), + make_fake_to_child_transaction("2", "0"), +>>>>>>> 44b35a0 (BucketTransaction has date field) ]; let result = @@ -157,18 +181,31 @@ mod test { 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 = [ +<<<<<<< HEAD make_fake_parent_transaction("0"), make_fake_child_withdrawal_builder("1", 100, "0") +======= + make_fake_parent_transaction_builder("0") + .date(ActualbudgetDate::from_ymd(2026, 1, 16)) + .build() + .unwrap(), + make_fake_from_child_transaction_builder("1", 100, "0") +>>>>>>> 44b35a0 (BucketTransaction has date field) .category_id(Some(String::from("0"))) .build() .unwrap(), @@ -176,8 +213,16 @@ mod test { .category_id(Some(String::from("1"))) .build() .unwrap(), +<<<<<<< HEAD make_fake_parent_transaction("3"), make_fake_child_withdrawal_builder("4", 200, "3") +======= + make_fake_parent_transaction_builder("3") + .date(ActualbudgetDate::from_ymd(2026, 1, 21)) + .build() + .unwrap(), + make_fake_from_child_transaction_builder("4", 200, "3") +>>>>>>> 44b35a0 (BucketTransaction has date field) .category_id(Some(String::from("1"))) .build() .unwrap(), @@ -209,6 +254,10 @@ mod test { 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() @@ -217,6 +266,10 @@ mod test { 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() @@ -225,6 +278,10 @@ mod test { 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() @@ -233,6 +290,10 @@ mod test { 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] 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 index decc189..20b8383 100644 --- 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 @@ -1,4 +1,5 @@ use schist_models::{Bucket, BucketTransaction}; +use schist_traits::dateable::Dateable; use super::bucket_names::{BUCKET_NAME_GENERAL, BUCKET_NAME_SAVINGS}; @@ -24,8 +25,15 @@ fn matches_withdrawal_120_savings_23_oct_2024( id: _id, description, amount: -120_00, + date, bucket_id, - } => *bucket_id == savings_bucket.id && description.as_str() == "", + } => { + *bucket_id == savings_bucket.id + && description.as_str() == "" + && date.year() == 2024 + && date.month() == 10 + && date.day() == 23 + } _ => false, } } @@ -44,8 +52,15 @@ fn matches_deposit_120_general_23_oct_2024( id: _id, description, amount: 120_00, + date, bucket_id, - } => *bucket_id == general_bucket.id && description.as_str() == "", + } => { + *bucket_id == general_bucket.id + && description.as_str() == "" + && date.year() == 2024 + && date.month() == 10 + && date.day() == 23 + } _ => false, } } -- cgit v1.2.3 From f1da16e460fe21581837c9385c1ca759ac55060c Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sat, 17 Jan 2026 09:22:46 +0000 Subject: ActualbudgetDate::oldest_valid_date() --- .../actualbudget_models/src/actualbudget_date.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_date.rs b/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_date.rs index 08b648e..d8b6e87 100644 --- a/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_date.rs +++ b/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_date.rs @@ -63,6 +63,10 @@ impl ActualbudgetDate { Self(year.into() * 10_000 + month.into() * 100 + day.into()) } + pub fn oldest_valid_date() -> Self { + Self(10000101) + } + fn date_part(&self) -> DatePart { if self.0 < 10000000 { DatePart::MonthOfYear -- cgit v1.2.3 From 9d0add85692402afd328b9a81e42b22fe4da5db1 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sat, 17 Jan 2026 09:24:08 +0000 Subject: Account for zero budgets not carried over when transforming bucket transactions from Actualbudget --- .../src/transform_state.rs | 2 + ...t_transactions_from_budgets_not_carried_over.rs | 350 +++++++++++++++++++++ .../src/transform_state/transform_state.rs | 22 +- 3 files changed, 366 insertions(+), 8 deletions(-) create mode 100644 transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_bucket_transactions_from_budgets_not_carried_over.rs 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 ae560c6..6101403 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 @@ -2,6 +2,7 @@ mod is_account_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; @@ -17,6 +18,7 @@ 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; 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..6001153 --- /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::bucket_transaction::BucketTransaction; +use schist_traits::dateable::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> { + 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 = + 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, + quantity: -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::{bucket_transaction::BucketTransaction, date_utc::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::::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.quantity, 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.quantity == 2); + assert!(transaction.is_some()); + let transaction = transaction.unwrap(); + assert_eq!(transaction.quantity, 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.quantity == 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_state.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs index d3ebee5..0d21a71 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,7 +1,10 @@ use anyhow::{Context, Result}; 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, @@ -37,13 +40,16 @@ fn transform_state_without_context(actualbudget_state: ActualbudgetState) -> Res ); let pipes: Vec = transform_zero_budgets_to_pipes(&actualbudget_state.actualbudget_zero_budgets)?; - let bucket_transactions: Vec = - vec![transform_bucket_transactions_from_category_transfers( - &remaining_transactions, - )?] - .into_iter() - .flatten() - .collect(); + let bucket_transactions: Vec = 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, -- cgit v1.2.3 From 75b2969586b3320e97f9df0030184762b12acb5b Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sat, 17 Jan 2026 16:46:08 +0000 Subject: Rename transform_budget_drips -> transform_zero_budgets_to_drips --- .../src/transform_state.rs | 2 +- .../src/transform_state/transform_state.rs | 7 ++++--- .../src/transform_state/transform_zero_budgets_to_drips.rs | 12 ++++++------ 3 files changed, 11 insertions(+), 10 deletions(-) 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 6101403..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 @@ -24,7 +24,7 @@ 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_zero_budgets_to_drips::transform_budget_drips; +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/transform_state.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs index 0d21a71..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 @@ -8,8 +8,8 @@ use crate::{ use super::{ transform_account_transfers, transform_accounts, - transform_bucket_transactions_from_category_transfers, transform_budget_drips, - transform_categories_to_buckets, transform_date, transform_transactions, + transform_bucket_transactions_from_category_transfers, transform_categories_to_buckets, + transform_date, transform_transactions, transform_zero_budgets_to_drips, transform_zero_budgets_to_pipes, }; @@ -31,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 = transform_categories_to_buckets( &actualbudget_state.actualbudget_categories, &actualbudget_state.actualbudget_category_groups, 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> { let non_zero_actualbudget_zero_budgets: Vec = 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"); -- cgit v1.2.3 From 23de65aef4a7feadaafa0d42c2a801d19f6f31da Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sat, 17 Jan 2026 16:48:30 +0000 Subject: Rename quantity -> amount --- schist_core/schist_queries/src/utils/sum_drips_for_bucket.rs | 2 +- schist_core/schist_queries/tests/bucket_transactions.rs | 2 +- schist_core/schist_queries/tests/pipes.rs | 2 +- ...nsform_bucket_transactions_from_budgets_not_carried_over.rs | 10 +++++----- .../transform_bucket_transactions_from_category_transfers.rs | 6 +++--- .../tests/common/drip_matchers.rs | 6 +++--- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/schist_core/schist_queries/src/utils/sum_drips_for_bucket.rs b/schist_core/schist_queries/src/utils/sum_drips_for_bucket.rs index 20d05a0..27f6036 100644 --- a/schist_core/schist_queries/src/utils/sum_drips_for_bucket.rs +++ b/schist_core/schist_queries/src/utils/sum_drips_for_bucket.rs @@ -42,7 +42,7 @@ mod test { } #[test] - fn when_one_budget_drip_then_return_quantity() { + fn when_one_budget_drip_then_return_amount() { let bucket = make_fake_bucket(0); let budget_drips = [make_fake_drip_builder(0) .amount(100) diff --git a/schist_core/schist_queries/tests/bucket_transactions.rs b/schist_core/schist_queries/tests/bucket_transactions.rs index 9c877af..0b63458 100644 --- a/schist_core/schist_queries/tests/bucket_transactions.rs +++ b/schist_core/schist_queries/tests/bucket_transactions.rs @@ -122,7 +122,7 @@ fn given_deleted_bucket_transactions_when_get_all_bucket_transactions_then_retur } #[test] -fn given_bucket_transactions_when_sum_quantity_per_bucket_id_then_returns_sum_per_bucket_id() { +fn given_bucket_transactions_when_sum_amount_per_bucket_id_then_returns_sum_per_bucket_id() { let context = TestContext::new(); let connection = &mut SqliteConnection::establish(&context.db_url).unwrap(); let buckets = [make_fake_bucket(0), make_fake_bucket(1)]; diff --git a/schist_core/schist_queries/tests/pipes.rs b/schist_core/schist_queries/tests/pipes.rs index 9203aa3..f883fe6 100644 --- a/schist_core/schist_queries/tests/pipes.rs +++ b/schist_core/schist_queries/tests/pipes.rs @@ -115,7 +115,7 @@ fn given_deleted_pipes_when_get_all_pipes_then_return_empty() { } #[test] -fn given_pipes_when_sum_quantity_per_bucket_id_then_returns_sum_quantity_per_bucket_id() { +fn given_pipes_when_sum_amount_per_bucket_id_then_returns_sum_amount_per_bucket_id() { let context = TestContext::new(); let connection = &mut SqliteConnection::establish(&context.db_url).unwrap(); given_accounts(connection); 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 index 6001153..aba37c2 100644 --- 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 @@ -73,7 +73,7 @@ pub fn transform_bucket_transactions_from_budgets_not_carried_over( id: transform_id(&budget.id), description: format!("Actualbudget budget <{}> not carried over", budget.id), date: start_of_next_month, - quantity: -balance_at_end_of_month, + amount: -balance_at_end_of_month, bucket_id: transform_id(&budget.category_id), }); } @@ -193,7 +193,7 @@ mod test { let result = result.unwrap(); assert_eq!(result.len(), 1); let transaction = result.first().unwrap(); - assert_eq!(transaction.quantity, 1); + assert_eq!(transaction.amount, 1); assert_eq!(transaction.bucket_id, transform_id("c0")); assert_eq!(transaction.date, DateUtc::from_ymd(2026, 2, 1).unwrap()); } @@ -291,14 +291,14 @@ mod test { let result = result.unwrap(); assert_eq!(result.len(), 2); - let transaction = result.iter().find(|bt| bt.quantity == 2); + let transaction = result.iter().find(|bt| bt.amount == 2); assert!(transaction.is_some()); let transaction = transaction.unwrap(); - assert_eq!(transaction.quantity, 2); + 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.quantity == 3); + 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")); 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 index c5a8cf6..88ab416 100644 --- 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 @@ -106,7 +106,7 @@ fn transform_category_transfer_without_context( bail!("actualbudget category transfer amounts did not sum to zero"); } - let quantity: i32 = deposit.amount.try_into().unwrap(); + let amount: i32 = deposit.amount.try_into().unwrap(); let date = transform_date(&actualbudget_parent_category_transfer.date)?; Ok(vec![ @@ -116,7 +116,7 @@ fn transform_category_transfer_without_context( .notes .clone() .unwrap_or_else(String::new), - amount: -quantity, + amount: -amount, bucket_id: withdrawal_bucket_id, }, BucketTransaction { @@ -125,7 +125,7 @@ fn transform_category_transfer_without_context( .notes .clone() .unwrap_or_else(String::new), - amount: quantity, + amount, date, bucket_id: deposit_id, }, 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, ); } -- cgit v1.2.3 From 11dbd9b38609fe85c2881bbe3588045f445645e5 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sat, 17 Jan 2026 16:52:10 +0000 Subject: typo --- schist_core/schist_fakes/src/bucket_transaction.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/schist_core/schist_fakes/src/bucket_transaction.rs b/schist_core/schist_fakes/src/bucket_transaction.rs index aecc057..45aa39c 100644 --- a/schist_core/schist_fakes/src/bucket_transaction.rs +++ b/schist_core/schist_fakes/src/bucket_transaction.rs @@ -1,7 +1,4 @@ -use schist_models::{ - bucket_transaction::{BucketTransaction, BucketTransactionBuilder}, - date_utc::DateUtc, -}; +use schist_models::{BucketTransaction, BucketTransactionBuilder, DateUtc}; pub fn make_fake_bucket_transaction(id: i32) -> BucketTransaction { make_fake_bucket_transaction_builder(id).build().unwrap() -- cgit v1.2.3 From 796be14444b7ed46f9555999041edd6ef3e85bde Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sat, 17 Jan 2026 17:04:12 +0000 Subject: More typos and stuff --- schist_desktop_gui/src/paths.rs | 2 +- ...t_transactions_from_budgets_not_carried_over.rs | 6 ++-- ..._bucket_transactions_from_category_transfers.rs | 38 ++++------------------ .../tests/common/bucket_transaction_matchers.rs | 2 +- 4 files changed, 12 insertions(+), 36 deletions(-) diff --git a/schist_desktop_gui/src/paths.rs b/schist_desktop_gui/src/paths.rs index f5cbf33..b098859 100644 --- a/schist_desktop_gui/src/paths.rs +++ b/schist_desktop_gui/src/paths.rs @@ -33,7 +33,7 @@ fn get_file_paths_checked() -> anyhow::Result> { fn is_sqlite_file(entry: &std::fs::DirEntry) -> bool { entry .file_type() - .is_ok_and(|ft| (FileType::is_file(&ft) || FileType::is_symlink(&ft))) + .is_ok_and(|ft| FileType::is_file(&ft) || FileType::is_symlink(&ft)) && entry .path() .extension() 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 index aba37c2..1bdeaff 100644 --- 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 @@ -3,8 +3,8 @@ use std::collections::HashMap; use actualbudget_models::{ActualbudgetDate, ActualbudgetTransaction, ActualbudgetZeroBudget}; use anyhow::Result; use itertools::Itertools; -use schist_models::bucket_transaction::BucketTransaction; -use schist_traits::dateable::Dateable; +use schist_models::BucketTransaction; +use schist_traits::Dateable; use crate::transform_state::{transform_date, transform_id}; @@ -89,7 +89,7 @@ mod test { actualbudget_zero_budget::make_fake_actualbudget_zero_budget_builder, }; use actualbudget_models::ActualbudgetDate; - use schist_models::{bucket_transaction::BucketTransaction, date_utc::DateUtc}; + 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, 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 index 88ab416..4dce8fa 100644 --- 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 @@ -117,6 +117,7 @@ fn transform_category_transfer_without_context( .clone() .unwrap_or_else(String::new), amount: -amount, + date, bucket_id: withdrawal_bucket_id, }, BucketTransaction { @@ -134,40 +135,25 @@ fn transform_category_transfer_without_context( #[cfg(test)] mod test { -<<<<<<< HEAD use actualbudget_fakes::actualbudget_transaction::make_fake_actualbudget_transaction_builder; - use actualbudget_models::{ActualbudgetTransaction, ActualbudgetTransactionBuilder}; - use schist_models::BucketTransaction; -======= - use actualbudget_fakes::{ - actualbudget_account::make_fake_actualbudget_account, - actualbudget_transaction::make_fake_actualbudget_transaction_builder, - }; use actualbudget_models::{ ActualbudgetDate, ActualbudgetTransaction, ActualbudgetTransactionBuilder, }; - use schist_models::{bucket_transaction::BucketTransaction, date_utc::DateUtc}; ->>>>>>> 44b35a0 (BucketTransaction has date field) + use schist_models::{BucketTransaction, DateUtc}; - use crate::transform_state::transform_id::transform_id; + 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 = [ -<<<<<<< HEAD - make_fake_parent_transaction("0"), - make_fake_child_withdrawal("1", "0"), - make_fake_child_deposit("2", "0"), -======= make_fake_parent_transaction_builder("0") .date(ActualbudgetDate::from_ymd(2026, 01, 16)) .build() .unwrap(), - make_fake_from_child_transaction("1", "0"), - make_fake_to_child_transaction("2", "0"), ->>>>>>> 44b35a0 (BucketTransaction has date field) + make_fake_child_withdrawal("1", "0"), + make_fake_child_deposit("2", "0"), ]; let result = @@ -196,16 +182,11 @@ mod test { #[test] fn when_many_transactions_then_transforms() { let actualbudget_transactions = [ -<<<<<<< HEAD - make_fake_parent_transaction("0"), - make_fake_child_withdrawal_builder("1", 100, "0") -======= make_fake_parent_transaction_builder("0") .date(ActualbudgetDate::from_ymd(2026, 1, 16)) .build() .unwrap(), - make_fake_from_child_transaction_builder("1", 100, "0") ->>>>>>> 44b35a0 (BucketTransaction has date field) + make_fake_child_withdrawal_builder("1", 100, "0") .category_id(Some(String::from("0"))) .build() .unwrap(), @@ -213,16 +194,11 @@ mod test { .category_id(Some(String::from("1"))) .build() .unwrap(), -<<<<<<< HEAD - make_fake_parent_transaction("3"), - make_fake_child_withdrawal_builder("4", 200, "3") -======= make_fake_parent_transaction_builder("3") .date(ActualbudgetDate::from_ymd(2026, 1, 21)) .build() .unwrap(), - make_fake_from_child_transaction_builder("4", 200, "3") ->>>>>>> 44b35a0 (BucketTransaction has date field) + make_fake_child_withdrawal_builder("4", 200, "3") .category_id(Some(String::from("1"))) .build() .unwrap(), 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 index 20b8383..95bb54f 100644 --- 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 @@ -1,5 +1,5 @@ use schist_models::{Bucket, BucketTransaction}; -use schist_traits::dateable::Dateable; +use schist_traits::Dateable; use super::bucket_names::{BUCKET_NAME_GENERAL, BUCKET_NAME_SAVINGS}; -- cgit v1.2.3 From ac45871306b7a917b199876598c192320ebc8a4f Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sat, 17 Jan 2026 17:11:14 +0000 Subject: remove queries/categories --- schist_core/schist_queries/tests/categories.rs | 83 -------------------------- 1 file changed, 83 deletions(-) delete mode 100644 schist_core/schist_queries/tests/categories.rs diff --git a/schist_core/schist_queries/tests/categories.rs b/schist_core/schist_queries/tests/categories.rs deleted file mode 100644 index 72dd4c7..0000000 --- a/schist_core/schist_queries/tests/categories.rs +++ /dev/null @@ -1,83 +0,0 @@ -mod common; - -use common::test_context::TestContext; -use diesel::{Connection, SqliteConnection}; -use schist_fakes::bucket::make_fake_bucket; -use schist_models::Bucket; -use schist_queries as queries; - -fn given_buckets(connection: &mut SqliteConnection) -> Vec { - let buckets = vec![make_fake_bucket(0), make_fake_bucket(1)]; - let _ = queries::buckets::insert_buckets(&buckets, connection).unwrap(); - buckets -} - -#[test] -fn when_insert_buckets_then_returns_num_buckets() { - let context = TestContext::new(); - let connection = &mut SqliteConnection::establish(&context.db_url).unwrap(); - let buckets = [make_fake_bucket(0), make_fake_bucket(1)]; - - let num_buckets_inserted = queries::buckets::insert_buckets(&buckets, connection).unwrap(); - - assert_eq!(num_buckets_inserted, 2); -} - -#[test] -fn given_buckets_when_get_all_buckets_then_return_buckets() { - let context = TestContext::new(); - let connection = &mut SqliteConnection::establish(&context.db_url).unwrap(); - let buckets = given_buckets(connection); - - let buckets_in_db = queries::buckets::get_all_buckets(connection).unwrap(); - - assert_eq!(buckets_in_db.len(), buckets.len()); - for bucket in &buckets { - assert!(buckets_in_db - .iter() - .any(|bucket_in_db| bucket_in_db.eq(&bucket))); - } - for bucket_in_db in buckets_in_db { - assert!(buckets.iter().any(|bucket| bucket.eq(&bucket_in_db))); - } -} - -#[test] -fn given_buckets_when_delete_all_buckets_then_return_num_of_buckets() { - let context = TestContext::new(); - let connection = &mut SqliteConnection::establish(&context.db_url).unwrap(); - let buckets = given_buckets(connection); - - let num_buckets_deleted = queries::buckets::delete_all_buckets(connection).unwrap(); - - assert_eq!(num_buckets_deleted, buckets.len()); -} - -#[test] -fn given_deleted_buckets_when_get_all_buckets_then_return_empty() { - let context = TestContext::new(); - let connection = &mut SqliteConnection::establish(&context.db_url).unwrap(); - given_buckets(connection); - let _ = queries::buckets::delete_all_buckets(connection).unwrap(); - - let buckets_returned = queries::buckets::get_all_buckets(connection).unwrap(); - - assert_eq!(buckets_returned.len(), 0); -} - -#[test] -fn given_buckets_when_get_all_ids_then_return_all_ids() { - let context = TestContext::new(); - let connection = &mut SqliteConnection::establish(&context.db_url).unwrap(); - let buckets = given_buckets(connection); - - let ids = queries::buckets::get_all_bucket_ids(connection).unwrap(); - - assert_eq!(buckets.len(), ids.len()); - for bucket in &buckets { - assert!(ids.iter().any(|id| bucket.id == *id)); - } - for id in ids { - assert!(buckets.iter().any(|bucket| bucket.id == id)); - } -} -- cgit v1.2.3 From f69ec8416c4e6301dcfa06791b025785d7f61b6b Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sat, 17 Jan 2026 17:17:10 +0000 Subject: remove bucket_transfer_matchers.rs --- .../tests/common/bucket_transfer_matchers.rs | 39 ---------------------- 1 file changed, 39 deletions(-) delete mode 100644 transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_transfer_matchers.rs 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 a093cce..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, BucketTransaction}; - -use super::bucket_names::{BUCKET_NAME_GENERAL, BUCKET_NAME_SAVINGS}; - -pub fn get_bucket_transfer_matchers() -> Vec 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: &BucketTransaction, - 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 { - BucketTransaction { - 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, - } -} -- cgit v1.2.3