envelope_cli/services/
income.rs1use crate::audit::EntityType;
6use crate::error::{EnvelopeError, EnvelopeResult};
7use crate::models::{BudgetPeriod, IncomeExpectation, Money};
8use crate::storage::Storage;
9
10pub struct IncomeService<'a> {
12 storage: &'a Storage,
13}
14
15impl<'a> IncomeService<'a> {
16 pub fn new(storage: &'a Storage) -> Self {
18 Self { storage }
19 }
20
21 pub fn set_expected_income(
23 &self,
24 period: &BudgetPeriod,
25 amount: Money,
26 notes: Option<String>,
27 ) -> EnvelopeResult<IncomeExpectation> {
28 if let Some(existing) = self.storage.income.get_for_period(period) {
30 let mut updated = existing.clone();
31 let before = existing.clone();
32
33 updated.set_expected_amount(amount);
34 if let Some(n) = notes {
35 updated.set_notes(n);
36 }
37
38 updated
40 .validate()
41 .map_err(|e| EnvelopeError::Validation(e.to_string()))?;
42
43 self.storage.income.upsert(updated.clone())?;
45 self.storage.income.save()?;
46
47 self.storage.log_update(
49 EntityType::IncomeExpectation,
50 updated.id.to_string(),
51 Some(format!("Income for {}", period)),
52 &before,
53 &updated,
54 Some(format!(
55 "{} -> {}",
56 before.expected_amount, updated.expected_amount
57 )),
58 )?;
59
60 Ok(updated)
61 } else {
62 let mut expectation = IncomeExpectation::new(period.clone(), amount);
63 if let Some(n) = notes {
64 expectation.set_notes(n);
65 }
66
67 expectation
69 .validate()
70 .map_err(|e| EnvelopeError::Validation(e.to_string()))?;
71
72 self.storage.income.upsert(expectation.clone())?;
74 self.storage.income.save()?;
75
76 self.storage.log_create(
78 EntityType::IncomeExpectation,
79 expectation.id.to_string(),
80 Some(format!("Income for {}", period)),
81 &expectation,
82 )?;
83
84 Ok(expectation)
85 }
86 }
87
88 pub fn get_expected_income(&self, period: &BudgetPeriod) -> Option<Money> {
90 self.storage
91 .income
92 .get_for_period(period)
93 .map(|e| e.expected_amount)
94 }
95
96 pub fn get_income_expectation(&self, period: &BudgetPeriod) -> Option<IncomeExpectation> {
98 self.storage.income.get_for_period(period)
99 }
100
101 pub fn delete_expected_income(&self, period: &BudgetPeriod) -> EnvelopeResult<bool> {
103 if let Some(removed) = self.storage.income.delete_for_period(period) {
104 self.storage.income.save()?;
105
106 self.storage.log_delete(
108 EntityType::IncomeExpectation,
109 removed.id.to_string(),
110 Some(format!("Income for {}", period)),
111 &removed,
112 )?;
113
114 Ok(true)
115 } else {
116 Ok(false)
117 }
118 }
119
120 pub fn get_all_expectations(&self) -> EnvelopeResult<Vec<IncomeExpectation>> {
122 self.storage.income.get_all()
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use crate::config::paths::EnvelopePaths;
130 use tempfile::TempDir;
131
132 fn create_test_storage() -> (TempDir, Storage) {
133 let temp_dir = TempDir::new().unwrap();
134 let paths = EnvelopePaths::with_base_dir(temp_dir.path().to_path_buf());
135 let mut storage = Storage::new(paths).unwrap();
136 storage.load_all().unwrap();
137 (temp_dir, storage)
138 }
139
140 #[test]
141 fn test_set_expected_income() {
142 let (_temp_dir, storage) = create_test_storage();
143 let service = IncomeService::new(&storage);
144 let period = BudgetPeriod::monthly(2025, 1);
145
146 let expectation = service
147 .set_expected_income(&period, Money::from_cents(500000), None)
148 .unwrap();
149
150 assert_eq!(expectation.expected_amount.cents(), 500000);
151 assert_eq!(expectation.period, period);
152 }
153
154 #[test]
155 fn test_update_expected_income() {
156 let (_temp_dir, storage) = create_test_storage();
157 let service = IncomeService::new(&storage);
158 let period = BudgetPeriod::monthly(2025, 1);
159
160 service
162 .set_expected_income(&period, Money::from_cents(500000), None)
163 .unwrap();
164
165 let updated = service
167 .set_expected_income(
168 &period,
169 Money::from_cents(600000),
170 Some("Updated".to_string()),
171 )
172 .unwrap();
173
174 assert_eq!(updated.expected_amount.cents(), 600000);
175 assert_eq!(updated.notes, "Updated");
176 }
177
178 #[test]
179 fn test_get_expected_income() {
180 let (_temp_dir, storage) = create_test_storage();
181 let service = IncomeService::new(&storage);
182 let period = BudgetPeriod::monthly(2025, 1);
183
184 assert!(service.get_expected_income(&period).is_none());
186
187 service
189 .set_expected_income(&period, Money::from_cents(500000), None)
190 .unwrap();
191
192 let income = service.get_expected_income(&period).unwrap();
194 assert_eq!(income.cents(), 500000);
195 }
196
197 #[test]
198 fn test_delete_expected_income() {
199 let (_temp_dir, storage) = create_test_storage();
200 let service = IncomeService::new(&storage);
201 let period = BudgetPeriod::monthly(2025, 1);
202
203 service
205 .set_expected_income(&period, Money::from_cents(500000), None)
206 .unwrap();
207
208 let deleted = service.delete_expected_income(&period).unwrap();
209 assert!(deleted);
210
211 assert!(service.get_expected_income(&period).is_none());
213
214 let deleted_again = service.delete_expected_income(&period).unwrap();
216 assert!(!deleted_again);
217 }
218
219 #[test]
220 fn test_negative_amount_rejected() {
221 let (_temp_dir, storage) = create_test_storage();
222 let service = IncomeService::new(&storage);
223 let period = BudgetPeriod::monthly(2025, 1);
224
225 let result = service.set_expected_income(&period, Money::from_cents(-100), None);
226 assert!(result.is_err());
227 }
228}