use anyhow::Result; use crate::{models::{ budget_update::BudgetUpdate, category::Category, date_utc::DateUtc }, traits::nowlike::Nowlike}; pub fn calculate_budgets_accrual(category: &Category, budget_updates: &[BudgetUpdate]) -> Result { let mut budget_updates = budget_updates .iter() .filter(|bu| bu.category_id == category.id) .collect::>(); budget_updates.sort_by_key(|bu| bu.date.clone()); budget_updates.reverse(); let mut budgets_accrual = 0.0_f64; let mut calculated_back_to_date = DateUtc::now(); for budget_update in budget_updates { let days_on_this_budget = calculated_back_to_date.day_diff(&budget_update.date); if days_on_this_budget == 0_u32 { return Err(anyhow::Error::msg(format!( "failed to calculate budgets accrual since two budget updates had the same date ({})", budget_update.date, ))); } let budget_per_day = budget_update.new_budget as f64 / budget_update.new_period as f64; budgets_accrual += days_on_this_budget as f64 * budget_per_day; calculated_back_to_date = budget_update.date.clone(); } Ok(budgets_accrual.floor() as i64) } #[cfg(test)] mod test { use chrono::{Days, Utc}; use crate::{ models::{budget_update::BudgetUpdate, category::Category, date_utc::DateUtc}, traits::nowlike::Nowlike, utils::calculate_budgets_accrual }; #[test] fn when_no_budget_updates_then_return_zero() { let category = Category { id: 0, name: String::from("Groceries"), }; let budget_updates = []; let result = calculate_budgets_accrual(&category, &budget_updates); assert!(result.is_ok()); assert_eq!(result.unwrap(), 0_i64); } #[test] fn when_one_budget_update_for_another_category_then_return_zero() { let category = Category { id: 0, name: String::from("Groceries"), }; let budget_updates = [BudgetUpdate { id: 0, category_id: 1, date: DateUtc::now(), new_budget: 100, new_period: 1, }]; let result = calculate_budgets_accrual(&category, &budget_updates); assert!(result.is_ok()); assert_eq!(result.unwrap(), 0_i64); } #[test] fn when_one_budget_update_then_return_quantity_times_days_in_past() { let category = Category { id: 0, name: String::from("Groceries"), }; let budget_updates = [BudgetUpdate { id: 0, category_id: 0, date: DateUtc::from(Utc::now().checked_sub_days(Days::new(120)).unwrap()), new_budget: 100, new_period: 1, }]; let result = calculate_budgets_accrual(&category, &budget_updates); assert!(result.is_ok()); assert_eq!(result.unwrap(), 12_000); } #[test] fn when_two_budget_updates_then_calculates_as_expected() { let category = Category { id: 0, name: String::from("Groceries"), }; let budget_updates = [ BudgetUpdate { id: 0, category_id: 0, date: DateUtc::from(&Utc::now().checked_sub_days(Days::new(60)).unwrap()), new_budget: 200, new_period: 1, }, BudgetUpdate { id: 0, category_id: 0, date: DateUtc::from(&Utc::now().checked_sub_days(Days::new(120)).unwrap()), new_budget: 100, new_period: 1, }, ]; let result = calculate_budgets_accrual(&category, &budget_updates); assert!(result.is_ok()); assert_eq!(result.unwrap(), 18_000); } #[test] fn when_two_budget_updates_on_same_day_then_returns_err() { let category = Category { id: 0, name: String::from("Groceries"), }; let budget_updates = [ BudgetUpdate { id: 0, category_id: 0, date: DateUtc::from_ymd(2020, 12, 25).unwrap(), new_budget: 200, new_period: 1, }, BudgetUpdate { id: 0, category_id: 0, date: DateUtc::from_ymd(2020, 12, 25).unwrap(), new_budget: 100, new_period: 1, }, ]; let mut budget_updates_reversed = budget_updates.clone(); budget_updates_reversed.reverse(); let result_leftways = calculate_budgets_accrual(&category, &budget_updates); let result_rightways = calculate_budgets_accrual(&category, &budget_updates_reversed); assert!(result_leftways.is_err()); assert!(result_rightways.is_err()); } }