envelope_cli/storage/
income.rs1use std::collections::HashMap;
6use std::path::PathBuf;
7use std::sync::RwLock;
8
9use crate::error::EnvelopeError;
10use crate::models::{BudgetPeriod, IncomeExpectation, IncomeId};
11
12use super::file_io::{read_json, write_json_atomic};
13
14#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
15struct IncomeData {
16 #[serde(default)]
17 expectations: Vec<IncomeExpectation>,
18}
19
20pub struct IncomeRepository {
22 path: PathBuf,
23 expectations: RwLock<HashMap<BudgetPeriod, IncomeExpectation>>,
24}
25
26impl IncomeRepository {
27 pub fn new(path: PathBuf) -> Self {
29 Self {
30 path,
31 expectations: RwLock::new(HashMap::new()),
32 }
33 }
34
35 pub fn load(&self) -> Result<(), EnvelopeError> {
37 let file_data: IncomeData = read_json(&self.path)?;
38
39 let mut expectations = self
40 .expectations
41 .write()
42 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire write lock: {}", e)))?;
43
44 expectations.clear();
45 for expectation in file_data.expectations {
46 expectations.insert(expectation.period.clone(), expectation);
47 }
48
49 Ok(())
50 }
51
52 pub fn save(&self) -> Result<(), EnvelopeError> {
54 let expectations = self
55 .expectations
56 .read()
57 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
58
59 let mut list: Vec<_> = expectations.values().cloned().collect();
60 list.sort_by(|a, b| a.created_at.cmp(&b.created_at));
61
62 let file_data = IncomeData { expectations: list };
63
64 write_json_atomic(&self.path, &file_data)
65 }
66
67 pub fn get_for_period(&self, period: &BudgetPeriod) -> Option<IncomeExpectation> {
69 let expectations = self.expectations.read().ok()?;
70 expectations.get(period).cloned()
71 }
72
73 pub fn get(&self, id: IncomeId) -> Result<Option<IncomeExpectation>, EnvelopeError> {
75 let expectations = self
76 .expectations
77 .read()
78 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
79
80 Ok(expectations.values().find(|e| e.id == id).cloned())
81 }
82
83 pub fn upsert(&self, expectation: IncomeExpectation) -> Result<(), EnvelopeError> {
85 let mut expectations = self
86 .expectations
87 .write()
88 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire write lock: {}", e)))?;
89
90 expectations.insert(expectation.period.clone(), expectation);
91 Ok(())
92 }
93
94 pub fn delete_for_period(&self, period: &BudgetPeriod) -> Option<IncomeExpectation> {
96 let mut expectations = self.expectations.write().ok()?;
97 expectations.remove(period)
98 }
99
100 pub fn get_all(&self) -> Result<Vec<IncomeExpectation>, EnvelopeError> {
102 let expectations = self
103 .expectations
104 .read()
105 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
106
107 let mut list: Vec<_> = expectations.values().cloned().collect();
108 list.sort_by(|a, b| a.period.cmp(&b.period));
109 Ok(list)
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use crate::models::Money;
117 use tempfile::TempDir;
118
119 #[test]
120 fn test_upsert_and_get() {
121 let temp_dir = TempDir::new().unwrap();
122 let path = temp_dir.path().join("income.json");
123 let repo = IncomeRepository::new(path);
124
125 let period = BudgetPeriod::monthly(2025, 1);
126 let expectation = IncomeExpectation::new(period.clone(), Money::from_cents(500000));
127
128 repo.upsert(expectation).unwrap();
129
130 let retrieved = repo.get_for_period(&period).unwrap();
131 assert_eq!(retrieved.expected_amount.cents(), 500000);
132 }
133
134 #[test]
135 fn test_save_and_load() {
136 let temp_dir = TempDir::new().unwrap();
137 let path = temp_dir.path().join("income.json");
138
139 {
141 let repo = IncomeRepository::new(path.clone());
142 let period = BudgetPeriod::monthly(2025, 1);
143 let expectation = IncomeExpectation::new(period, Money::from_cents(500000));
144 repo.upsert(expectation).unwrap();
145 repo.save().unwrap();
146 }
147
148 {
150 let repo = IncomeRepository::new(path);
151 repo.load().unwrap();
152 let period = BudgetPeriod::monthly(2025, 1);
153 let retrieved = repo.get_for_period(&period).unwrap();
154 assert_eq!(retrieved.expected_amount.cents(), 500000);
155 }
156 }
157
158 #[test]
159 fn test_delete() {
160 let temp_dir = TempDir::new().unwrap();
161 let path = temp_dir.path().join("income.json");
162 let repo = IncomeRepository::new(path);
163
164 let period = BudgetPeriod::monthly(2025, 1);
165 let expectation = IncomeExpectation::new(period.clone(), Money::from_cents(500000));
166
167 repo.upsert(expectation).unwrap();
168 assert!(repo.get_for_period(&period).is_some());
169
170 repo.delete_for_period(&period);
171 assert!(repo.get_for_period(&period).is_none());
172 }
173
174 #[test]
175 fn test_get_all() {
176 let temp_dir = TempDir::new().unwrap();
177 let path = temp_dir.path().join("income.json");
178 let repo = IncomeRepository::new(path);
179
180 let period1 = BudgetPeriod::monthly(2025, 1);
181 let period2 = BudgetPeriod::monthly(2025, 2);
182
183 repo.upsert(IncomeExpectation::new(period1, Money::from_cents(500000)))
184 .unwrap();
185 repo.upsert(IncomeExpectation::new(period2, Money::from_cents(550000)))
186 .unwrap();
187
188 let all = repo.get_all().unwrap();
189 assert_eq!(all.len(), 2);
190 }
191}