Skip to main content

koprogo_api/application/use_cases/
stats_use_cases.rs

1use crate::application::dto::{
2    AdminDashboardStats, DuAupresDuneAcp, SeedDataStats, SyndicDashboardStats, UrgentTask,
3};
4use crate::application::error::AppError;
5use crate::application::ports::StatsRepository;
6use rust_decimal::Decimal;
7use std::sync::Arc;
8use uuid::Uuid;
9
10pub struct StatsUseCases {
11    repo: Arc<dyn StatsRepository>,
12}
13
14impl StatsUseCases {
15    pub fn new(repo: Arc<dyn StatsRepository>) -> Self {
16        Self { repo }
17    }
18
19    pub async fn get_admin_dashboard_stats(&self) -> Result<AdminDashboardStats, AppError> {
20        self.repo.get_admin_dashboard_stats().await
21    }
22
23    pub async fn get_seed_data_stats(&self) -> Result<SeedDataStats, AppError> {
24        self.repo.get_seed_data_stats().await
25    }
26
27    pub async fn get_syndic_stats(
28        &self,
29        organization_id: Uuid,
30    ) -> Result<SyndicDashboardStats, AppError> {
31        self.repo.get_syndic_stats(organization_id).await
32    }
33
34    /// Returns owner stats. If the user has no owner record returns empty stats.
35    pub async fn get_owner_stats_by_user_id(
36        &self,
37        user_id: Uuid,
38    ) -> Result<SyndicDashboardStats, AppError> {
39        match self.repo.find_owner_id_by_user_id(user_id).await? {
40            None => Ok(SyndicDashboardStats {
41                total_buildings: 0,
42                total_units: 0,
43                declared_units: 0,
44                total_owners: 0,
45                pending_expenses_count: 0,
46                pending_expenses_amount: Decimal::ZERO,
47                next_meeting: None,
48            }),
49            Some(owner_id) => self.repo.get_owner_stats(owner_id).await,
50        }
51    }
52
53    /// Ce que le copropriétaire doit, ventilé par association.
54    ///
55    /// Liste vide si l'utilisateur n'est rattaché à aucune fiche de
56    /// copropriétaire : ce n'est pas une erreur, c'est un compte qui n'a pas
57    /// encore de lot. Rendre une erreur ferait afficher une panne là où il n'y
58    /// a rien à payer.
59    pub async fn get_owner_dues_by_acp(
60        &self,
61        user_id: Uuid,
62    ) -> Result<Vec<DuAupresDuneAcp>, AppError> {
63        match self.repo.find_owner_id_by_user_id(user_id).await? {
64            None => Ok(Vec::new()),
65            Some(owner_id) => self.repo.get_owner_dues_by_acp(owner_id).await,
66        }
67    }
68
69    pub async fn get_syndic_urgent_tasks(
70        &self,
71        organization_id: Uuid,
72    ) -> Result<Vec<UrgentTask>, AppError> {
73        self.repo.get_syndic_urgent_tasks(organization_id).await
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use async_trait::async_trait;
81    use rust_decimal_macros::dec;
82
83    struct MockStatsRepository {
84        owner_id: Option<Uuid>,
85    }
86
87    #[async_trait]
88    impl StatsRepository for MockStatsRepository {
89        async fn get_admin_dashboard_stats(&self) -> Result<AdminDashboardStats, AppError> {
90            Ok(AdminDashboardStats {
91                total_organizations: 5,
92                total_users: 50,
93                total_buildings: 10,
94                active_subscriptions: 4,
95                total_owners: 30,
96                total_units: 100,
97                total_expenses: 200,
98                total_meetings: 20,
99            })
100        }
101        async fn get_seed_data_stats(&self) -> Result<SeedDataStats, AppError> {
102            Ok(SeedDataStats {
103                seed_organizations: 1,
104                production_organizations: 4,
105                seed_buildings: 3,
106                seed_units: 15,
107                seed_owners: 10,
108                seed_unit_owners: 15,
109                seed_expenses: 20,
110                seed_meetings: 5,
111                seed_users: 8,
112            })
113        }
114        async fn get_syndic_stats(
115            &self,
116            _organization_id: Uuid,
117        ) -> Result<SyndicDashboardStats, AppError> {
118            Ok(SyndicDashboardStats {
119                total_buildings: 2,
120                total_units: 10,
121                declared_units: 12,
122                total_owners: 8,
123                pending_expenses_count: 3,
124                pending_expenses_amount: dec!(1500.00),
125                next_meeting: None,
126            })
127        }
128        async fn get_owner_dues_by_acp(
129            &self,
130            _owner_id: Uuid,
131        ) -> Result<Vec<crate::application::dto::DuAupresDuneAcp>, AppError> {
132            // DEUX associations : c'est le cas qui compte. Une doublure à une
133            // seule ACP laisserait passer un écran qui additionne les dettes
134            // de personnes morales distinctes — le défaut de #867.
135            Ok(vec![
136                crate::application::dto::DuAupresDuneAcp {
137                    acp_id: "acp-1".to_string(),
138                    acp_name: "Les Érables".to_string(),
139                    bce_number: Some("0123.456.789".to_string()),
140                    charges_en_attente: 2,
141                    montant: dec!(842.50),
142                },
143                crate::application::dto::DuAupresDuneAcp {
144                    acp_id: "acp-2".to_string(),
145                    acp_name: "Les Glycines".to_string(),
146                    bce_number: Some("0987.654.321".to_string()),
147                    charges_en_attente: 1,
148                    montant: dec!(420.00),
149                },
150            ])
151        }
152
153        async fn get_owner_stats(&self, _owner_id: Uuid) -> Result<SyndicDashboardStats, AppError> {
154            Ok(SyndicDashboardStats {
155                total_buildings: 1,
156                total_units: 2,
157                declared_units: 2,
158                total_owners: 5,
159                pending_expenses_count: 1,
160                pending_expenses_amount: dec!(500.00),
161                next_meeting: None,
162            })
163        }
164        async fn find_owner_id_by_user_id(&self, _user_id: Uuid) -> Result<Option<Uuid>, AppError> {
165            Ok(self.owner_id)
166        }
167        async fn get_syndic_urgent_tasks(
168            &self,
169            _organization_id: Uuid,
170        ) -> Result<Vec<UrgentTask>, AppError> {
171            Ok(vec![])
172        }
173    }
174
175    #[tokio::test]
176    async fn test_get_admin_dashboard_stats() {
177        let repo = Arc::new(MockStatsRepository { owner_id: None });
178        let use_cases = StatsUseCases::new(repo);
179        let stats = use_cases.get_admin_dashboard_stats().await.unwrap();
180        assert_eq!(stats.total_organizations, 5);
181        assert_eq!(stats.total_buildings, 10);
182    }
183
184    #[tokio::test]
185    async fn test_get_owner_stats_no_owner_record_returns_empty() {
186        let repo = Arc::new(MockStatsRepository { owner_id: None });
187        let use_cases = StatsUseCases::new(repo);
188        let stats = use_cases
189            .get_owner_stats_by_user_id(Uuid::new_v4())
190            .await
191            .unwrap();
192        assert_eq!(stats.total_buildings, 0);
193        assert_eq!(stats.total_units, 0);
194        assert!(stats.next_meeting.is_none());
195    }
196
197    #[tokio::test]
198    async fn test_get_owner_stats_with_owner_record() {
199        let owner_id = Uuid::new_v4();
200        let repo = Arc::new(MockStatsRepository {
201            owner_id: Some(owner_id),
202        });
203        let use_cases = StatsUseCases::new(repo);
204        let stats = use_cases
205            .get_owner_stats_by_user_id(Uuid::new_v4())
206            .await
207            .unwrap();
208        assert_eq!(stats.total_buildings, 1);
209        assert_eq!(stats.pending_expenses_count, 1);
210    }
211}