koprogo_api/application/use_cases/
dashboard_use_cases.rs

1// Application Use Cases: Dashboard
2//
3// Business logic for dashboard statistics and recent transactions
4
5use crate::application::dto::{
6    AccountantDashboardStats, ExpenseFilters, PageRequest, RecentTransaction, TransactionType,
7};
8use crate::application::ports::{
9    ExpenseRepository, OwnerContributionRepository, PaymentReminderRepository,
10};
11use crate::domain::entities::{ApprovalStatus, ReminderStatus};
12use chrono::{Datelike, Timelike, Utc};
13use std::collections::HashSet;
14use std::sync::Arc;
15use uuid::Uuid;
16
17pub struct DashboardUseCases {
18    expense_repo: Arc<dyn ExpenseRepository>,
19    owner_contribution_repo: Arc<dyn OwnerContributionRepository>,
20    payment_reminder_repo: Arc<dyn PaymentReminderRepository>,
21}
22
23impl DashboardUseCases {
24    pub fn new(
25        expense_repo: Arc<dyn ExpenseRepository>,
26        owner_contribution_repo: Arc<dyn OwnerContributionRepository>,
27        payment_reminder_repo: Arc<dyn PaymentReminderRepository>,
28    ) -> Self {
29        Self {
30            expense_repo,
31            owner_contribution_repo,
32            payment_reminder_repo,
33        }
34    }
35
36    /// Get accountant dashboard statistics
37    pub async fn get_accountant_stats(
38        &self,
39        organization_id: Uuid,
40    ) -> Result<AccountantDashboardStats, String> {
41        // Get all expenses for the organization
42        let filters = ExpenseFilters {
43            organization_id: Some(organization_id),
44            ..Default::default()
45        };
46
47        // Get all expenses (use large page size to get all)
48        let page_request = PageRequest {
49            page: 1,
50            per_page: 10000, // Large enough to get all expenses
51            sort_by: None,
52            order: Default::default(),
53        };
54
55        let (all_expenses, _total) = self
56            .expense_repo
57            .find_all_paginated(&page_request, &filters)
58            .await?;
59
60        // Get current month start date
61        let now = Utc::now();
62        let current_month_start = Utc::now()
63            .with_day(1)
64            .and_then(|d| d.with_hour(0))
65            .and_then(|d| d.with_minute(0))
66            .and_then(|d| d.with_second(0))
67            .unwrap_or(now);
68
69        // Filter expenses for current month
70        let current_month_expenses: Vec<_> = all_expenses
71            .iter()
72            .filter(|e| e.expense_date >= current_month_start)
73            .collect();
74
75        // Calculate total expenses for current month
76        let total_expenses_current_month: f64 = current_month_expenses
77            .iter()
78            .map(|e| e.amount_incl_vat.unwrap_or(0.0))
79            .sum();
80
81        // Calculate paid expenses (status = Approved AND paid_date is set)
82        let paid_expenses: Vec<_> = all_expenses
83            .iter()
84            .filter(|e| e.approval_status == ApprovalStatus::Approved && e.paid_date.is_some())
85            .collect();
86
87        let total_paid: f64 = paid_expenses
88            .iter()
89            .map(|e| e.amount_incl_vat.unwrap_or(0.0))
90            .sum();
91
92        // Calculate pending expenses (not paid)
93        let pending_expenses: Vec<_> = all_expenses
94            .iter()
95            .filter(|e| e.paid_date.is_none())
96            .collect();
97
98        let total_pending: f64 = pending_expenses
99            .iter()
100            .map(|e| e.amount_incl_vat.unwrap_or(0.0))
101            .sum();
102
103        // Calculate percentages
104        let total_all = total_paid + total_pending;
105        let paid_percentage = if total_all > 0.0 {
106            (total_paid / total_all) * 100.0
107        } else {
108            0.0
109        };
110        let pending_percentage = if total_all > 0.0 {
111            (total_pending / total_all) * 100.0
112        } else {
113            0.0
114        };
115
116        // Calculate owners with overdue payments from payment_reminders
117        // Get all reminders for the organization
118        let all_reminders = self
119            .payment_reminder_repo
120            .find_by_organization(organization_id)
121            .await?;
122
123        // Filter for active reminders (not Paid or Cancelled)
124        let active_reminders: Vec<_> = all_reminders
125            .iter()
126            .filter(|r| r.status != ReminderStatus::Paid && r.status != ReminderStatus::Cancelled)
127            .collect();
128
129        // Count unique owners with active reminders
130        let unique_owners: HashSet<Uuid> = active_reminders.iter().map(|r| r.owner_id).collect();
131
132        let owners_with_overdue = unique_owners.len() as i64;
133
134        Ok(AccountantDashboardStats {
135            total_expenses_current_month,
136            total_paid,
137            paid_percentage,
138            total_pending,
139            pending_percentage,
140            owners_with_overdue,
141        })
142    }
143
144    /// Get recent transactions for dashboard
145    pub async fn get_recent_transactions(
146        &self,
147        organization_id: Uuid,
148        limit: usize,
149    ) -> Result<Vec<RecentTransaction>, String> {
150        // Get all expenses for the organization
151        let filters = ExpenseFilters {
152            organization_id: Some(organization_id),
153            ..Default::default()
154        };
155
156        let page_request = PageRequest {
157            page: 1,
158            per_page: 1000, // Get enough for sorting
159            sort_by: None,
160            order: Default::default(),
161        };
162
163        let (all_expenses, _total) = self
164            .expense_repo
165            .find_all_paginated(&page_request, &filters)
166            .await?;
167
168        // Get all owner contributions for the organization
169        let all_contributions = self
170            .owner_contribution_repo
171            .find_by_organization(organization_id)
172            .await?;
173
174        // Convert expenses to transactions (OUTGOING = negative)
175        let expense_transactions: Vec<RecentTransaction> = all_expenses
176            .iter()
177            .map(|expense| {
178                let transaction_type = TransactionType::PaymentMade;
179                let amount_value = expense.amount_incl_vat.unwrap_or(0.0);
180                let amount = -amount_value; // Negative for expenses
181
182                RecentTransaction {
183                    id: expense.id,
184                    transaction_type,
185                    description: expense.description.clone(),
186                    related_entity: expense.supplier.clone(),
187                    amount,
188                    date: expense.expense_date,
189                }
190            })
191            .collect();
192
193        // Convert owner contributions to transactions (INCOMING = positive)
194        let contribution_transactions: Vec<RecentTransaction> = all_contributions
195            .iter()
196            .map(|contribution| {
197                let transaction_type = TransactionType::PaymentReceived;
198                let amount = contribution.amount; // Positive for revenue
199
200                RecentTransaction {
201                    id: contribution.id,
202                    transaction_type,
203                    description: contribution.description.clone(),
204                    related_entity: Some("Copropriétaire".to_string()), // Could link to owner name if needed
205                    amount,
206                    date: contribution.contribution_date,
207                }
208            })
209            .collect();
210
211        // Merge both transaction types
212        let mut all_transactions = Vec::new();
213        all_transactions.extend(expense_transactions);
214        all_transactions.extend(contribution_transactions);
215
216        // Sort by date (most recent first)
217        all_transactions.sort_by(|a, b| b.date.cmp(&a.date));
218
219        // Take the most recent ones
220        let recent_transactions: Vec<RecentTransaction> =
221            all_transactions.into_iter().take(limit).collect();
222
223        Ok(recent_transactions)
224    }
225}
226
227#[cfg(test)]
228mod tests {
229
230    #[test]
231    fn test_percentage_calculation() {
232        let total_paid = 41270.0;
233        let total_pending = 4580.0;
234        let total = total_paid + total_pending;
235
236        let paid_percentage = (total_paid / total) * 100.0;
237        let pending_percentage = (total_pending / total) * 100.0;
238
239        assert!((paid_percentage - 90.0_f64).abs() < 0.1);
240        assert!((pending_percentage - 10.0_f64).abs() < 0.1);
241    }
242
243    #[test]
244    fn test_percentage_with_zero_total() {
245        let total_paid = 0.0;
246        let total_pending = 0.0;
247        let total = total_paid + total_pending;
248
249        let paid_percentage = if total > 0.0 {
250            (total_paid / total) * 100.0
251        } else {
252            0.0
253        };
254
255        assert_eq!(paid_percentage, 0.0);
256    }
257}