envelope_cli/storage/
accounts.rs1use std::collections::HashMap;
6use std::path::PathBuf;
7use std::sync::RwLock;
8
9use crate::error::EnvelopeError;
10use crate::models::{Account, AccountId};
11
12use super::file_io::{read_json, write_json_atomic};
13
14#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
16struct AccountData {
17 accounts: Vec<Account>,
18}
19
20pub struct AccountRepository {
22 path: PathBuf,
23 data: RwLock<HashMap<AccountId, Account>>,
24}
25
26impl AccountRepository {
27 pub fn new(path: PathBuf) -> Self {
29 Self {
30 path,
31 data: RwLock::new(HashMap::new()),
32 }
33 }
34
35 pub fn load(&self) -> Result<(), EnvelopeError> {
37 let file_data: AccountData = read_json(&self.path)?;
38
39 let mut data = self
40 .data
41 .write()
42 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire write lock: {}", e)))?;
43
44 data.clear();
45 for account in file_data.accounts {
46 data.insert(account.id, account);
47 }
48
49 Ok(())
50 }
51
52 pub fn save(&self) -> Result<(), EnvelopeError> {
54 let data = self
55 .data
56 .read()
57 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
58
59 let file_data = AccountData {
60 accounts: data.values().cloned().collect(),
61 };
62
63 write_json_atomic(&self.path, &file_data)
64 }
65
66 pub fn get(&self, id: AccountId) -> Result<Option<Account>, EnvelopeError> {
68 let data = self
69 .data
70 .read()
71 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
72
73 Ok(data.get(&id).cloned())
74 }
75
76 pub fn get_all(&self) -> Result<Vec<Account>, EnvelopeError> {
78 let data = self
79 .data
80 .read()
81 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
82
83 let mut accounts: Vec<_> = data.values().cloned().collect();
84 accounts.sort_by(|a, b| a.sort_order.cmp(&b.sort_order).then(a.name.cmp(&b.name)));
85 Ok(accounts)
86 }
87
88 pub fn get_active(&self) -> Result<Vec<Account>, EnvelopeError> {
90 let all = self.get_all()?;
91 Ok(all.into_iter().filter(|a| !a.archived).collect())
92 }
93
94 pub fn get_by_name(&self, name: &str) -> Result<Option<Account>, EnvelopeError> {
96 let data = self
97 .data
98 .read()
99 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
100
101 let name_lower = name.to_lowercase();
102 Ok(data
103 .values()
104 .find(|a| a.name.to_lowercase() == name_lower)
105 .cloned())
106 }
107
108 pub fn upsert(&self, account: Account) -> Result<(), EnvelopeError> {
110 let mut data = self
111 .data
112 .write()
113 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire write lock: {}", e)))?;
114
115 data.insert(account.id, account);
116 Ok(())
117 }
118
119 pub fn delete(&self, id: AccountId) -> Result<bool, EnvelopeError> {
121 let mut data = self
122 .data
123 .write()
124 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire write lock: {}", e)))?;
125
126 Ok(data.remove(&id).is_some())
127 }
128
129 pub fn exists(&self, id: AccountId) -> Result<bool, EnvelopeError> {
131 let data = self
132 .data
133 .read()
134 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
135
136 Ok(data.contains_key(&id))
137 }
138
139 pub fn name_exists(
141 &self,
142 name: &str,
143 exclude_id: Option<AccountId>,
144 ) -> Result<bool, EnvelopeError> {
145 let data = self
146 .data
147 .read()
148 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
149
150 let name_lower = name.to_lowercase();
151 Ok(data
152 .values()
153 .any(|a| a.name.to_lowercase() == name_lower && Some(a.id) != exclude_id))
154 }
155
156 pub fn count(&self) -> Result<usize, EnvelopeError> {
158 let data = self
159 .data
160 .read()
161 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
162
163 Ok(data.len())
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use crate::models::AccountType;
171 use tempfile::TempDir;
172
173 fn create_test_repo() -> (TempDir, AccountRepository) {
174 let temp_dir = TempDir::new().unwrap();
175 let path = temp_dir.path().join("accounts.json");
176 let repo = AccountRepository::new(path);
177 (temp_dir, repo)
178 }
179
180 #[test]
181 fn test_empty_load() {
182 let (_temp_dir, repo) = create_test_repo();
183 repo.load().unwrap();
184 assert_eq!(repo.count().unwrap(), 0);
185 }
186
187 #[test]
188 fn test_upsert_and_get() {
189 let (_temp_dir, repo) = create_test_repo();
190 repo.load().unwrap();
191
192 let account = Account::new("Checking", AccountType::Checking);
193 let id = account.id;
194
195 repo.upsert(account.clone()).unwrap();
196
197 let retrieved = repo.get(id).unwrap().unwrap();
198 assert_eq!(retrieved.name, "Checking");
199 }
200
201 #[test]
202 fn test_save_and_reload() {
203 let (temp_dir, repo) = create_test_repo();
204
205 let account = Account::new("Savings", AccountType::Savings);
206 let id = account.id;
207
208 repo.load().unwrap();
209 repo.upsert(account).unwrap();
210 repo.save().unwrap();
211
212 let path = temp_dir.path().join("accounts.json");
214 let repo2 = AccountRepository::new(path);
215 repo2.load().unwrap();
216
217 let retrieved = repo2.get(id).unwrap().unwrap();
218 assert_eq!(retrieved.name, "Savings");
219 }
220
221 #[test]
222 fn test_get_by_name() {
223 let (_temp_dir, repo) = create_test_repo();
224 repo.load().unwrap();
225
226 let account = Account::new("My Checking", AccountType::Checking);
227 repo.upsert(account).unwrap();
228
229 let found = repo.get_by_name("my checking").unwrap();
231 assert!(found.is_some());
232 assert_eq!(found.unwrap().name, "My Checking");
233
234 let not_found = repo.get_by_name("other").unwrap();
235 assert!(not_found.is_none());
236 }
237
238 #[test]
239 fn test_delete() {
240 let (_temp_dir, repo) = create_test_repo();
241 repo.load().unwrap();
242
243 let account = Account::new("Test", AccountType::Checking);
244 let id = account.id;
245
246 repo.upsert(account).unwrap();
247 assert!(repo.exists(id).unwrap());
248
249 repo.delete(id).unwrap();
250 assert!(!repo.exists(id).unwrap());
251 }
252
253 #[test]
254 fn test_get_active_filters_archived() {
255 let (_temp_dir, repo) = create_test_repo();
256 repo.load().unwrap();
257
258 let account1 = Account::new("Active", AccountType::Checking);
259 let mut account2 = Account::new("Archived", AccountType::Savings);
260 account2.archive();
261
262 repo.upsert(account1).unwrap();
263 repo.upsert(account2).unwrap();
264
265 let all = repo.get_all().unwrap();
266 assert_eq!(all.len(), 2);
267
268 let active = repo.get_active().unwrap();
269 assert_eq!(active.len(), 1);
270 assert_eq!(active[0].name, "Active");
271 }
272
273 #[test]
274 fn test_name_exists() {
275 let (_temp_dir, repo) = create_test_repo();
276 repo.load().unwrap();
277
278 let account = Account::new("Test Account", AccountType::Checking);
279 let id = account.id;
280 repo.upsert(account).unwrap();
281
282 assert!(repo.name_exists("test account", None).unwrap());
284
285 assert!(!repo.name_exists("test account", Some(id)).unwrap());
287
288 assert!(!repo.name_exists("other", None).unwrap());
290 }
291}