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