use crate::models::{budget_update::BudgetUpdate, category::Category, date_time_utc::DateTimeUtc}; pub fn calculate_budgets_accrual(category: &Category, budget_updates: &[BudgetUpdate]) -> i64 { let mut budget_updates = budget_updates .iter() .filter(|bu| bu.category_id == category.id) .collect::>(); budget_updates.sort_by_key(|bu| bu.date); budget_updates.reverse(); let mut budgets_accrual = 0.0_f64; let mut calculated_back_to_date = DateTimeUtc::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 { continue; } 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; } budgets_accrual.floor() as i64 } #[cfg(test)] mod test { use chrono::{Days, Timelike, Utc}; use crate::models::{ budget_update::BudgetUpdate, category::Category, date_time_utc::DateTimeUtc, }; use super::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_eq!(result, 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: DateTimeUtc::now(), new_budget: 100, new_period: 1, }]; let result = calculate_budgets_accrual(&category, &budget_updates); assert_eq!(result, 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: DateTimeUtc::new(Utc::now().checked_sub_days(Days::new(120)).unwrap()), new_budget: 100, new_period: 1, }]; let result = calculate_budgets_accrual(&category, &budget_updates); assert_eq!(result, 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: DateTimeUtc::new(Utc::now().checked_sub_days(Days::new(60)).unwrap()), new_budget: 200, new_period: 1, }, BudgetUpdate { id: 0, category_id: 0, date: DateTimeUtc::new(Utc::now().checked_sub_days(Days::new(120)).unwrap()), new_budget: 100, new_period: 1, }, ]; let result = calculate_budgets_accrual(&category, &budget_updates); assert_eq!(result, 18_000); } #[test] fn when_two_budget_updates_on_same_day_then_uses_latest_only() { let category = Category { id: 0, name: String::from("Groceries"), }; let budget_updates = [ BudgetUpdate { id: 0, category_id: 0, date: DateTimeUtc::new( Utc::now() .checked_sub_days(Days::new(120)) .unwrap() .with_hour(13) .unwrap(), ), new_budget: 200, new_period: 1, }, BudgetUpdate { id: 0, category_id: 0, date: DateTimeUtc::new( Utc::now() .checked_sub_days(Days::new(120)) .unwrap() .with_hour(12) .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_eq!(result_leftways, 24_000); assert_eq!(result_rightways, 24_000); } }