1use crate::application::dto::{
2 AddTrackingNumberDto, BulkCreateRemindersDto, BulkCreateRemindersResponseDto,
3 CancelReminderDto, CreatePaymentReminderDto, EscalateReminderDto, MarkReminderSentDto,
4 OverdueExpenseDto, PaymentRecoveryStatsDto, PaymentReminderResponseDto, ReminderLevelCountDto,
5 ReminderStatusCountDto,
6};
7use crate::application::ports::{ExpenseRepository, OwnerRepository, PaymentReminderRepository};
8use crate::domain::entities::{PaymentReminder, PaymentStatus, ReminderStatus};
9use chrono::{DateTime, Utc};
10use std::sync::Arc;
11use uuid::Uuid;
12
13pub struct PaymentReminderUseCases {
14 reminder_repository: Arc<dyn PaymentReminderRepository>,
15 expense_repository: Arc<dyn ExpenseRepository>,
16 owner_repository: Arc<dyn OwnerRepository>,
17}
18
19impl PaymentReminderUseCases {
20 pub fn new(
21 reminder_repository: Arc<dyn PaymentReminderRepository>,
22 expense_repository: Arc<dyn ExpenseRepository>,
23 owner_repository: Arc<dyn OwnerRepository>,
24 ) -> Self {
25 Self {
26 reminder_repository,
27 expense_repository,
28 owner_repository,
29 }
30 }
31
32 async fn enrich_with_owner_info(
34 &self,
35 mut dto: PaymentReminderResponseDto,
36 ) -> Result<PaymentReminderResponseDto, String> {
37 let owner_id =
38 Uuid::parse_str(&dto.owner_id).map_err(|_| "Invalid owner_id format".to_string())?;
39
40 if let Ok(Some(owner)) = self.owner_repository.find_by_id(owner_id).await {
41 dto.owner_name = Some(owner.full_name());
42 dto.owner_email = Some(owner.email.clone());
43 }
44
45 Ok(dto)
46 }
47
48 pub async fn create_reminder(
50 &self,
51 dto: CreatePaymentReminderDto,
52 ) -> Result<PaymentReminderResponseDto, String> {
53 let organization_id = Uuid::parse_str(&dto.organization_id)
54 .map_err(|_| "Invalid organization_id format".to_string())?;
55 let expense_id = Uuid::parse_str(&dto.expense_id)
56 .map_err(|_| "Invalid expense_id format".to_string())?;
57 let owner_id =
58 Uuid::parse_str(&dto.owner_id).map_err(|_| "Invalid owner_id format".to_string())?;
59
60 let due_date = DateTime::parse_from_rfc3339(&dto.due_date)
61 .map_err(|_| "Invalid date format".to_string())?
62 .with_timezone(&Utc);
63
64 let expense = self
66 .expense_repository
67 .find_by_id(expense_id)
68 .await?
69 .ok_or_else(|| "Expense not found".to_string())?;
70
71 if expense.payment_status == PaymentStatus::Paid {
72 return Err("Cannot create reminder for paid expense".to_string());
73 }
74
75 let existing_reminders = self.reminder_repository.find_by_expense(expense_id).await?;
77
78 if existing_reminders.iter().any(|r| {
79 r.owner_id == owner_id
80 && r.level == dto.level
81 && r.status != ReminderStatus::Cancelled
82 && r.status != ReminderStatus::Paid
83 }) {
84 return Err(format!(
85 "Active reminder already exists for this expense at {:?} level",
86 dto.level
87 ));
88 }
89
90 let reminder = PaymentReminder::new(
91 organization_id,
92 expense_id,
93 owner_id,
94 dto.level,
95 dto.amount_owed,
96 due_date,
97 dto.days_overdue,
98 )?;
99
100 let created = self.reminder_repository.create(&reminder).await?;
101 Ok(created.into())
102 }
103
104 pub async fn get_reminder(
106 &self,
107 id: Uuid,
108 ) -> Result<Option<PaymentReminderResponseDto>, String> {
109 let reminder = self.reminder_repository.find_by_id(id).await?;
110 Ok(reminder.map(|r| r.into()))
111 }
112
113 pub async fn list_by_expense(
115 &self,
116 expense_id: Uuid,
117 ) -> Result<Vec<PaymentReminderResponseDto>, String> {
118 let reminders = self.reminder_repository.find_by_expense(expense_id).await?;
119 Ok(reminders.into_iter().map(|r| r.into()).collect())
120 }
121
122 pub async fn list_by_owner(
124 &self,
125 owner_id: Uuid,
126 ) -> Result<Vec<PaymentReminderResponseDto>, String> {
127 let reminders = self.reminder_repository.find_by_owner(owner_id).await?;
128 Ok(reminders.into_iter().map(|r| r.into()).collect())
129 }
130
131 pub async fn list_by_organization(
133 &self,
134 organization_id: Uuid,
135 ) -> Result<Vec<PaymentReminderResponseDto>, String> {
136 let reminders = self
137 .reminder_repository
138 .find_by_organization(organization_id)
139 .await?;
140
141 let mut enriched_reminders = Vec::new();
143 for reminder in reminders {
144 let dto: PaymentReminderResponseDto = reminder.into();
145 let enriched = self.enrich_with_owner_info(dto).await?;
146 enriched_reminders.push(enriched);
147 }
148
149 Ok(enriched_reminders)
150 }
151
152 pub async fn list_active_by_owner(
154 &self,
155 owner_id: Uuid,
156 ) -> Result<Vec<PaymentReminderResponseDto>, String> {
157 let reminders = self
158 .reminder_repository
159 .find_active_by_owner(owner_id)
160 .await?;
161 Ok(reminders.into_iter().map(|r| r.into()).collect())
162 }
163
164 pub async fn mark_as_sent(
166 &self,
167 id: Uuid,
168 dto: MarkReminderSentDto,
169 ) -> Result<PaymentReminderResponseDto, String> {
170 let mut reminder = self
171 .reminder_repository
172 .find_by_id(id)
173 .await?
174 .ok_or_else(|| "Reminder not found".to_string())?;
175
176 reminder.mark_as_sent(dto.pdf_path)?;
177
178 let updated = self.reminder_repository.update(&reminder).await?;
179 Ok(updated.into())
180 }
181
182 pub async fn mark_as_opened(&self, id: Uuid) -> Result<PaymentReminderResponseDto, String> {
184 let mut reminder = self
185 .reminder_repository
186 .find_by_id(id)
187 .await?
188 .ok_or_else(|| "Reminder not found".to_string())?;
189
190 reminder.mark_as_opened()?;
191
192 let updated = self.reminder_repository.update(&reminder).await?;
193 Ok(updated.into())
194 }
195
196 pub async fn mark_as_paid(&self, id: Uuid) -> Result<PaymentReminderResponseDto, String> {
198 let mut reminder = self
199 .reminder_repository
200 .find_by_id(id)
201 .await?
202 .ok_or_else(|| "Reminder not found".to_string())?;
203
204 reminder.mark_as_paid()?;
205
206 let updated = self.reminder_repository.update(&reminder).await?;
207 Ok(updated.into())
208 }
209
210 pub async fn cancel_reminder(
212 &self,
213 id: Uuid,
214 dto: CancelReminderDto,
215 ) -> Result<PaymentReminderResponseDto, String> {
216 let mut reminder = self
217 .reminder_repository
218 .find_by_id(id)
219 .await?
220 .ok_or_else(|| "Reminder not found".to_string())?;
221
222 reminder.cancel(dto.reason)?;
223
224 let updated = self.reminder_repository.update(&reminder).await?;
225 Ok(updated.into())
226 }
227
228 pub async fn escalate_reminder(
230 &self,
231 id: Uuid,
232 _dto: EscalateReminderDto,
233 ) -> Result<Option<PaymentReminderResponseDto>, String> {
234 let mut reminder = self
235 .reminder_repository
236 .find_by_id(id)
237 .await?
238 .ok_or_else(|| "Reminder not found".to_string())?;
239
240 let next_level = reminder.escalate()?;
241
242 let updated = self.reminder_repository.update(&reminder).await?;
243
244 if let Some(level) = next_level {
246 let days_overdue = (Utc::now() - reminder.due_date).num_days();
248
249 let next_reminder = PaymentReminder::new(
251 reminder.organization_id,
252 reminder.expense_id,
253 reminder.owner_id,
254 level,
255 reminder.amount_owed,
256 reminder.due_date,
257 days_overdue,
258 )?;
259
260 let created = self.reminder_repository.create(&next_reminder).await?;
261 return Ok(Some(created.into()));
262 }
263
264 Ok(Some(updated.into()))
265 }
266
267 pub async fn add_tracking_number(
269 &self,
270 id: Uuid,
271 dto: AddTrackingNumberDto,
272 ) -> Result<PaymentReminderResponseDto, String> {
273 let mut reminder = self
274 .reminder_repository
275 .find_by_id(id)
276 .await?
277 .ok_or_else(|| "Reminder not found".to_string())?;
278
279 reminder.set_tracking_number(dto.tracking_number)?;
280
281 let updated = self.reminder_repository.update(&reminder).await?;
282 Ok(updated.into())
283 }
284
285 pub async fn find_pending_reminders(&self) -> Result<Vec<PaymentReminderResponseDto>, String> {
287 let reminders = self.reminder_repository.find_pending_reminders().await?;
288 Ok(reminders.into_iter().map(|r| r.into()).collect())
289 }
290
291 pub async fn find_reminders_needing_escalation(
293 &self,
294 ) -> Result<Vec<PaymentReminderResponseDto>, String> {
295 let cutoff_date = Utc::now() - chrono::Duration::days(15);
296 let reminders = self
297 .reminder_repository
298 .find_reminders_needing_escalation(cutoff_date)
299 .await?;
300
301 let needs_escalation: Vec<PaymentReminder> = reminders
303 .into_iter()
304 .filter(|r| r.needs_escalation(Utc::now()))
305 .collect();
306
307 Ok(needs_escalation.into_iter().map(|r| r.into()).collect())
308 }
309
310 pub async fn get_recovery_stats(
312 &self,
313 organization_id: Uuid,
314 ) -> Result<PaymentRecoveryStatsDto, String> {
315 let (total_owed, total_penalties, level_counts) = self
316 .reminder_repository
317 .get_dashboard_stats(organization_id)
318 .await?;
319
320 let status_counts = self
321 .reminder_repository
322 .count_by_status(organization_id)
323 .await?;
324
325 Ok(PaymentRecoveryStatsDto {
326 total_owed,
327 total_penalties,
328 reminder_counts: level_counts
329 .into_iter()
330 .map(|(level, count)| ReminderLevelCountDto { level, count })
331 .collect(),
332 status_counts: status_counts
333 .into_iter()
334 .map(|(status, count)| ReminderStatusCountDto { status, count })
335 .collect(),
336 })
337 }
338
339 pub async fn find_overdue_expenses_without_reminders(
341 &self,
342 organization_id: Uuid,
343 min_days_overdue: i64,
344 ) -> Result<Vec<OverdueExpenseDto>, String> {
345 let results = self
346 .reminder_repository
347 .find_overdue_expenses_without_reminders(organization_id, min_days_overdue)
348 .await?;
349
350 Ok(results
351 .into_iter()
352 .map(|(expense_id, owner_id, days_overdue, amount)| {
353 OverdueExpenseDto::new(
354 expense_id.to_string(),
355 owner_id.to_string(),
356 days_overdue,
357 amount,
358 )
359 })
360 .collect())
361 }
362
363 pub async fn bulk_create_reminders(
365 &self,
366 dto: BulkCreateRemindersDto,
367 ) -> Result<BulkCreateRemindersResponseDto, String> {
368 let organization_id = Uuid::parse_str(&dto.organization_id)
369 .map_err(|_| "Invalid organization_id format".to_string())?;
370
371 let overdue_list = self
372 .find_overdue_expenses_without_reminders(organization_id, dto.min_days_overdue)
373 .await?;
374
375 let mut created_count = 0;
376 let mut skipped_count = 0;
377 let mut errors = Vec::new();
378 let mut created_reminders = Vec::new();
379
380 for overdue in overdue_list {
381 let expense_id = Uuid::parse_str(&overdue.expense_id);
382 let owner_id = Uuid::parse_str(&overdue.owner_id);
383
384 if expense_id.is_err() || owner_id.is_err() {
385 errors.push(
386 "Invalid UUID format for expense_id or owner_id in overdue item".to_string(),
387 );
388 skipped_count += 1;
389 continue;
390 }
391
392 let expense_id = expense_id.unwrap();
393 let owner_id = owner_id.unwrap();
394
395 let expense_result = self.expense_repository.find_by_id(expense_id).await;
397
398 match expense_result {
399 Ok(Some(expense)) => {
400 let due_date = expense.expense_date;
401
402 let create_dto = CreatePaymentReminderDto {
403 organization_id: organization_id.to_string(),
404 expense_id: expense_id.to_string(),
405 owner_id: owner_id.to_string(),
406 level: overdue.recommended_level,
407 amount_owed: overdue.amount,
408 due_date: due_date.to_rfc3339(),
409 days_overdue: overdue.days_overdue,
410 };
411
412 match self.create_reminder(create_dto).await {
413 Ok(reminder) => {
414 created_count += 1;
415 created_reminders.push(reminder);
416 }
417 Err(e) => {
418 errors.push(format!(
419 "Error creating reminder for expense {}: {}",
420 expense_id, e
421 ));
422 skipped_count += 1;
423 }
424 }
425 }
426 Ok(None) => {
427 errors.push(format!("Expense {} not found", expense_id));
428 skipped_count += 1;
429 }
430 Err(e) => {
431 errors.push(format!("Error fetching expense {}: {}", expense_id, e));
432 skipped_count += 1;
433 }
434 }
435 }
436
437 Ok(BulkCreateRemindersResponseDto {
438 created_count,
439 skipped_count,
440 errors,
441 created_reminders,
442 })
443 }
444
445 pub async fn process_automatic_escalations(&self) -> Result<i32, String> {
447 let reminders = self.find_reminders_needing_escalation().await?;
448 let mut escalated_count = 0;
449
450 for reminder_dto in reminders {
451 let id =
452 Uuid::parse_str(&reminder_dto.id).map_err(|_| "Invalid reminder ID".to_string())?;
453
454 match self
455 .escalate_reminder(id, EscalateReminderDto { reason: None })
456 .await
457 {
458 Ok(_) => escalated_count += 1,
459 Err(e) => {
460 eprintln!("Error escalating reminder {}: {}", id, e);
461 }
462 }
463 }
464
465 Ok(escalated_count)
466 }
467
468 pub async fn recalculate_all_penalties(&self, organization_id: Uuid) -> Result<i32, String> {
470 let reminders = self
471 .reminder_repository
472 .find_by_organization_and_status(organization_id, ReminderStatus::Sent)
473 .await?;
474
475 let mut updated_count = 0;
476
477 for mut reminder in reminders {
478 let current_days = (Utc::now() - reminder.due_date).num_days();
479 if current_days != reminder.days_overdue {
480 reminder.recalculate_penalties(current_days);
481 self.reminder_repository.update(&reminder).await?;
482 updated_count += 1;
483 }
484 }
485
486 Ok(updated_count)
487 }
488
489 pub async fn delete_reminder(&self, id: Uuid) -> Result<bool, String> {
491 self.reminder_repository.delete(id).await
492 }
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498 use crate::application::ports::PaymentReminderRepository;
499 use crate::domain::entities::ReminderLevel;
500 use async_trait::async_trait;
501 use std::collections::HashMap;
502 use std::sync::Mutex;
503
504 #[allow(dead_code)]
506 struct MockPaymentReminderRepository {
507 reminders: Mutex<HashMap<Uuid, PaymentReminder>>,
508 }
509
510 #[allow(dead_code)]
511 impl MockPaymentReminderRepository {
512 fn new() -> Self {
513 Self {
514 reminders: Mutex::new(HashMap::new()),
515 }
516 }
517 }
518
519 #[async_trait]
520 impl PaymentReminderRepository for MockPaymentReminderRepository {
521 async fn create(&self, reminder: &PaymentReminder) -> Result<PaymentReminder, String> {
522 let mut reminders = self.reminders.lock().unwrap();
523 reminders.insert(reminder.id, reminder.clone());
524 Ok(reminder.clone())
525 }
526
527 async fn find_by_id(&self, id: Uuid) -> Result<Option<PaymentReminder>, String> {
528 let reminders = self.reminders.lock().unwrap();
529 Ok(reminders.get(&id).cloned())
530 }
531
532 async fn find_by_expense(&self, expense_id: Uuid) -> Result<Vec<PaymentReminder>, String> {
533 let reminders = self.reminders.lock().unwrap();
534 Ok(reminders
535 .values()
536 .filter(|r| r.expense_id == expense_id)
537 .cloned()
538 .collect())
539 }
540
541 async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<PaymentReminder>, String> {
542 let reminders = self.reminders.lock().unwrap();
543 Ok(reminders
544 .values()
545 .filter(|r| r.owner_id == owner_id)
546 .cloned()
547 .collect())
548 }
549
550 async fn find_by_organization(
551 &self,
552 organization_id: Uuid,
553 ) -> Result<Vec<PaymentReminder>, String> {
554 let reminders = self.reminders.lock().unwrap();
555 Ok(reminders
556 .values()
557 .filter(|r| r.organization_id == organization_id)
558 .cloned()
559 .collect())
560 }
561
562 async fn find_by_status(
563 &self,
564 status: ReminderStatus,
565 ) -> Result<Vec<PaymentReminder>, String> {
566 let reminders = self.reminders.lock().unwrap();
567 Ok(reminders
568 .values()
569 .filter(|r| r.status == status)
570 .cloned()
571 .collect())
572 }
573
574 async fn find_by_organization_and_status(
575 &self,
576 organization_id: Uuid,
577 status: ReminderStatus,
578 ) -> Result<Vec<PaymentReminder>, String> {
579 let reminders = self.reminders.lock().unwrap();
580 Ok(reminders
581 .values()
582 .filter(|r| r.organization_id == organization_id && r.status == status)
583 .cloned()
584 .collect())
585 }
586
587 async fn find_pending_reminders(&self) -> Result<Vec<PaymentReminder>, String> {
588 self.find_by_status(ReminderStatus::Pending).await
589 }
590
591 async fn find_reminders_needing_escalation(
592 &self,
593 _cutoff_date: DateTime<Utc>,
594 ) -> Result<Vec<PaymentReminder>, String> {
595 Ok(vec![])
596 }
597
598 async fn find_latest_by_expense(
599 &self,
600 _expense_id: Uuid,
601 ) -> Result<Option<PaymentReminder>, String> {
602 Ok(None)
603 }
604
605 async fn find_active_by_owner(
606 &self,
607 _owner_id: Uuid,
608 ) -> Result<Vec<PaymentReminder>, String> {
609 Ok(vec![])
610 }
611
612 async fn count_by_status(
613 &self,
614 _organization_id: Uuid,
615 ) -> Result<Vec<(ReminderStatus, i64)>, String> {
616 Ok(vec![])
617 }
618
619 async fn get_total_owed_by_organization(
620 &self,
621 _organization_id: Uuid,
622 ) -> Result<f64, String> {
623 Ok(0.0)
624 }
625
626 async fn get_total_penalties_by_organization(
627 &self,
628 _organization_id: Uuid,
629 ) -> Result<f64, String> {
630 Ok(0.0)
631 }
632
633 async fn find_overdue_expenses_without_reminders(
634 &self,
635 _organization_id: Uuid,
636 _min_days_overdue: i64,
637 ) -> Result<Vec<(Uuid, Uuid, i64, f64)>, String> {
638 Ok(vec![])
639 }
640
641 async fn update(&self, reminder: &PaymentReminder) -> Result<PaymentReminder, String> {
642 let mut reminders = self.reminders.lock().unwrap();
643 reminders.insert(reminder.id, reminder.clone());
644 Ok(reminder.clone())
645 }
646
647 async fn delete(&self, id: Uuid) -> Result<bool, String> {
648 let mut reminders = self.reminders.lock().unwrap();
649 Ok(reminders.remove(&id).is_some())
650 }
651
652 async fn get_dashboard_stats(
653 &self,
654 _organization_id: Uuid,
655 ) -> Result<(f64, f64, Vec<(ReminderLevel, i64)>), String> {
656 Ok((0.0, 0.0, vec![]))
657 }
658 }
659
660 }