koprogo_api/application/use_cases/
dashboard_use_cases.rs1use 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 pub async fn get_accountant_stats(
38 &self,
39 organization_id: Uuid,
40 ) -> Result<AccountantDashboardStats, String> {
41 let filters = ExpenseFilters {
43 organization_id: Some(organization_id),
44 ..Default::default()
45 };
46
47 let page_request = PageRequest {
49 page: 1,
50 per_page: 10000, 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 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 let current_month_expenses: Vec<_> = all_expenses
71 .iter()
72 .filter(|e| e.expense_date >= current_month_start)
73 .collect();
74
75 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 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 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 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 let all_reminders = self
119 .payment_reminder_repo
120 .find_by_organization(organization_id)
121 .await?;
122
123 let active_reminders: Vec<_> = all_reminders
125 .iter()
126 .filter(|r| r.status != ReminderStatus::Paid && r.status != ReminderStatus::Cancelled)
127 .collect();
128
129 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 pub async fn get_recent_transactions(
146 &self,
147 organization_id: Uuid,
148 limit: usize,
149 ) -> Result<Vec<RecentTransaction>, String> {
150 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, 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 let all_contributions = self
170 .owner_contribution_repo
171 .find_by_organization(organization_id)
172 .await?;
173
174 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; 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 let contribution_transactions: Vec<RecentTransaction> = all_contributions
195 .iter()
196 .map(|contribution| {
197 let transaction_type = TransactionType::PaymentReceived;
198 let amount = contribution.amount; RecentTransaction {
201 id: contribution.id,
202 transaction_type,
203 description: contribution.description.clone(),
204 related_entity: Some("Copropriétaire".to_string()), amount,
206 date: contribution.contribution_date,
207 }
208 })
209 .collect();
210
211 let mut all_transactions = Vec::new();
213 all_transactions.extend(expense_transactions);
214 all_transactions.extend(contribution_transactions);
215
216 all_transactions.sort_by(|a, b| b.date.cmp(&a.date));
218
219 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}