1use std::collections::HashMap;
6use std::path::PathBuf;
7use std::sync::RwLock;
8
9use crate::error::EnvelopeError;
10use crate::models::{Category, CategoryGroup, CategoryGroupId, CategoryId};
11
12use super::file_io::{read_json, write_json_atomic};
13
14#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
16pub struct CategoryData {
17 pub groups: Vec<CategoryGroup>,
18 pub categories: Vec<Category>,
19}
20
21pub struct CategoryRepository {
23 path: PathBuf,
24 groups: RwLock<HashMap<CategoryGroupId, CategoryGroup>>,
25 categories: RwLock<HashMap<CategoryId, Category>>,
26}
27
28impl CategoryRepository {
29 pub fn new(path: PathBuf) -> Self {
31 Self {
32 path,
33 groups: RwLock::new(HashMap::new()),
34 categories: RwLock::new(HashMap::new()),
35 }
36 }
37
38 pub fn load(&self) -> Result<(), EnvelopeError> {
40 let file_data: CategoryData = read_json(&self.path)?;
41
42 let mut groups = self
43 .groups
44 .write()
45 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire write lock: {}", e)))?;
46 let mut categories = self
47 .categories
48 .write()
49 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire write lock: {}", e)))?;
50
51 groups.clear();
52 categories.clear();
53
54 for group in file_data.groups {
55 groups.insert(group.id, group);
56 }
57
58 for category in file_data.categories {
59 categories.insert(category.id, category);
60 }
61
62 Ok(())
63 }
64
65 pub fn save(&self) -> Result<(), EnvelopeError> {
67 let groups = self
68 .groups
69 .read()
70 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
71 let categories = self
72 .categories
73 .read()
74 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
75
76 let mut group_list: Vec<_> = groups.values().cloned().collect();
77 group_list.sort_by_key(|g| g.sort_order);
78
79 let mut category_list: Vec<_> = categories.values().cloned().collect();
80 category_list.sort_by_key(|c| (c.sort_order, c.name.clone()));
81
82 let file_data = CategoryData {
83 groups: group_list,
84 categories: category_list,
85 };
86
87 write_json_atomic(&self.path, &file_data)
88 }
89
90 pub fn get_group(&self, id: CategoryGroupId) -> Result<Option<CategoryGroup>, EnvelopeError> {
94 let groups = self
95 .groups
96 .read()
97 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
98
99 Ok(groups.get(&id).cloned())
100 }
101
102 pub fn get_all_groups(&self) -> Result<Vec<CategoryGroup>, EnvelopeError> {
104 let groups = self
105 .groups
106 .read()
107 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
108
109 let mut list: Vec<_> = groups.values().cloned().collect();
110 list.sort_by_key(|g| g.sort_order);
111 Ok(list)
112 }
113
114 pub fn get_group_by_name(&self, name: &str) -> Result<Option<CategoryGroup>, EnvelopeError> {
116 let groups = self
117 .groups
118 .read()
119 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
120
121 let name_lower = name.to_lowercase();
122 Ok(groups
123 .values()
124 .find(|g| g.name.to_lowercase() == name_lower)
125 .cloned())
126 }
127
128 pub fn upsert_group(&self, group: CategoryGroup) -> Result<(), EnvelopeError> {
130 let mut groups = self
131 .groups
132 .write()
133 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire write lock: {}", e)))?;
134
135 groups.insert(group.id, group);
136 Ok(())
137 }
138
139 pub fn delete_group(
141 &self,
142 id: CategoryGroupId,
143 delete_categories: bool,
144 ) -> Result<bool, EnvelopeError> {
145 let mut groups = self
146 .groups
147 .write()
148 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire write lock: {}", e)))?;
149
150 if delete_categories {
151 let mut categories = self.categories.write().map_err(|e| {
152 EnvelopeError::Storage(format!("Failed to acquire write lock: {}", e))
153 })?;
154 categories.retain(|_, c| c.group_id != id);
155 }
156
157 Ok(groups.remove(&id).is_some())
158 }
159
160 pub fn get_category(&self, id: CategoryId) -> Result<Option<Category>, EnvelopeError> {
164 let categories = self
165 .categories
166 .read()
167 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
168
169 Ok(categories.get(&id).cloned())
170 }
171
172 pub fn get_all_categories(&self) -> Result<Vec<Category>, EnvelopeError> {
174 let categories = self
175 .categories
176 .read()
177 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
178
179 let mut list: Vec<_> = categories.values().cloned().collect();
180 list.sort_by_key(|c| (c.sort_order, c.name.clone()));
181 Ok(list)
182 }
183
184 pub fn get_categories_in_group(
186 &self,
187 group_id: CategoryGroupId,
188 ) -> Result<Vec<Category>, EnvelopeError> {
189 let categories = self
190 .categories
191 .read()
192 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
193
194 let mut list: Vec<_> = categories
195 .values()
196 .filter(|c| c.group_id == group_id)
197 .cloned()
198 .collect();
199 list.sort_by_key(|c| (c.sort_order, c.name.clone()));
200 Ok(list)
201 }
202
203 pub fn get_category_by_name(&self, name: &str) -> Result<Option<Category>, EnvelopeError> {
205 let categories = self
206 .categories
207 .read()
208 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
209
210 let name_lower = name.to_lowercase();
211 Ok(categories
212 .values()
213 .find(|c| c.name.to_lowercase() == name_lower)
214 .cloned())
215 }
216
217 pub fn upsert_category(&self, category: Category) -> Result<(), EnvelopeError> {
219 let mut categories = self
220 .categories
221 .write()
222 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire write lock: {}", e)))?;
223
224 categories.insert(category.id, category);
225 Ok(())
226 }
227
228 pub fn delete_category(&self, id: CategoryId) -> Result<bool, EnvelopeError> {
230 let mut categories = self
231 .categories
232 .write()
233 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire write lock: {}", e)))?;
234
235 Ok(categories.remove(&id).is_some())
236 }
237
238 pub fn group_count(&self) -> Result<usize, EnvelopeError> {
240 let groups = self
241 .groups
242 .read()
243 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
244 Ok(groups.len())
245 }
246
247 pub fn category_count(&self) -> Result<usize, EnvelopeError> {
249 let categories = self
250 .categories
251 .read()
252 .map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
253 Ok(categories.len())
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260 use tempfile::TempDir;
261
262 fn create_test_repo() -> (TempDir, CategoryRepository) {
263 let temp_dir = TempDir::new().unwrap();
264 let path = temp_dir.path().join("budget.json");
265 let repo = CategoryRepository::new(path);
266 (temp_dir, repo)
267 }
268
269 #[test]
270 fn test_empty_load() {
271 let (_temp_dir, repo) = create_test_repo();
272 repo.load().unwrap();
273 assert_eq!(repo.group_count().unwrap(), 0);
274 assert_eq!(repo.category_count().unwrap(), 0);
275 }
276
277 #[test]
278 fn test_group_operations() {
279 let (_temp_dir, repo) = create_test_repo();
280 repo.load().unwrap();
281
282 let group = CategoryGroup::new("Bills");
283 let id = group.id;
284
285 repo.upsert_group(group).unwrap();
286 assert_eq!(repo.group_count().unwrap(), 1);
287
288 let retrieved = repo.get_group(id).unwrap().unwrap();
289 assert_eq!(retrieved.name, "Bills");
290
291 repo.delete_group(id, false).unwrap();
292 assert_eq!(repo.group_count().unwrap(), 0);
293 }
294
295 #[test]
296 fn test_category_operations() {
297 let (_temp_dir, repo) = create_test_repo();
298 repo.load().unwrap();
299
300 let group = CategoryGroup::new("Bills");
301 repo.upsert_group(group.clone()).unwrap();
302
303 let category = Category::new("Rent", group.id);
304 let cat_id = category.id;
305
306 repo.upsert_category(category).unwrap();
307 assert_eq!(repo.category_count().unwrap(), 1);
308
309 let retrieved = repo.get_category(cat_id).unwrap().unwrap();
310 assert_eq!(retrieved.name, "Rent");
311
312 let in_group = repo.get_categories_in_group(group.id).unwrap();
313 assert_eq!(in_group.len(), 1);
314 }
315
316 #[test]
317 fn test_save_and_reload() {
318 let (temp_dir, repo) = create_test_repo();
319 repo.load().unwrap();
320
321 let group = CategoryGroup::new("Bills");
322 let category = Category::new("Rent", group.id);
323 let cat_id = category.id;
324
325 repo.upsert_group(group).unwrap();
326 repo.upsert_category(category).unwrap();
327 repo.save().unwrap();
328
329 let path = temp_dir.path().join("budget.json");
331 let repo2 = CategoryRepository::new(path);
332 repo2.load().unwrap();
333
334 assert_eq!(repo2.group_count().unwrap(), 1);
335 assert_eq!(repo2.category_count().unwrap(), 1);
336
337 let retrieved = repo2.get_category(cat_id).unwrap().unwrap();
338 assert_eq!(retrieved.name, "Rent");
339 }
340
341 #[test]
342 fn test_get_by_name() {
343 let (_temp_dir, repo) = create_test_repo();
344 repo.load().unwrap();
345
346 let group = CategoryGroup::new("My Bills");
347 repo.upsert_group(group.clone()).unwrap();
348
349 let category = Category::new("Monthly Rent", group.id);
350 repo.upsert_category(category).unwrap();
351
352 let found_group = repo.get_group_by_name("my bills").unwrap();
354 assert!(found_group.is_some());
355
356 let found_cat = repo.get_category_by_name("MONTHLY RENT").unwrap();
357 assert!(found_cat.is_some());
358 }
359
360 #[test]
361 fn test_delete_group_with_categories() {
362 let (_temp_dir, repo) = create_test_repo();
363 repo.load().unwrap();
364
365 let group = CategoryGroup::new("Bills");
366 let group_id = group.id;
367 repo.upsert_group(group.clone()).unwrap();
368
369 let cat1 = Category::new("Rent", group.id);
370 let cat2 = Category::new("Utilities", group.id);
371 repo.upsert_category(cat1).unwrap();
372 repo.upsert_category(cat2).unwrap();
373
374 assert_eq!(repo.category_count().unwrap(), 2);
375
376 repo.delete_group(group_id, true).unwrap();
377
378 assert_eq!(repo.group_count().unwrap(), 0);
379 assert_eq!(repo.category_count().unwrap(), 0);
380 }
381}