1use crate::audit::EntityType;
7use crate::error::{EnvelopeError, EnvelopeResult};
8use crate::models::{CategoryId, Payee, PayeeId};
9use crate::storage::Storage;
10
11pub struct PayeeService<'a> {
13 storage: &'a Storage,
14}
15
16impl<'a> PayeeService<'a> {
17 pub fn new(storage: &'a Storage) -> Self {
19 Self { storage }
20 }
21
22 pub fn create(&self, name: &str) -> EnvelopeResult<Payee> {
24 let name = name.trim();
25 if name.is_empty() {
26 return Err(EnvelopeError::Validation(
27 "Payee name cannot be empty".into(),
28 ));
29 }
30
31 if self.storage.payees.get_by_name(name)?.is_some() {
33 return Err(EnvelopeError::Duplicate {
34 entity_type: "Payee",
35 identifier: name.to_string(),
36 });
37 }
38
39 let mut payee = Payee::new(name);
40 payee.manual = true;
41
42 payee
44 .validate()
45 .map_err(|e| EnvelopeError::Validation(e.to_string()))?;
46
47 self.storage.payees.upsert(payee.clone())?;
49 self.storage.payees.save()?;
50
51 self.storage.log_create(
53 EntityType::Payee,
54 payee.id.to_string(),
55 Some(payee.name.clone()),
56 &payee,
57 )?;
58
59 Ok(payee)
60 }
61
62 pub fn create_with_category(
64 &self,
65 name: &str,
66 category_id: CategoryId,
67 ) -> EnvelopeResult<Payee> {
68 let name = name.trim();
69 if name.is_empty() {
70 return Err(EnvelopeError::Validation(
71 "Payee name cannot be empty".into(),
72 ));
73 }
74
75 self.storage
77 .categories
78 .get_category(category_id)?
79 .ok_or_else(|| EnvelopeError::category_not_found(category_id.to_string()))?;
80
81 if self.storage.payees.get_by_name(name)?.is_some() {
83 return Err(EnvelopeError::Duplicate {
84 entity_type: "Payee",
85 identifier: name.to_string(),
86 });
87 }
88
89 let payee = Payee::with_default_category(name, category_id);
90
91 payee
93 .validate()
94 .map_err(|e| EnvelopeError::Validation(e.to_string()))?;
95
96 self.storage.payees.upsert(payee.clone())?;
98 self.storage.payees.save()?;
99
100 self.storage.log_create(
102 EntityType::Payee,
103 payee.id.to_string(),
104 Some(payee.name.clone()),
105 &payee,
106 )?;
107
108 Ok(payee)
109 }
110
111 pub fn get(&self, id: PayeeId) -> EnvelopeResult<Option<Payee>> {
113 self.storage.payees.get(id)
114 }
115
116 pub fn get_by_name(&self, name: &str) -> EnvelopeResult<Option<Payee>> {
118 self.storage.payees.get_by_name(name)
119 }
120
121 pub fn find(&self, identifier: &str) -> EnvelopeResult<Option<Payee>> {
123 if let Some(payee) = self.storage.payees.get_by_name(identifier)? {
125 return Ok(Some(payee));
126 }
127
128 if let Ok(id) = identifier.parse::<PayeeId>() {
130 return self.storage.payees.get(id);
131 }
132
133 Ok(None)
134 }
135
136 pub fn get_or_create(&self, name: &str) -> EnvelopeResult<Payee> {
138 self.storage.payees.get_or_create(name)
139 }
140
141 pub fn list(&self) -> EnvelopeResult<Vec<Payee>> {
143 self.storage.payees.get_all()
144 }
145
146 pub fn search(&self, query: &str, limit: usize) -> EnvelopeResult<Vec<Payee>> {
148 self.storage.payees.search(query, limit)
149 }
150
151 pub fn suggest(&self, partial: &str) -> EnvelopeResult<Vec<Payee>> {
153 self.storage.payees.search(partial, 10)
154 }
155
156 pub fn get_suggested_category(&self, payee_name: &str) -> EnvelopeResult<Option<CategoryId>> {
158 if let Some(payee) = self.storage.payees.get_by_name(payee_name)? {
159 Ok(payee.suggested_category())
160 } else {
161 Ok(None)
162 }
163 }
164
165 pub fn set_default_category(
167 &self,
168 id: PayeeId,
169 category_id: CategoryId,
170 ) -> EnvelopeResult<Payee> {
171 let mut payee = self
172 .storage
173 .payees
174 .get(id)?
175 .ok_or_else(|| EnvelopeError::payee_not_found(id.to_string()))?;
176
177 self.storage
179 .categories
180 .get_category(category_id)?
181 .ok_or_else(|| EnvelopeError::category_not_found(category_id.to_string()))?;
182
183 let before = payee.clone();
184 payee.set_default_category(category_id);
185
186 self.storage.payees.upsert(payee.clone())?;
188 self.storage.payees.save()?;
189
190 self.storage.log_update(
192 EntityType::Payee,
193 payee.id.to_string(),
194 Some(payee.name.clone()),
195 &before,
196 &payee,
197 Some(format!(
198 "default_category: {:?} -> {:?}",
199 before.default_category_id, payee.default_category_id
200 )),
201 )?;
202
203 Ok(payee)
204 }
205
206 pub fn clear_default_category(&self, id: PayeeId) -> EnvelopeResult<Payee> {
208 let mut payee = self
209 .storage
210 .payees
211 .get(id)?
212 .ok_or_else(|| EnvelopeError::payee_not_found(id.to_string()))?;
213
214 let before = payee.clone();
215 payee.clear_default_category();
216
217 self.storage.payees.upsert(payee.clone())?;
219 self.storage.payees.save()?;
220
221 self.storage.log_update(
223 EntityType::Payee,
224 payee.id.to_string(),
225 Some(payee.name.clone()),
226 &before,
227 &payee,
228 Some(format!(
229 "default_category: {:?} -> None",
230 before.default_category_id
231 )),
232 )?;
233
234 Ok(payee)
235 }
236
237 pub fn record_category_usage(
239 &self,
240 payee_id: PayeeId,
241 category_id: CategoryId,
242 ) -> EnvelopeResult<()> {
243 if let Some(mut payee) = self.storage.payees.get(payee_id)? {
244 payee.record_category_usage(category_id);
245 self.storage.payees.upsert(payee)?;
246 self.storage.payees.save()?;
247 }
248 Ok(())
249 }
250
251 pub fn delete(&self, id: PayeeId) -> EnvelopeResult<Payee> {
253 let payee = self
254 .storage
255 .payees
256 .get(id)?
257 .ok_or_else(|| EnvelopeError::payee_not_found(id.to_string()))?;
258
259 self.storage.payees.delete(id)?;
260 self.storage.payees.save()?;
261
262 self.storage.log_delete(
264 EntityType::Payee,
265 id.to_string(),
266 Some(payee.name.clone()),
267 &payee,
268 )?;
269
270 Ok(payee)
271 }
272
273 pub fn rename(&self, id: PayeeId, new_name: &str) -> EnvelopeResult<Payee> {
275 let new_name = new_name.trim();
276 if new_name.is_empty() {
277 return Err(EnvelopeError::Validation(
278 "Payee name cannot be empty".into(),
279 ));
280 }
281
282 let mut payee = self
283 .storage
284 .payees
285 .get(id)?
286 .ok_or_else(|| EnvelopeError::payee_not_found(id.to_string()))?;
287
288 if let Some(existing) = self.storage.payees.get_by_name(new_name)? {
290 if existing.id != id {
291 return Err(EnvelopeError::Duplicate {
292 entity_type: "Payee",
293 identifier: new_name.to_string(),
294 });
295 }
296 }
297
298 let before = payee.clone();
299 payee.name = new_name.to_string();
300 payee.updated_at = chrono::Utc::now();
301
302 payee
304 .validate()
305 .map_err(|e| EnvelopeError::Validation(e.to_string()))?;
306
307 self.storage.payees.upsert(payee.clone())?;
309 self.storage.payees.save()?;
310
311 self.storage.log_update(
313 EntityType::Payee,
314 payee.id.to_string(),
315 Some(payee.name.clone()),
316 &before,
317 &payee,
318 Some(format!("name: '{}' -> '{}'", before.name, payee.name)),
319 )?;
320
321 Ok(payee)
322 }
323
324 pub fn count(&self) -> EnvelopeResult<usize> {
326 self.storage.payees.count()
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use crate::config::paths::EnvelopePaths;
334 use crate::models::{Category, CategoryGroup};
335 use tempfile::TempDir;
336
337 fn create_test_storage() -> (TempDir, Storage) {
338 let temp_dir = TempDir::new().unwrap();
339 let paths = EnvelopePaths::with_base_dir(temp_dir.path().to_path_buf());
340 let mut storage = Storage::new(paths).unwrap();
341 storage.load_all().unwrap();
342 (temp_dir, storage)
343 }
344
345 fn setup_test_category(storage: &Storage) -> CategoryId {
346 let group = CategoryGroup::new("Test Group");
347 storage.categories.upsert_group(group.clone()).unwrap();
348
349 let category = Category::new("Groceries", group.id);
350 let category_id = category.id;
351 storage.categories.upsert_category(category).unwrap();
352 storage.categories.save().unwrap();
353
354 category_id
355 }
356
357 #[test]
358 fn test_create_payee() {
359 let (_temp_dir, storage) = create_test_storage();
360 let service = PayeeService::new(&storage);
361
362 let payee = service.create("Test Store").unwrap();
363 assert_eq!(payee.name, "Test Store");
364 assert!(payee.manual);
365 }
366
367 #[test]
368 fn test_create_with_category() {
369 let (_temp_dir, storage) = create_test_storage();
370 let category_id = setup_test_category(&storage);
371 let service = PayeeService::new(&storage);
372
373 let payee = service
374 .create_with_category("Grocery Store", category_id)
375 .unwrap();
376
377 assert_eq!(payee.default_category_id, Some(category_id));
378 assert!(payee.manual);
379 }
380
381 #[test]
382 fn test_duplicate_payee() {
383 let (_temp_dir, storage) = create_test_storage();
384 let service = PayeeService::new(&storage);
385
386 service.create("Test Store").unwrap();
387 let result = service.create("test store"); assert!(matches!(result, Err(EnvelopeError::Duplicate { .. })));
389 }
390
391 #[test]
392 fn test_search_payees() {
393 let (_temp_dir, storage) = create_test_storage();
394 let service = PayeeService::new(&storage);
395
396 service.create("Grocery Store").unwrap();
397 service.create("Gas Station").unwrap();
398 service.create("Restaurant").unwrap();
399
400 let results = service.search("groc", 10).unwrap();
401 assert!(!results.is_empty());
402 assert_eq!(results[0].name, "Grocery Store");
403 }
404
405 #[test]
406 fn test_category_learning() {
407 let (_temp_dir, storage) = create_test_storage();
408 let category_id = setup_test_category(&storage);
409 let service = PayeeService::new(&storage);
410
411 let payee = service.create("Learning Store").unwrap();
412
413 service
415 .record_category_usage(payee.id, category_id)
416 .unwrap();
417 service
418 .record_category_usage(payee.id, category_id)
419 .unwrap();
420
421 let suggested = service.get_suggested_category("Learning Store").unwrap();
423 assert_eq!(suggested, Some(category_id));
424 }
425
426 #[test]
427 fn test_set_default_category() {
428 let (_temp_dir, storage) = create_test_storage();
429 let category_id = setup_test_category(&storage);
430 let service = PayeeService::new(&storage);
431
432 let payee = service.create("Test Payee").unwrap();
433 assert!(payee.default_category_id.is_none());
434
435 let updated = service.set_default_category(payee.id, category_id).unwrap();
436 assert_eq!(updated.default_category_id, Some(category_id));
437 assert!(updated.manual);
438 }
439
440 #[test]
441 fn test_delete_payee() {
442 let (_temp_dir, storage) = create_test_storage();
443 let service = PayeeService::new(&storage);
444
445 let payee = service.create("To Delete").unwrap();
446 assert_eq!(service.count().unwrap(), 1);
447
448 service.delete(payee.id).unwrap();
449 assert_eq!(service.count().unwrap(), 0);
450 }
451
452 #[test]
453 fn test_rename_payee() {
454 let (_temp_dir, storage) = create_test_storage();
455 let service = PayeeService::new(&storage);
456
457 let payee = service.create("Old Name").unwrap();
458 let renamed = service.rename(payee.id, "New Name").unwrap();
459
460 assert_eq!(renamed.name, "New Name");
461 assert!(service.get_by_name("Old Name").unwrap().is_none());
462 assert!(service.get_by_name("New Name").unwrap().is_some());
463 }
464}