envelope_cli/models/
income.rs1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9use super::ids::IncomeId;
10use super::money::Money;
11use super::period::BudgetPeriod;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum IncomeValidationError {
16 NegativeAmount,
17}
18
19impl std::fmt::Display for IncomeValidationError {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 match self {
22 Self::NegativeAmount => write!(f, "Expected income cannot be negative"),
23 }
24 }
25}
26
27impl std::error::Error for IncomeValidationError {}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct IncomeExpectation {
32 pub id: IncomeId,
33 pub period: BudgetPeriod,
34 pub expected_amount: Money,
35 #[serde(default)]
36 pub notes: String,
37 pub created_at: DateTime<Utc>,
38 pub updated_at: DateTime<Utc>,
39}
40
41impl IncomeExpectation {
42 pub fn new(period: BudgetPeriod, expected_amount: Money) -> Self {
44 let now = Utc::now();
45 Self {
46 id: IncomeId::new(),
47 period,
48 expected_amount,
49 notes: String::new(),
50 created_at: now,
51 updated_at: now,
52 }
53 }
54
55 pub fn set_expected_amount(&mut self, amount: Money) {
57 self.expected_amount = amount;
58 self.updated_at = Utc::now();
59 }
60
61 pub fn set_notes(&mut self, notes: impl Into<String>) {
63 self.notes = notes.into();
64 self.updated_at = Utc::now();
65 }
66
67 pub fn validate(&self) -> Result<(), IncomeValidationError> {
69 if self.expected_amount.is_negative() {
70 return Err(IncomeValidationError::NegativeAmount);
71 }
72 Ok(())
73 }
74
75 pub fn is_over_budget(&self, total_budgeted: Money) -> bool {
77 total_budgeted > self.expected_amount
78 }
79
80 pub fn budget_difference(&self, total_budgeted: Money) -> Money {
83 self.expected_amount - total_budgeted
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 #[test]
92 fn test_new_income_expectation() {
93 let period = BudgetPeriod::monthly(2025, 1);
94 let income = IncomeExpectation::new(period.clone(), Money::from_cents(500000));
95
96 assert_eq!(income.period, period);
97 assert_eq!(income.expected_amount.cents(), 500000);
98 assert!(income.notes.is_empty());
99 }
100
101 #[test]
102 fn test_validation_negative_amount() {
103 let period = BudgetPeriod::monthly(2025, 1);
104 let income = IncomeExpectation::new(period, Money::from_cents(-100));
105
106 assert!(matches!(
107 income.validate(),
108 Err(IncomeValidationError::NegativeAmount)
109 ));
110 }
111
112 #[test]
113 fn test_over_budget_detection() {
114 let period = BudgetPeriod::monthly(2025, 1);
115 let income = IncomeExpectation::new(period, Money::from_cents(500000)); assert!(!income.is_over_budget(Money::from_cents(400000))); assert!(!income.is_over_budget(Money::from_cents(500000))); assert!(income.is_over_budget(Money::from_cents(600000))); }
126
127 #[test]
128 fn test_budget_difference() {
129 let period = BudgetPeriod::monthly(2025, 1);
130 let income = IncomeExpectation::new(period, Money::from_cents(500000)); let diff = income.budget_difference(Money::from_cents(400000));
134 assert_eq!(diff.cents(), 100000);
135
136 let diff = income.budget_difference(Money::from_cents(600000));
138 assert_eq!(diff.cents(), -100000);
139 }
140
141 #[test]
142 fn test_set_expected_amount() {
143 let period = BudgetPeriod::monthly(2025, 1);
144 let mut income = IncomeExpectation::new(period, Money::from_cents(500000));
145 let original_updated = income.updated_at;
146
147 std::thread::sleep(std::time::Duration::from_millis(10));
149
150 income.set_expected_amount(Money::from_cents(600000));
151 assert_eq!(income.expected_amount.cents(), 600000);
152 assert!(income.updated_at >= original_updated);
153 }
154
155 #[test]
156 fn test_set_notes() {
157 let period = BudgetPeriod::monthly(2025, 1);
158 let mut income = IncomeExpectation::new(period, Money::from_cents(500000));
159
160 income.set_notes("Includes bonus");
161 assert_eq!(income.notes, "Includes bonus");
162 }
163
164 #[test]
165 fn test_serialization() {
166 let period = BudgetPeriod::monthly(2025, 1);
167 let income = IncomeExpectation::new(period, Money::from_cents(500000));
168
169 let json = serde_json::to_string(&income).unwrap();
170 let deserialized: IncomeExpectation = serde_json::from_str(&json).unwrap();
171
172 assert_eq!(income.id, deserialized.id);
173 assert_eq!(income.period, deserialized.period);
174 assert_eq!(income.expected_amount, deserialized.expected_amount);
175 }
176}