koprogo_api/application/use_cases/
dashboard_use_cases.rs1use crate::application::dto::{
6 AccountantDashboardStats, ExpenseFilters, PageRequest, RecentTransaction, TransactionType,
7};
8use crate::application::error::AppError;
9use crate::application::ports::{
10 ExpenseRepository, OwnerContributionRepository, PaymentReminderRepository,
11};
12use crate::domain::entities::{ApprovalStatus, ReminderStatus};
13use chrono::{Datelike, Timelike, Utc};
14use rust_decimal::Decimal;
15use rust_decimal_macros::dec;
16use std::collections::HashSet;
17use std::sync::Arc;
18use uuid::Uuid;
19
20pub struct DashboardUseCases {
21 expense_repo: Arc<dyn ExpenseRepository>,
22 owner_contribution_repo: Arc<dyn OwnerContributionRepository>,
23 payment_reminder_repo: Arc<dyn PaymentReminderRepository>,
24}
25
26impl DashboardUseCases {
27 pub fn new(
28 expense_repo: Arc<dyn ExpenseRepository>,
29 owner_contribution_repo: Arc<dyn OwnerContributionRepository>,
30 payment_reminder_repo: Arc<dyn PaymentReminderRepository>,
31 ) -> Self {
32 Self {
33 expense_repo,
34 owner_contribution_repo,
35 payment_reminder_repo,
36 }
37 }
38
39 pub async fn get_accountant_stats(
41 &self,
42 organization_id: Uuid,
43 ) -> Result<AccountantDashboardStats, AppError> {
44 let filters = ExpenseFilters {
46 organization_id: Some(organization_id),
47 ..Default::default()
48 };
49
50 let page_request = PageRequest {
52 page: 1,
53 per_page: 10000, sort_by: None,
55 order: Default::default(),
56 };
57
58 let (all_expenses, _total) = self
59 .expense_repo
60 .find_all_paginated(&page_request, &filters)
61 .await?;
62
63 let now = Utc::now();
65 let current_month_start = Utc::now()
66 .with_day(1)
67 .and_then(|d| d.with_hour(0))
68 .and_then(|d| d.with_minute(0))
69 .and_then(|d| d.with_second(0))
70 .unwrap_or(now);
71
72 let current_month_expenses: Vec<_> = all_expenses
74 .iter()
75 .filter(|e| e.expense_date >= current_month_start)
76 .collect();
77
78 let total_expenses_current_month: Decimal = current_month_expenses
80 .iter()
81 .map(|e| e.amount_incl_vat.unwrap_or(Decimal::ZERO))
82 .sum();
83
84 let paid_expenses: Vec<_> = all_expenses
86 .iter()
87 .filter(|e| e.approval_status == ApprovalStatus::Approved && e.paid_date.is_some())
88 .collect();
89
90 let total_paid: Decimal = paid_expenses
91 .iter()
92 .map(|e| e.amount_incl_vat.unwrap_or(Decimal::ZERO))
93 .sum();
94
95 let pending_expenses: Vec<_> = all_expenses
97 .iter()
98 .filter(|e| e.paid_date.is_none())
99 .collect();
100
101 let total_pending: Decimal = pending_expenses
102 .iter()
103 .map(|e| e.amount_incl_vat.unwrap_or(Decimal::ZERO))
104 .sum();
105
106 let total_all = total_paid + total_pending;
108 let paid_percentage = if total_all > Decimal::ZERO {
109 (total_paid / total_all) * dec!(100)
110 } else {
111 Decimal::ZERO
112 };
113 let pending_percentage = if total_all > Decimal::ZERO {
114 (total_pending / total_all) * dec!(100)
115 } else {
116 Decimal::ZERO
117 };
118
119 let all_reminders = self
122 .payment_reminder_repo
123 .find_by_organization(organization_id)
124 .await?;
125
126 let active_reminders: Vec<_> = all_reminders
128 .iter()
129 .filter(|r| r.status != ReminderStatus::Paid && r.status != ReminderStatus::Cancelled)
130 .collect();
131
132 let unique_owners: HashSet<Uuid> = active_reminders.iter().map(|r| r.owner_id).collect();
134
135 let owners_with_overdue = unique_owners.len() as i64;
136
137 Ok(AccountantDashboardStats {
138 total_expenses_current_month,
139 total_paid,
140 paid_percentage,
141 total_pending,
142 pending_percentage,
143 owners_with_overdue,
144 })
145 }
146
147 pub async fn get_recent_transactions(
149 &self,
150 organization_id: Uuid,
151 limit: usize,
152 ) -> Result<Vec<RecentTransaction>, AppError> {
153 let filters = ExpenseFilters {
155 organization_id: Some(organization_id),
156 ..Default::default()
157 };
158
159 let page_request = PageRequest {
160 page: 1,
161 per_page: 1000, sort_by: None,
163 order: Default::default(),
164 };
165
166 let (all_expenses, _total) = self
167 .expense_repo
168 .find_all_paginated(&page_request, &filters)
169 .await?;
170
171 let all_contributions = self
173 .owner_contribution_repo
174 .find_by_organization(organization_id)
175 .await?;
176
177 let expense_transactions: Vec<RecentTransaction> = all_expenses
179 .iter()
180 .map(|expense| {
181 let transaction_type = TransactionType::PaymentMade;
182 let amount_value = expense.amount_incl_vat.unwrap_or(Decimal::ZERO);
183 let amount = -amount_value; RecentTransaction {
186 id: expense.id,
187 transaction_type,
188 description: expense.description.clone(),
189 related_entity: expense.supplier.clone(),
190 amount,
191 date: expense.expense_date,
192 }
193 })
194 .collect();
195
196 let contribution_transactions: Vec<RecentTransaction> = all_contributions
198 .iter()
199 .map(|contribution| {
200 let transaction_type = TransactionType::PaymentReceived;
201 let amount = contribution.amount; RecentTransaction {
204 id: contribution.id,
205 transaction_type,
206 description: contribution.description.clone(),
207 related_entity: Some("Copropriétaire".to_string()), amount,
209 date: contribution.contribution_date,
210 }
211 })
212 .collect();
213
214 let mut all_transactions = Vec::new();
216 all_transactions.extend(expense_transactions);
217 all_transactions.extend(contribution_transactions);
218
219 all_transactions.sort_by_key(|t| std::cmp::Reverse(t.date));
221
222 let recent_transactions: Vec<RecentTransaction> =
224 all_transactions.into_iter().take(limit).collect();
225
226 Ok(recent_transactions)
227 }
228}
229
230#[cfg(test)]
231mod tests {
232
233 #[test]
234 fn test_percentage_calculation() {
235 let total_paid = 41270.0;
236 let total_pending = 4580.0;
237 let total = total_paid + total_pending;
238
239 let paid_percentage = (total_paid / total) * 100.0;
240 let pending_percentage = (total_pending / total) * 100.0;
241
242 assert!((paid_percentage - 90.0_f64).abs() < 0.1);
243 assert!((pending_percentage - 10.0_f64).abs() < 0.1);
244 }
245
246 #[test]
247 fn test_percentage_with_zero_total() {
248 let total_paid = 0.0;
249 let total_pending = 0.0;
250 let total = total_paid + total_pending;
251
252 let paid_percentage = if total > 0.0 {
253 (total_paid / total) * 100.0
254 } else {
255 0.0
256 };
257
258 assert_eq!(paid_percentage, 0.0);
259 }
260}