koprogo_api/application/use_cases/
generate_ago_resolutions_use_case.rs1use crate::application::error::AppError;
13use crate::application::ports::{MeetingRepository, ResolutionRepository};
14use crate::domain::entities::{MeetingType, Resolution};
15use std::sync::Arc;
16use uuid::Uuid;
17
18pub struct GenerateAgoResolutionsUseCase {
19 meeting_repository: Arc<dyn MeetingRepository>,
20 resolution_repository: Arc<dyn ResolutionRepository>,
21}
22
23impl GenerateAgoResolutionsUseCase {
24 pub fn new(
25 meeting_repository: Arc<dyn MeetingRepository>,
26 resolution_repository: Arc<dyn ResolutionRepository>,
27 ) -> Self {
28 Self {
29 meeting_repository,
30 resolution_repository,
31 }
32 }
33
34 pub async fn generate_ago_resolutions(
40 &self,
41 meeting_id: Uuid,
42 ) -> Result<Vec<Resolution>, AppError> {
43 let meeting = self
44 .meeting_repository
45 .find_by_id(meeting_id)
46 .await
47 .map_err(AppError::from)?
48 .ok_or_else(|| AppError::NotFound(format!("meeting {}", meeting_id)))?;
49
50 if meeting.meeting_type != MeetingType::Ordinary {
51 return Ok(Vec::new());
52 }
53
54 let existantes = self
55 .resolution_repository
56 .find_by_meeting_id(meeting_id)
57 .await
58 .map_err(AppError::from)?;
59 if let Some(deja_generee) = existantes.into_iter().find(|r| r.is_auto_generated()) {
60 return Ok(vec![deja_generee]);
61 }
62
63 let resolution = Resolution::new_evaluation_contractors_auto(meeting_id);
64 let created = self
65 .resolution_repository
66 .create(&resolution)
67 .await
68 .map_err(AppError::from)?;
69 Ok(vec![created])
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use crate::domain::entities::{Meeting, ResolutionStatus};
77 use async_trait::async_trait;
78 use std::collections::HashMap;
79 use std::sync::Mutex;
80
81 struct MockMeetingRepository {
82 meetings: Mutex<HashMap<Uuid, Meeting>>,
83 }
84
85 impl MockMeetingRepository {
86 fn new() -> Self {
87 Self {
88 meetings: Mutex::new(HashMap::new()),
89 }
90 }
91
92 fn pose(&self, meeting: Meeting) {
93 self.meetings.lock().unwrap().insert(meeting.id, meeting);
94 }
95 }
96
97 #[async_trait]
98 impl MeetingRepository for MockMeetingRepository {
99 async fn create(&self, meeting: &Meeting) -> Result<Meeting, String> {
100 self.meetings
101 .lock()
102 .unwrap()
103 .insert(meeting.id, meeting.clone());
104 Ok(meeting.clone())
105 }
106 async fn find_by_id(&self, id: Uuid) -> Result<Option<Meeting>, String> {
107 Ok(self.meetings.lock().unwrap().get(&id).cloned())
108 }
109 async fn find_by_building(&self, _building_id: Uuid) -> Result<Vec<Meeting>, String> {
110 Ok(vec![])
111 }
112 async fn update(&self, meeting: &Meeting) -> Result<Meeting, String> {
113 self.meetings
114 .lock()
115 .unwrap()
116 .insert(meeting.id, meeting.clone());
117 Ok(meeting.clone())
118 }
119 async fn delete(&self, id: Uuid) -> Result<bool, String> {
120 Ok(self.meetings.lock().unwrap().remove(&id).is_some())
121 }
122 async fn find_all_paginated(
123 &self,
124 _page_request: &crate::application::dto::PageRequest,
125 _organization_id: Option<Uuid>,
126 ) -> Result<(Vec<Meeting>, i64), String> {
127 Ok((vec![], 0))
128 }
129 }
130
131 struct MockResolutionRepository {
132 resolutions: Mutex<HashMap<Uuid, Resolution>>,
133 }
134
135 impl MockResolutionRepository {
136 fn new() -> Self {
137 Self {
138 resolutions: Mutex::new(HashMap::new()),
139 }
140 }
141 }
142
143 #[async_trait]
144 impl ResolutionRepository for MockResolutionRepository {
145 async fn create(&self, resolution: &Resolution) -> Result<Resolution, String> {
146 self.resolutions
147 .lock()
148 .unwrap()
149 .insert(resolution.id, resolution.clone());
150 Ok(resolution.clone())
151 }
152 async fn find_by_id(&self, id: Uuid) -> Result<Option<Resolution>, String> {
153 Ok(self.resolutions.lock().unwrap().get(&id).cloned())
154 }
155 async fn find_by_meeting_id(&self, meeting_id: Uuid) -> Result<Vec<Resolution>, String> {
156 Ok(self
157 .resolutions
158 .lock()
159 .unwrap()
160 .values()
161 .filter(|r| r.meeting_id == meeting_id)
162 .cloned()
163 .collect())
164 }
165 async fn find_by_status(
166 &self,
167 status: ResolutionStatus,
168 ) -> Result<Vec<Resolution>, String> {
169 Ok(self
170 .resolutions
171 .lock()
172 .unwrap()
173 .values()
174 .filter(|r| r.status == status)
175 .cloned()
176 .collect())
177 }
178 async fn update(&self, resolution: &Resolution) -> Result<Resolution, String> {
179 self.resolutions
180 .lock()
181 .unwrap()
182 .insert(resolution.id, resolution.clone());
183 Ok(resolution.clone())
184 }
185 async fn delete(&self, id: Uuid) -> Result<bool, String> {
186 Ok(self.resolutions.lock().unwrap().remove(&id).is_some())
187 }
188 async fn update_vote_counts(
189 &self,
190 _resolution_id: Uuid,
191 _vote_count_pour: i32,
192 _vote_count_contre: i32,
193 _vote_count_abstention: i32,
194 _total_voting_power_pour: rust_decimal::Decimal,
195 _total_voting_power_contre: rust_decimal::Decimal,
196 _total_voting_power_abstention: rust_decimal::Decimal,
197 ) -> Result<(), String> {
198 Ok(())
199 }
200 async fn close_voting(
201 &self,
202 _resolution_id: Uuid,
203 _final_status: ResolutionStatus,
204 _voix_plafonnees: Option<serde_json::Value>,
205 ) -> Result<(), String> {
206 Ok(())
207 }
208 async fn get_meeting_vote_summary(
209 &self,
210 meeting_id: Uuid,
211 ) -> Result<Vec<Resolution>, String> {
212 self.find_by_meeting_id(meeting_id).await
213 }
214 }
215
216 fn ago(id: Uuid) -> Meeting {
217 Meeting::new(
218 Uuid::new_v4(),
219 Uuid::new_v4(),
220 Uuid::new_v4(),
221 MeetingType::Ordinary,
222 "AGO 2026".to_string(),
223 None,
224 chrono::Utc::now() + chrono::Duration::days(30),
225 "Salle des fêtes".to_string(),
226 )
227 .map(|mut m| {
228 m.id = id;
229 m
230 })
231 .expect("meeting AGO de test")
232 }
233
234 fn age(id: Uuid) -> Meeting {
235 Meeting::new(
236 Uuid::new_v4(),
237 Uuid::new_v4(),
238 Uuid::new_v4(),
239 MeetingType::Extraordinary,
240 "AGE 2026".to_string(),
241 None,
242 chrono::Utc::now() + chrono::Duration::days(30),
243 "Salle des fêtes".to_string(),
244 )
245 .map(|mut m| {
246 m.id = id;
247 m
248 })
249 .expect("meeting AGE de test")
250 }
251
252 #[tokio::test]
255 async fn happy_ago_genere_la_resolution_auto() {
256 let meeting_id = Uuid::new_v4();
257 let meeting_repo = Arc::new(MockMeetingRepository::new());
258 meeting_repo.pose(ago(meeting_id));
259 let resolution_repo = Arc::new(MockResolutionRepository::new());
260 let use_case = GenerateAgoResolutionsUseCase::new(meeting_repo, resolution_repo);
261
262 let created = use_case
263 .generate_ago_resolutions(meeting_id)
264 .await
265 .expect("génération réussie");
266
267 assert_eq!(created.len(), 1);
268 assert!(created[0].is_auto_generated());
269 assert_eq!(created[0].meeting_id, meeting_id);
270 }
271
272 #[tokio::test]
275 async fn edge_age_ne_genere_rien() {
276 let meeting_id = Uuid::new_v4();
277 let meeting_repo = Arc::new(MockMeetingRepository::new());
278 meeting_repo.pose(age(meeting_id));
279 let resolution_repo = Arc::new(MockResolutionRepository::new());
280 let use_case = GenerateAgoResolutionsUseCase::new(meeting_repo, resolution_repo);
281
282 let created = use_case
283 .generate_ago_resolutions(meeting_id)
284 .await
285 .expect("pas d'erreur, juste rien à générer");
286
287 assert!(created.is_empty());
288 }
289
290 #[tokio::test]
295 async fn security_generation_repetee_ne_duplique_pas() {
296 let meeting_id = Uuid::new_v4();
297 let meeting_repo = Arc::new(MockMeetingRepository::new());
298 meeting_repo.pose(ago(meeting_id));
299 let resolution_repo = Arc::new(MockResolutionRepository::new());
300 let use_case = GenerateAgoResolutionsUseCase::new(meeting_repo, resolution_repo);
301
302 let premiere = use_case.generate_ago_resolutions(meeting_id).await.unwrap();
303 let seconde = use_case.generate_ago_resolutions(meeting_id).await.unwrap();
304
305 assert_eq!(premiere.len(), 1);
306 assert_eq!(seconde.len(), 1);
307 assert_eq!(premiere[0].id, seconde[0].id);
308 }
309
310 #[tokio::test]
313 async fn negative_meeting_introuvable_rend_404() {
314 let meeting_repo = Arc::new(MockMeetingRepository::new());
315 let resolution_repo = Arc::new(MockResolutionRepository::new());
316 let use_case = GenerateAgoResolutionsUseCase::new(meeting_repo, resolution_repo);
317
318 let result = use_case.generate_ago_resolutions(Uuid::new_v4()).await;
319
320 assert!(matches!(result, Err(AppError::NotFound(_))));
321 }
322}