1use crate::application::dto::{
2 ConvocationRecipientResponse, ConvocationResponse, CreateConvocationRequest,
3 EligibleRecipientResponse, RecipientTrackingSummaryResponse, ScheduleConvocationRequest,
4 SendConvocationRequest,
5};
6use crate::application::error::AppError;
7use crate::application::ports::{
8 BuildingRepository, ConvocationRecipientRepository, ConvocationRepository, MeetingRepository,
9 OwnerRepository, UserRepository,
10};
11use crate::domain::entities::{
12 AttendanceStatus, Convocation, ConvocationRecipient, QualiteDuMandataire,
13};
14use crate::domain::services::ConvocationExporter;
15use chrono::Utc;
16use std::sync::Arc;
17use uuid::Uuid;
18
19pub struct ConvocationUseCases {
20 convocation_repository: Arc<dyn ConvocationRepository>,
21 recipient_repository: Arc<dyn ConvocationRecipientRepository>,
22 owner_repository: Arc<dyn OwnerRepository>,
23 building_repository: Arc<dyn BuildingRepository>,
24 meeting_repository: Arc<dyn MeetingRepository>,
25 unit_owner_repository: Arc<dyn crate::application::ports::UnitOwnerRepository>,
29 user_repository: Arc<dyn UserRepository>,
35}
36
37impl ConvocationUseCases {
38 pub fn new(
39 convocation_repository: Arc<dyn ConvocationRepository>,
40 recipient_repository: Arc<dyn ConvocationRecipientRepository>,
41 owner_repository: Arc<dyn OwnerRepository>,
42 building_repository: Arc<dyn BuildingRepository>,
43 meeting_repository: Arc<dyn MeetingRepository>,
44 unit_owner_repository: Arc<dyn crate::application::ports::UnitOwnerRepository>,
45 user_repository: Arc<dyn UserRepository>,
46 ) -> Self {
47 Self {
48 convocation_repository,
49 recipient_repository,
50 owner_repository,
51 building_repository,
52 meeting_repository,
53 unit_owner_repository,
54 user_repository,
55 }
56 }
57
58 pub async fn create_convocation(
60 &self,
61 organization_id: Uuid,
62 request: CreateConvocationRequest,
63 created_by: Uuid,
64 ) -> Result<ConvocationResponse, String> {
65 let building = self
68 .building_repository
69 .find_by_id(request.building_id)
70 .await?
71 .ok_or_else(|| "Immeuble introuvable".to_string())?;
72
73 let convocation = Convocation::new(
75 building.acp_id,
76 organization_id,
77 request.building_id,
78 request.meeting_id,
79 request.meeting_type,
80 request.meeting_date,
81 request.language,
82 created_by,
83 )?;
84
85 let created = self.convocation_repository.create(&convocation).await?;
86
87 Ok(ConvocationResponse::from(created))
88 }
89
90 async fn active_owner_ids(&self, building_id: Uuid) -> Result<Vec<Uuid>, String> {
97 let detenteurs = self
98 .unit_owner_repository
99 .find_active_by_building(building_id)
100 .await?;
101 let mut vus = std::collections::BTreeSet::new();
104 Ok(detenteurs
105 .into_iter()
106 .filter_map(|(_unit_id, owner_id, _quota)| vus.insert(owner_id).then_some(owner_id))
107 .collect())
108 }
109
110 pub async fn list_eligible_recipients(
119 &self,
120 building_id: Uuid,
121 ) -> Result<Vec<EligibleRecipientResponse>, AppError> {
122 let owner_ids = self.active_owner_ids(building_id).await?;
123 let mut destinataires = Vec::with_capacity(owner_ids.len());
124 for owner_id in owner_ids {
125 let owner = self
126 .owner_repository
127 .find_by_id(owner_id)
128 .await?
129 .ok_or_else(|| AppError::NotFound(format!("Owner {}", owner_id)))?;
130 destinataires.push(EligibleRecipientResponse {
131 owner_id: owner.id,
132 full_name: format!("{} {}", owner.first_name, owner.last_name),
133 email: owner.email,
134 });
135 }
136 destinataires.sort_by(|a, b| a.full_name.cmp(&b.full_name));
137 Ok(destinataires)
138 }
139
140 pub async fn get_convocation(&self, id: Uuid) -> Result<ConvocationResponse, String> {
142 let convocation = self
143 .convocation_repository
144 .find_by_id(id)
145 .await?
146 .ok_or_else(|| format!("Convocation not found: {}", id))?;
147
148 Ok(ConvocationResponse::from(convocation))
149 }
150
151 pub async fn get_convocation_by_meeting(
153 &self,
154 meeting_id: Uuid,
155 ) -> Result<Option<ConvocationResponse>, String> {
156 let convocation = self
157 .convocation_repository
158 .find_by_meeting_id(meeting_id)
159 .await?;
160
161 Ok(convocation.map(ConvocationResponse::from))
162 }
163
164 pub async fn list_building_convocations(
166 &self,
167 building_id: Uuid,
168 ) -> Result<Vec<ConvocationResponse>, String> {
169 let convocations = self
170 .convocation_repository
171 .find_by_building(building_id)
172 .await?;
173
174 Ok(convocations
175 .into_iter()
176 .map(ConvocationResponse::from)
177 .collect())
178 }
179
180 pub async fn list_organization_convocations(
182 &self,
183 organization_id: Uuid,
184 ) -> Result<Vec<ConvocationResponse>, String> {
185 let convocations = self
186 .convocation_repository
187 .find_by_organization(organization_id)
188 .await?;
189
190 Ok(convocations
191 .into_iter()
192 .map(ConvocationResponse::from)
193 .collect())
194 }
195
196 pub async fn schedule_convocation(
198 &self,
199 id: Uuid,
200 request: ScheduleConvocationRequest,
201 ) -> Result<ConvocationResponse, String> {
202 let mut convocation = self
203 .convocation_repository
204 .find_by_id(id)
205 .await?
206 .ok_or_else(|| format!("Convocation not found: {}", id))?;
207
208 convocation.schedule(request.send_date)?;
209
210 let updated = self.convocation_repository.update(&convocation).await?;
211
212 Ok(ConvocationResponse::from(updated))
213 }
214
215 pub async fn send_convocation(
218 &self,
219 id: Uuid,
220 request: SendConvocationRequest,
221 ) -> Result<ConvocationResponse, String> {
222 if matches!(&request.recipient_owner_ids, Some(ids) if ids.is_empty()) {
230 return Err(
231 "Sélectionnez au moins un destinataire avant d'envoyer la convocation.".to_string(),
232 );
233 }
234
235 let mut convocation = self
236 .convocation_repository
237 .find_by_id(id)
238 .await?
239 .ok_or_else(|| format!("Convocation not found: {}", id))?;
240
241 let building = self
243 .building_repository
244 .find_by_id(convocation.building_id)
245 .await?
246 .ok_or_else(|| format!("Building not found: {}", convocation.building_id))?;
247
248 let meeting = self
250 .meeting_repository
251 .find_by_id(convocation.meeting_id)
252 .await?
253 .ok_or_else(|| format!("Meeting not found: {}", convocation.meeting_id))?;
254
255 let pdf_bytes = ConvocationExporter::export_to_pdf(&building, &meeting, &convocation)
257 .map_err(|e| format!("Failed to generate PDF: {}", e))?;
258
259 let upload_dir =
261 std::env::var("UPLOAD_DIR").unwrap_or_else(|_| "/tmp/koprogo-uploads".to_string());
262 let pdf_file_path = format!("{}/convocations/conv-{}.pdf", upload_dir, id);
263 ConvocationExporter::save_to_file(&pdf_bytes, &pdf_file_path)
264 .map_err(|e| format!("Failed to save PDF: {}", e))?;
265
266 let destinataires: Vec<Uuid> = match &request.recipient_owner_ids {
270 Some(ids) => ids.clone(),
271 None => self.active_owner_ids(convocation.building_id).await?,
272 };
273
274 if destinataires.is_empty() {
281 return Err(
282 "Aucun copropriétaire à convoquer : cet immeuble n'a pas de lot \
283 attribué. Rattachez les copropriétaires à leurs lots avant de \
284 convoquer l'assemblée."
285 .to_string(),
286 );
287 }
288
289 let mut recipients = Vec::new();
291 for owner_id in &destinataires {
292 let owner = self
293 .owner_repository
294 .find_by_id(*owner_id)
295 .await?
296 .ok_or_else(|| format!("Owner not found: {}", owner_id))?;
297
298 let mut recipient = ConvocationRecipient::new(id, *owner_id, owner.email)?;
299 recipient.mark_email_sent();
300 recipients.push(recipient);
301 }
302
303 let created_recipients = self.recipient_repository.create_many(&recipients).await?;
305
306 convocation.mark_sent(pdf_file_path, created_recipients.len() as i32)?;
308
309 let updated = self.convocation_repository.update(&convocation).await?;
310
311 Ok(ConvocationResponse::from(updated))
312 }
313
314 pub async fn mark_recipient_email_sent(
316 &self,
317 recipient_id: Uuid,
318 ) -> Result<ConvocationRecipientResponse, String> {
319 let mut recipient = self
320 .recipient_repository
321 .find_by_id(recipient_id)
322 .await?
323 .ok_or_else(|| format!("Recipient not found: {}", recipient_id))?;
324
325 recipient.mark_email_sent();
326
327 let updated = self.recipient_repository.update(&recipient).await?;
328
329 Ok(ConvocationRecipientResponse::from(updated))
330 }
331
332 pub async fn mark_recipient_email_opened(
334 &self,
335 recipient_id: Uuid,
336 ) -> Result<ConvocationRecipientResponse, String> {
337 let mut recipient = self
338 .recipient_repository
339 .find_by_id(recipient_id)
340 .await?
341 .ok_or_else(|| format!("Recipient not found: {}", recipient_id))?;
342
343 recipient.mark_email_opened()?;
344
345 let updated = self.recipient_repository.update(&recipient).await?;
346
347 self.update_convocation_tracking(recipient.convocation_id)
349 .await?;
350
351 Ok(ConvocationRecipientResponse::from(updated))
352 }
353
354 pub async fn update_recipient_attendance(
356 &self,
357 recipient_id: Uuid,
358 status: AttendanceStatus,
359 ) -> Result<ConvocationRecipientResponse, String> {
360 let mut recipient = self
361 .recipient_repository
362 .find_by_id(recipient_id)
363 .await?
364 .ok_or_else(|| format!("Recipient not found: {}", recipient_id))?;
365
366 recipient.update_attendance_status(status)?;
367
368 let updated = self.recipient_repository.update(&recipient).await?;
369
370 self.update_convocation_tracking(recipient.convocation_id)
372 .await?;
373
374 Ok(ConvocationRecipientResponse::from(updated))
375 }
376
377 pub async fn set_recipient_proxy(
379 &self,
380 recipient_id: Uuid,
381 proxy_owner_id: Uuid,
382 ) -> Result<ConvocationRecipientResponse, String> {
383 let mut recipient = self
384 .recipient_repository
385 .find_by_id(recipient_id)
386 .await?
387 .ok_or_else(|| format!("Recipient not found: {}", recipient_id))?;
388
389 let qualite = self.qualite_du_mandataire(proxy_owner_id).await?;
390 recipient.set_proxy(proxy_owner_id, qualite)?;
391
392 let updated = self.recipient_repository.update(&recipient).await?;
393
394 Ok(ConvocationRecipientResponse::from(updated))
395 }
396
397 async fn qualite_du_mandataire(
413 &self,
414 proxy_owner_id: Uuid,
415 ) -> Result<QualiteDuMandataire, String> {
416 let mandataire = self
417 .owner_repository
418 .find_by_id(proxy_owner_id)
419 .await?
420 .ok_or_else(|| format!("Mandataire introuvable : {}", proxy_owner_id))?;
421
422 let Some(user_id) = mandataire.user_id else {
423 return Ok(QualiteDuMandataire::Coproprietaire);
424 };
425
426 let Some(utilisateur) = self.user_repository.find_by_id(user_id).await? else {
427 return Ok(QualiteDuMandataire::Coproprietaire);
428 };
429
430 Ok(match utilisateur.role {
431 crate::domain::plateforme::user::UserRole::Syndic => QualiteDuMandataire::Syndic,
432 _ => QualiteDuMandataire::Coproprietaire,
433 })
434 }
435
436 pub async fn send_reminders(
439 &self,
440 convocation_id: Uuid,
441 ) -> Result<Vec<ConvocationRecipientResponse>, String> {
442 let recipients = self
444 .recipient_repository
445 .find_needing_reminder(convocation_id)
446 .await?;
447
448 let mut updated_recipients = Vec::new();
449
450 for mut recipient in recipients {
451 recipient.mark_reminder_sent()?;
452 let updated = self.recipient_repository.update(&recipient).await?;
453 updated_recipients.push(ConvocationRecipientResponse::from(updated));
454 }
455
456 if !updated_recipients.is_empty() {
458 let mut convocation = self
459 .convocation_repository
460 .find_by_id(convocation_id)
461 .await?
462 .ok_or_else(|| format!("Convocation not found: {}", convocation_id))?;
463
464 convocation.mark_reminder_sent()?;
465 self.convocation_repository.update(&convocation).await?;
466 }
467
468 Ok(updated_recipients)
469 }
470
471 pub async fn get_tracking_summary(
473 &self,
474 convocation_id: Uuid,
475 ) -> Result<RecipientTrackingSummaryResponse, String> {
476 let summary = self
477 .recipient_repository
478 .get_tracking_summary(convocation_id)
479 .await?;
480
481 Ok(RecipientTrackingSummaryResponse::new(
482 summary.total_count,
483 summary.opened_count,
484 summary.will_attend_count,
485 summary.will_not_attend_count,
486 summary.attended_count,
487 summary.did_not_attend_count,
488 summary.pending_count,
489 summary.failed_email_count,
490 ))
491 }
492
493 pub async fn list_convocation_recipients(
495 &self,
496 convocation_id: Uuid,
497 ) -> Result<Vec<ConvocationRecipientResponse>, String> {
498 let recipients = self
499 .recipient_repository
500 .find_by_convocation(convocation_id)
501 .await?;
502
503 Ok(recipients
504 .into_iter()
505 .map(ConvocationRecipientResponse::from)
506 .collect())
507 }
508
509 pub async fn cancel_convocation(&self, id: Uuid) -> Result<ConvocationResponse, String> {
511 let mut convocation = self
512 .convocation_repository
513 .find_by_id(id)
514 .await?
515 .ok_or_else(|| format!("Convocation not found: {}", id))?;
516
517 convocation.cancel()?;
518
519 let updated = self.convocation_repository.update(&convocation).await?;
520
521 Ok(ConvocationResponse::from(updated))
522 }
523
524 pub async fn delete_convocation(&self, id: Uuid) -> Result<bool, String> {
526 self.convocation_repository.delete(id).await
527 }
528
529 pub async fn process_scheduled_convocations(&self) -> Result<Vec<ConvocationResponse>, String> {
532 let now = Utc::now();
533 let scheduled = self
534 .convocation_repository
535 .find_pending_scheduled(now)
536 .await?;
537
538 let mut sent = Vec::new();
539
540 for convocation in scheduled {
541 sent.push(ConvocationResponse::from(convocation));
544 }
545
546 Ok(sent)
547 }
548
549 pub async fn process_reminder_sending(&self) -> Result<Vec<ConvocationResponse>, String> {
552 let now = Utc::now();
553 let needing_reminder = self
554 .convocation_repository
555 .find_needing_reminder(now)
556 .await?;
557
558 let mut processed = Vec::new();
559
560 for convocation in needing_reminder {
561 self.send_reminders(convocation.id).await?;
563 processed.push(ConvocationResponse::from(convocation));
564 }
565
566 Ok(processed)
567 }
568
569 pub async fn schedule_second_convocation(
582 &self,
583 organization_id: Uuid,
584 building_id: Uuid,
585 first_meeting_id: Uuid,
586 new_meeting_id: Uuid,
587 new_meeting_date: chrono::DateTime<chrono::Utc>,
588 language: String,
589 created_by: Uuid,
590 ) -> Result<ConvocationResponse, String> {
591 let first_meeting = self
593 .meeting_repository
594 .find_by_id(first_meeting_id)
595 .await?
596 .ok_or_else(|| format!("First meeting not found: {}", first_meeting_id))?;
597
598 let building = self
599 .building_repository
600 .find_by_id(building_id)
601 .await?
602 .ok_or_else(|| "Immeuble introuvable".to_string())?;
603
604 let second_convocation = Convocation::new_second_convocation(
607 building.acp_id,
608 organization_id,
609 building_id,
610 new_meeting_id,
611 first_meeting_id,
612 first_meeting.scheduled_date,
613 new_meeting_date,
614 language,
615 created_by,
616 )?;
617
618 let created = self
619 .convocation_repository
620 .create(&second_convocation)
621 .await?;
622
623 Ok(ConvocationResponse::from(created))
624 }
625
626 async fn update_convocation_tracking(&self, convocation_id: Uuid) -> Result<(), String> {
628 let summary = self
629 .recipient_repository
630 .get_tracking_summary(convocation_id)
631 .await?;
632
633 let mut convocation = self
634 .convocation_repository
635 .find_by_id(convocation_id)
636 .await?
637 .ok_or_else(|| format!("Convocation not found: {}", convocation_id))?;
638
639 convocation.update_tracking_counts(
640 summary.opened_count as i32,
641 summary.will_attend_count as i32,
642 summary.will_not_attend_count as i32,
643 );
644
645 self.convocation_repository.update(&convocation).await?;
646
647 Ok(())
648 }
649}
650
651#[cfg(test)]
652mod tests {
653 use super::*;
654 use crate::application::dto::{BuildingFilters, OwnerFilters, PageRequest};
655 use crate::application::ports::{
656 ConvocationRecipientRepository, ConvocationRepository, RecipientTrackingSummary,
657 };
658 use crate::domain::entities::{
659 AttendanceStatus, Building, Convocation, ConvocationRecipient, ConvocationStatus,
660 ConvocationType, Meeting, Owner,
661 };
662 use async_trait::async_trait;
663 use chrono::{Duration, Utc};
664 use mockall::mock;
665 use std::sync::Arc;
666 use uuid::Uuid;
667
668 mock! {
673 ConvRepo {}
674
675 #[async_trait]
676 impl ConvocationRepository for ConvRepo {
677 async fn create(&self, convocation: &Convocation) -> Result<Convocation, String>;
678 async fn find_by_id(&self, id: Uuid) -> Result<Option<Convocation>, String>;
679 async fn find_by_meeting_id(&self, meeting_id: Uuid) -> Result<Option<Convocation>, String>;
680 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Convocation>, String>;
681 async fn find_by_organization(&self, organization_id: Uuid) -> Result<Vec<Convocation>, String>;
682 async fn find_by_status(&self, organization_id: Uuid, status: ConvocationStatus) -> Result<Vec<Convocation>, String>;
683 async fn find_pending_scheduled(&self, now: chrono::DateTime<Utc>) -> Result<Vec<Convocation>, String>;
684 async fn find_needing_reminder(&self, now: chrono::DateTime<Utc>) -> Result<Vec<Convocation>, String>;
685 async fn update(&self, convocation: &Convocation) -> Result<Convocation, String>;
686 async fn delete(&self, id: Uuid) -> Result<bool, String>;
687 async fn count_by_building(&self, building_id: Uuid) -> Result<i64, String>;
688 async fn count_by_status(&self, organization_id: Uuid, status: ConvocationStatus) -> Result<i64, String>;
689 }
690 }
691
692 mock! {
693 RecipientRepo {}
694
695 #[async_trait]
696 impl ConvocationRecipientRepository for RecipientRepo {
697 async fn create(&self, recipient: &ConvocationRecipient) -> Result<ConvocationRecipient, String>;
698 async fn create_many(&self, recipients: &[ConvocationRecipient]) -> Result<Vec<ConvocationRecipient>, String>;
699 async fn find_by_id(&self, id: Uuid) -> Result<Option<ConvocationRecipient>, String>;
700 async fn find_by_convocation(&self, convocation_id: Uuid) -> Result<Vec<ConvocationRecipient>, String>;
701 async fn find_by_convocation_and_owner(&self, convocation_id: Uuid, owner_id: Uuid) -> Result<Option<ConvocationRecipient>, String>;
702 async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<ConvocationRecipient>, String>;
703 async fn find_by_attendance_status(&self, convocation_id: Uuid, status: AttendanceStatus) -> Result<Vec<ConvocationRecipient>, String>;
704 async fn find_needing_reminder(&self, convocation_id: Uuid) -> Result<Vec<ConvocationRecipient>, String>;
705 async fn find_failed_emails(&self, convocation_id: Uuid) -> Result<Vec<ConvocationRecipient>, String>;
706 async fn update(&self, recipient: &ConvocationRecipient) -> Result<ConvocationRecipient, String>;
707 async fn delete(&self, id: Uuid) -> Result<bool, String>;
708 async fn count_by_convocation(&self, convocation_id: Uuid) -> Result<i64, String>;
709 async fn count_opened(&self, convocation_id: Uuid) -> Result<i64, String>;
710 async fn count_by_attendance_status(&self, convocation_id: Uuid, status: AttendanceStatus) -> Result<i64, String>;
711 async fn get_tracking_summary(&self, convocation_id: Uuid) -> Result<RecipientTrackingSummary, String>;
712 }
713 }
714
715 mock! {
716 OwnerRepo {}
717
718 #[async_trait]
719 impl OwnerRepository for OwnerRepo {
720 async fn create(&self, owner: &Owner) -> Result<Owner, String>;
721 async fn find_by_id(&self, id: Uuid) -> Result<Option<Owner>, String>;
722 async fn find_by_user_id(&self, user_id: Uuid) -> Result<Option<Owner>, String>;
723 async fn find_by_user_id_and_organization(&self, user_id: Uuid, organization_id: Uuid) -> Result<Option<Owner>, String>;
724 async fn find_by_email(&self, email: &str) -> Result<Option<Owner>, String>;
725 async fn find_all(&self) -> Result<Vec<Owner>, String>;
726 async fn find_all_paginated(&self, page_request: &PageRequest, filters: &OwnerFilters) -> Result<(Vec<Owner>, i64), String>;
727 async fn update(&self, owner: &Owner) -> Result<Owner, String>;
728 async fn delete(&self, id: Uuid) -> Result<bool, String>;
729 async fn set_user_link(&self, owner_id: Uuid, user_id: Option<Uuid>) -> Result<bool, String>;
730 }
731 }
732
733 mock! {
734 BuildingRepo {}
735
736 #[async_trait]
737 impl BuildingRepository for BuildingRepo {
738 async fn create(&self, building: &Building) -> Result<Building, String>;
739 async fn find_by_id(&self, id: Uuid) -> Result<Option<Building>, String>;
740 async fn find_all(&self) -> Result<Vec<Building>, String>;
741 async fn find_all_paginated(&self, page_request: &PageRequest, filters: &BuildingFilters) -> Result<(Vec<Building>, i64), String>;
742 async fn update(&self, building: &Building) -> Result<Building, String>;
743 async fn delete(&self, id: Uuid) -> Result<bool, String>;
744 async fn find_by_slug(&self, slug: &str) -> Result<Option<Building>, String>;
745 async fn find_by_id_with_metrics(
746 &self,
747 id: Uuid,
748 ) -> Result<Option<(Building, crate::domain::entities::BuildingMetrics)>, String>;
749 }
750 }
751
752 mock! {
753 UnitOwnerRepo {}
754
755 #[async_trait]
756 impl crate::application::ports::UnitOwnerRepository for UnitOwnerRepo {
757 async fn create(&self, unit_owner: &crate::domain::entities::UnitOwner) -> Result<crate::domain::entities::UnitOwner, String>;
758 async fn find_by_id(&self, id: Uuid) -> Result<Option<crate::domain::entities::UnitOwner>, String>;
759 async fn find_current_owners_by_unit(&self, unit_id: Uuid) -> Result<Vec<crate::domain::entities::UnitOwner>, String>;
760 async fn find_current_units_by_owner(&self, owner_id: Uuid) -> Result<Vec<crate::domain::entities::UnitOwner>, String>;
761 async fn find_all_owners_by_unit(&self, unit_id: Uuid) -> Result<Vec<crate::domain::entities::UnitOwner>, String>;
762 async fn find_all_units_by_owner(&self, owner_id: Uuid) -> Result<Vec<crate::domain::entities::UnitOwner>, String>;
763 async fn update(&self, unit_owner: &crate::domain::entities::UnitOwner) -> Result<crate::domain::entities::UnitOwner, String>;
764 async fn delete(&self, id: Uuid) -> Result<(), String>;
765 async fn has_active_owners(&self, unit_id: Uuid) -> Result<bool, String>;
766 async fn get_total_ownership_percentage(&self, unit_id: Uuid) -> Result<rust_decimal::Decimal, String>;
767 async fn find_active_by_unit_and_owner(&self, unit_id: Uuid, owner_id: Uuid) -> Result<Option<crate::domain::entities::UnitOwner>, String>;
768 async fn find_active_by_building(&self, building_id: Uuid) -> Result<Vec<(Uuid, Uuid, rust_decimal::Decimal)>, String>;
769 async fn find_active_quota_shares_by_building(&self, building_id: Uuid) -> Result<Vec<(Uuid, Uuid, rust_decimal::Decimal)>, String>;
770 async fn find_voting_holders_by_unit(&self, unit_id: Uuid) -> Result<Vec<crate::domain::copropriete::LotHolder>, String>;
771 async fn is_voting_representative(&self, unit_owner_id: Uuid) -> Result<bool, String>;
772 async fn set_voting_representative(&self, unit_owner_id: Uuid) -> Result<(), String>;
773 }
774 }
775
776 mock! {
777 UserRepo {}
778
779 #[async_trait]
780 impl UserRepository for UserRepo {
781 async fn create(&self, user: &crate::domain::plateforme::user::User) -> Result<crate::domain::plateforme::user::User, String>;
782 async fn find_by_id(&self, id: Uuid) -> Result<Option<crate::domain::plateforme::user::User>, String>;
783 async fn find_by_email(&self, email: &str) -> Result<Option<crate::domain::plateforme::user::User>, String>;
784 async fn find_all(&self) -> Result<Vec<crate::domain::plateforme::user::User>, String>;
785 async fn find_page(
786 &self,
787 recherche: Option<String>,
788 role: Option<String>,
789 limit: i64,
790 offset: i64,
791 ) -> Result<Vec<crate::domain::plateforme::user::User>, String>;
792 async fn count_matching(
793 &self,
794 recherche: Option<String>,
795 role: Option<String>,
796 ) -> Result<i64, String>;
797 async fn find_by_organization(&self, org_id: Uuid) -> Result<Vec<crate::domain::plateforme::user::User>, String>;
798 async fn update(&self, user: &crate::domain::plateforme::user::User) -> Result<crate::domain::plateforme::user::User, String>;
799 async fn update_password(&self, id: Uuid, password_hash: &str) -> Result<bool, String>;
800 async fn activate(&self, id: Uuid) -> Result<Option<crate::domain::plateforme::user::User>, String>;
801 async fn deactivate(&self, id: Uuid) -> Result<Option<crate::domain::plateforme::user::User>, String>;
802 async fn delete(&self, id: Uuid) -> Result<bool, String>;
803 async fn count_by_organization(&self, org_id: Uuid) -> Result<i64, String>;
804 }
805 }
806
807 mock! {
808 MeetingRepo {}
809
810 #[async_trait]
811 impl MeetingRepository for MeetingRepo {
812 async fn create(&self, meeting: &Meeting) -> Result<Meeting, String>;
813 async fn find_by_id(&self, id: Uuid) -> Result<Option<Meeting>, String>;
814 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Meeting>, String>;
815 async fn update(&self, meeting: &Meeting) -> Result<Meeting, String>;
816 async fn delete(&self, id: Uuid) -> Result<bool, String>;
817 async fn find_all_paginated(&self, page_request: &PageRequest, organization_id: Option<Uuid>) -> Result<(Vec<Meeting>, i64), String>;
818 }
819 }
820
821 fn make_use_cases(
827 conv_repo: MockConvRepo,
828 recip_repo: MockRecipientRepo,
829 owner_repo: MockOwnerRepo,
830 mut building_repo: MockBuildingRepo,
831 meeting_repo: MockMeetingRepo,
832 ) -> ConvocationUseCases {
833 building_repo.expect_find_by_id().returning(|_| {
838 Ok(Some(
839 Building::new(
840 Uuid::new_v4(),
841 "Résidence du Parc".to_string(),
842 "12 Rue de la Loi".to_string(),
843 "Brussels".to_string(),
844 "1000".to_string(),
845 "Belgium".to_string(),
846 10,
847 1000,
848 Some(2015),
849 )
850 .expect("immeuble valide"),
851 ))
852 });
853 let mut unit_owner_repo = MockUnitOwnerRepo::new();
856 unit_owner_repo
857 .expect_find_active_by_building()
858 .returning(|_| {
859 Ok(vec![
860 (
861 Uuid::new_v4(),
862 Uuid::new_v4(),
863 rust_decimal::Decimal::from(500),
864 ),
865 (
866 Uuid::new_v4(),
867 Uuid::new_v4(),
868 rust_decimal::Decimal::from(500),
869 ),
870 ])
871 });
872
873 let mut user_repo = MockUserRepo::new();
878 user_repo.expect_find_by_id().returning(|_| Ok(None));
879
880 ConvocationUseCases::new(
881 Arc::new(conv_repo),
882 Arc::new(recip_repo),
883 Arc::new(owner_repo),
884 Arc::new(building_repo),
885 Arc::new(meeting_repo),
886 Arc::new(unit_owner_repo),
887 Arc::new(user_repo),
888 )
889 }
890
891 fn make_convocation(org_id: Uuid, building_id: Uuid, meeting_id: Uuid) -> Convocation {
893 let meeting_date = Utc::now() + Duration::days(20);
894 Convocation::new(
895 Uuid::new_v4(), org_id,
897 building_id,
898 meeting_id,
899 ConvocationType::Ordinary,
900 meeting_date,
901 "FR".to_string(),
902 Uuid::new_v4(),
903 )
904 .expect("helper should produce a valid convocation")
905 }
906
907 fn make_sent_convocation(org_id: Uuid, building_id: Uuid, meeting_id: Uuid) -> Convocation {
909 let mut conv = make_convocation(org_id, building_id, meeting_id);
910 conv.mark_sent("/tmp/conv.pdf".to_string(), 5).unwrap();
911 conv
912 }
913
914 fn make_use_cases_mandataire(
923 recip_repo: MockRecipientRepo,
924 proxy_owner_id: Uuid,
925 role: crate::domain::plateforme::user::UserRole,
926 ) -> ConvocationUseCases {
927 let utilisateur = crate::domain::plateforme::user::User::new(
928 "mandataire@example.be".to_string(),
929 "hash".to_string(),
930 "Marcel".to_string(),
931 "Devos".to_string(),
932 role,
933 Some(Uuid::new_v4()),
934 )
935 .expect("utilisateur valide");
936 let user_id = utilisateur.id;
937
938 let mut mandataire = Owner::new(
939 Uuid::new_v4(),
940 "Marcel".to_string(),
941 "Devos".to_string(),
942 "mandataire@example.be".to_string(),
943 None,
944 "12 Rue de la Loi".to_string(),
945 "Bruxelles".to_string(),
946 "1000".to_string(),
947 "Belgium".to_string(),
948 )
949 .expect("copropriétaire valide");
950 mandataire.id = proxy_owner_id;
951 mandataire.user_id = Some(user_id);
952
953 let mut owner_repo = MockOwnerRepo::new();
954 owner_repo
955 .expect_find_by_id()
956 .returning(move |_| Ok(Some(mandataire.clone())));
957
958 let mut user_repo = MockUserRepo::new();
959 user_repo
960 .expect_find_by_id()
961 .returning(move |_| Ok(Some(utilisateur.clone())));
962
963 let mut building_repo = MockBuildingRepo::new();
964 building_repo.expect_find_by_id().returning(|_| Ok(None));
965
966 ConvocationUseCases::new(
967 Arc::new(MockConvRepo::new()),
968 Arc::new(recip_repo),
969 Arc::new(owner_repo),
970 Arc::new(building_repo),
971 Arc::new(MockMeetingRepo::new()),
972 Arc::new(MockUnitOwnerRepo::new()),
973 Arc::new(user_repo),
974 )
975 }
976
977 fn make_recipient(convocation_id: Uuid, owner_id: Uuid) -> ConvocationRecipient {
978 let mut r =
979 ConvocationRecipient::new(convocation_id, owner_id, "owner@example.com".to_string())
980 .unwrap();
981 r.mark_email_sent();
982 r
983 }
984
985 #[tokio::test]
989 async fn test_create_convocation_ordinary_valid_deadline() {
990 let org_id = Uuid::new_v4();
991 let building_id = Uuid::new_v4();
992 let meeting_id = Uuid::new_v4();
993 let meeting_date = Utc::now() + Duration::days(20);
994
995 let mut conv_repo = MockConvRepo::new();
996 conv_repo.expect_create().returning(|conv| Ok(conv.clone()));
997
998 let uc = make_use_cases(
999 conv_repo,
1000 MockRecipientRepo::new(),
1001 MockOwnerRepo::new(),
1002 MockBuildingRepo::new(),
1003 MockMeetingRepo::new(),
1004 );
1005
1006 let request = CreateConvocationRequest {
1007 building_id,
1008 meeting_id,
1009 meeting_type: ConvocationType::Ordinary,
1010 meeting_date,
1011 language: "FR".to_string(),
1012 };
1013
1014 let result = uc.create_convocation(org_id, request, Uuid::new_v4()).await;
1015
1016 assert!(result.is_ok(), "Expected Ok, got: {:?}", result.err());
1017 let resp = result.unwrap();
1018 assert_eq!(resp.status, ConvocationStatus::Draft);
1019 assert_eq!(resp.language, "FR");
1020 assert!(resp.respects_legal_deadline);
1021 }
1022
1023 #[tokio::test]
1028 async fn test_create_convocation_violating_legal_deadline() {
1029 let org_id = Uuid::new_v4();
1030 let meeting_date = Utc::now() + Duration::days(5); let uc = make_use_cases(
1033 MockConvRepo::new(),
1034 MockRecipientRepo::new(),
1035 MockOwnerRepo::new(),
1036 MockBuildingRepo::new(),
1037 MockMeetingRepo::new(),
1038 );
1039
1040 let request = CreateConvocationRequest {
1041 building_id: Uuid::new_v4(),
1042 meeting_id: Uuid::new_v4(),
1043 meeting_type: ConvocationType::Ordinary,
1044 meeting_date,
1045 language: "FR".to_string(),
1046 };
1047
1048 let result = uc.create_convocation(org_id, request, Uuid::new_v4()).await;
1049
1050 assert!(result.is_err());
1051 let err = result.unwrap_err();
1052 assert!(
1053 err.contains("3.87"),
1054 "le refus doit citer l'article qui le fonde, reçu : {err}"
1055 );
1056 }
1057
1058 #[tokio::test]
1063 async fn test_create_convocation_extraordinary_valid_deadline() {
1064 let org_id = Uuid::new_v4();
1065 let meeting_date = Utc::now() + Duration::days(16); let mut conv_repo = MockConvRepo::new();
1068 conv_repo.expect_create().returning(|conv| Ok(conv.clone()));
1069
1070 let uc = make_use_cases(
1071 conv_repo,
1072 MockRecipientRepo::new(),
1073 MockOwnerRepo::new(),
1074 MockBuildingRepo::new(),
1075 MockMeetingRepo::new(),
1076 );
1077
1078 let request = CreateConvocationRequest {
1079 building_id: Uuid::new_v4(),
1080 meeting_id: Uuid::new_v4(),
1081 meeting_type: ConvocationType::Extraordinary,
1082 meeting_date,
1083 language: "NL".to_string(),
1084 };
1085
1086 let result = uc.create_convocation(org_id, request, Uuid::new_v4()).await;
1087
1088 assert!(result.is_ok());
1089 let resp = result.unwrap();
1090 assert_eq!(resp.language, "NL");
1091 }
1092
1093 #[tokio::test]
1097 async fn test_schedule_convocation_success() {
1098 let org_id = Uuid::new_v4();
1099 let building_id = Uuid::new_v4();
1100 let meeting_id = Uuid::new_v4();
1101 let conv = make_convocation(org_id, building_id, meeting_id);
1102 let conv_id = conv.id;
1103 let min_send_date = conv.minimum_send_date;
1104
1105 let mut conv_repo = MockConvRepo::new();
1106 let conv_clone = conv.clone();
1107 conv_repo
1108 .expect_find_by_id()
1109 .returning(move |_| Ok(Some(conv_clone.clone())));
1110 conv_repo.expect_update().returning(|conv| Ok(conv.clone()));
1111
1112 let uc = make_use_cases(
1113 conv_repo,
1114 MockRecipientRepo::new(),
1115 MockOwnerRepo::new(),
1116 MockBuildingRepo::new(),
1117 MockMeetingRepo::new(),
1118 );
1119
1120 let send_date = min_send_date - Duration::days(1);
1122 let request = ScheduleConvocationRequest { send_date };
1123
1124 let result = uc.schedule_convocation(conv_id, request).await;
1125
1126 assert!(result.is_ok(), "Expected Ok, got: {:?}", result.err());
1127 let resp = result.unwrap();
1128 assert_eq!(resp.status, ConvocationStatus::Scheduled);
1129 assert!(resp.scheduled_send_date.is_some());
1130 }
1131
1132 #[tokio::test]
1136 async fn test_cancel_convocation_success() {
1137 let org_id = Uuid::new_v4();
1138 let building_id = Uuid::new_v4();
1139 let meeting_id = Uuid::new_v4();
1140 let conv = make_convocation(org_id, building_id, meeting_id);
1141 let conv_id = conv.id;
1142
1143 let mut conv_repo = MockConvRepo::new();
1144 let conv_clone = conv.clone();
1145 conv_repo
1146 .expect_find_by_id()
1147 .returning(move |_| Ok(Some(conv_clone.clone())));
1148 conv_repo.expect_update().returning(|conv| Ok(conv.clone()));
1149
1150 let uc = make_use_cases(
1151 conv_repo,
1152 MockRecipientRepo::new(),
1153 MockOwnerRepo::new(),
1154 MockBuildingRepo::new(),
1155 MockMeetingRepo::new(),
1156 );
1157
1158 let result = uc.cancel_convocation(conv_id).await;
1159
1160 assert!(result.is_ok());
1161 let resp = result.unwrap();
1162 assert_eq!(resp.status, ConvocationStatus::Cancelled);
1163 }
1164
1165 #[tokio::test]
1169 async fn test_cancel_convocation_already_cancelled_error() {
1170 let org_id = Uuid::new_v4();
1171 let building_id = Uuid::new_v4();
1172 let meeting_id = Uuid::new_v4();
1173 let mut conv = make_convocation(org_id, building_id, meeting_id);
1174 conv.cancel().unwrap(); let conv_id = conv.id;
1176
1177 let mut conv_repo = MockConvRepo::new();
1178 let conv_clone = conv.clone();
1179 conv_repo
1180 .expect_find_by_id()
1181 .returning(move |_| Ok(Some(conv_clone.clone())));
1182
1183 let uc = make_use_cases(
1184 conv_repo,
1185 MockRecipientRepo::new(),
1186 MockOwnerRepo::new(),
1187 MockBuildingRepo::new(),
1188 MockMeetingRepo::new(),
1189 );
1190
1191 let result = uc.cancel_convocation(conv_id).await;
1192
1193 assert!(result.is_err());
1194 assert!(result.unwrap_err().contains("already cancelled"));
1195 }
1196
1197 #[tokio::test]
1201 async fn test_send_reminders_marks_recipients() {
1202 let org_id = Uuid::new_v4();
1203 let building_id = Uuid::new_v4();
1204 let meeting_id = Uuid::new_v4();
1205 let conv = make_sent_convocation(org_id, building_id, meeting_id);
1206 let conv_id = conv.id;
1207
1208 let owner1_id = Uuid::new_v4();
1209 let owner2_id = Uuid::new_v4();
1210 let r1 = make_recipient(conv_id, owner1_id);
1211 let r2 = make_recipient(conv_id, owner2_id);
1212
1213 let mut recip_repo = MockRecipientRepo::new();
1214 let r1_clone = r1.clone();
1215 let r2_clone = r2.clone();
1216 recip_repo
1217 .expect_find_needing_reminder()
1218 .returning(move |_| Ok(vec![r1_clone.clone(), r2_clone.clone()]));
1219 recip_repo.expect_update().returning(|r| Ok(r.clone()));
1220
1221 let mut conv_repo = MockConvRepo::new();
1222 let conv_clone = conv.clone();
1223 conv_repo
1224 .expect_find_by_id()
1225 .returning(move |_| Ok(Some(conv_clone.clone())));
1226 conv_repo.expect_update().returning(|conv| Ok(conv.clone()));
1227
1228 let uc = make_use_cases(
1229 conv_repo,
1230 recip_repo,
1231 MockOwnerRepo::new(),
1232 MockBuildingRepo::new(),
1233 MockMeetingRepo::new(),
1234 );
1235
1236 let result = uc.send_reminders(conv_id).await;
1237
1238 assert!(result.is_ok(), "Expected Ok, got: {:?}", result.err());
1239 let recipients = result.unwrap();
1240 assert_eq!(recipients.len(), 2);
1241 assert!(recipients[0].reminder_sent_at.is_some());
1243 assert!(recipients[1].reminder_sent_at.is_some());
1244 }
1245
1246 #[tokio::test]
1250 async fn test_mark_email_opened_updates_tracking() {
1251 let org_id = Uuid::new_v4();
1252 let building_id = Uuid::new_v4();
1253 let meeting_id = Uuid::new_v4();
1254 let conv = make_sent_convocation(org_id, building_id, meeting_id);
1255 let conv_id = conv.id;
1256
1257 let owner_id = Uuid::new_v4();
1258 let recipient = make_recipient(conv_id, owner_id);
1259 let recipient_id = recipient.id;
1260
1261 let mut recip_repo = MockRecipientRepo::new();
1262 let recip_clone = recipient.clone();
1263 recip_repo
1264 .expect_find_by_id()
1265 .returning(move |_| Ok(Some(recip_clone.clone())));
1266 recip_repo.expect_update().returning(|r| Ok(r.clone()));
1267 recip_repo
1268 .expect_get_tracking_summary()
1269 .returning(move |_| {
1270 Ok(RecipientTrackingSummary {
1271 total_count: 5,
1272 opened_count: 3,
1273 will_attend_count: 2,
1274 will_not_attend_count: 1,
1275 attended_count: 0,
1276 did_not_attend_count: 0,
1277 pending_count: 2,
1278 failed_email_count: 0,
1279 })
1280 });
1281
1282 let mut conv_repo = MockConvRepo::new();
1283 let conv_clone = conv.clone();
1284 conv_repo
1285 .expect_find_by_id()
1286 .returning(move |_| Ok(Some(conv_clone.clone())));
1287 conv_repo.expect_update().returning(|conv| Ok(conv.clone()));
1288
1289 let uc = make_use_cases(
1290 conv_repo,
1291 recip_repo,
1292 MockOwnerRepo::new(),
1293 MockBuildingRepo::new(),
1294 MockMeetingRepo::new(),
1295 );
1296
1297 let result = uc.mark_recipient_email_opened(recipient_id).await;
1298
1299 assert!(result.is_ok(), "Expected Ok, got: {:?}", result.err());
1300 let resp = result.unwrap();
1301 assert!(resp.has_opened_email);
1302 }
1303
1304 #[tokio::test]
1308 async fn test_update_attendance_will_attend() {
1309 let org_id = Uuid::new_v4();
1310 let building_id = Uuid::new_v4();
1311 let meeting_id = Uuid::new_v4();
1312 let conv = make_sent_convocation(org_id, building_id, meeting_id);
1313 let conv_id = conv.id;
1314
1315 let owner_id = Uuid::new_v4();
1316 let recipient = make_recipient(conv_id, owner_id);
1317 let recipient_id = recipient.id;
1318
1319 let mut recip_repo = MockRecipientRepo::new();
1320 let recip_clone = recipient.clone();
1321 recip_repo
1322 .expect_find_by_id()
1323 .returning(move |_| Ok(Some(recip_clone.clone())));
1324 recip_repo.expect_update().returning(|r| Ok(r.clone()));
1325 recip_repo
1326 .expect_get_tracking_summary()
1327 .returning(move |_| {
1328 Ok(RecipientTrackingSummary {
1329 total_count: 5,
1330 opened_count: 1,
1331 will_attend_count: 1,
1332 will_not_attend_count: 0,
1333 attended_count: 0,
1334 did_not_attend_count: 0,
1335 pending_count: 4,
1336 failed_email_count: 0,
1337 })
1338 });
1339
1340 let mut conv_repo = MockConvRepo::new();
1341 let conv_clone = conv.clone();
1342 conv_repo
1343 .expect_find_by_id()
1344 .returning(move |_| Ok(Some(conv_clone.clone())));
1345 conv_repo.expect_update().returning(|conv| Ok(conv.clone()));
1346
1347 let uc = make_use_cases(
1348 conv_repo,
1349 recip_repo,
1350 MockOwnerRepo::new(),
1351 MockBuildingRepo::new(),
1352 MockMeetingRepo::new(),
1353 );
1354
1355 let result = uc
1356 .update_recipient_attendance(recipient_id, AttendanceStatus::WillAttend)
1357 .await;
1358
1359 assert!(result.is_ok(), "Expected Ok, got: {:?}", result.err());
1360 let resp = result.unwrap();
1361 assert_eq!(resp.attendance_status, AttendanceStatus::WillAttend);
1362 assert!(resp.has_confirmed_attendance);
1363 }
1364
1365 #[tokio::test]
1369 async fn test_set_proxy_delegation() {
1370 let conv_id = Uuid::new_v4();
1371 let owner_id = Uuid::new_v4();
1372 let proxy_owner_id = Uuid::new_v4();
1373 let recipient = make_recipient(conv_id, owner_id);
1374 let recipient_id = recipient.id;
1375
1376 let mut recip_repo = MockRecipientRepo::new();
1377 let recip_clone = recipient.clone();
1378 recip_repo
1379 .expect_find_by_id()
1380 .returning(move |_| Ok(Some(recip_clone.clone())));
1381 recip_repo.expect_update().returning(|r| Ok(r.clone()));
1382
1383 let uc = make_use_cases_mandataire(
1384 recip_repo,
1385 proxy_owner_id,
1386 crate::domain::plateforme::user::UserRole::Owner,
1387 );
1388
1389 let result = uc.set_recipient_proxy(recipient_id, proxy_owner_id).await;
1390
1391 assert!(result.is_ok(), "Expected Ok, got: {:?}", result.err());
1392 let resp = result.unwrap();
1393 assert_eq!(resp.proxy_owner_id, Some(proxy_owner_id));
1394 }
1395
1396 #[tokio::test]
1400 async fn test_set_proxy_to_self_error() {
1401 let conv_id = Uuid::new_v4();
1402 let owner_id = Uuid::new_v4();
1403 let recipient = make_recipient(conv_id, owner_id);
1404 let recipient_id = recipient.id;
1405 let self_owner_id = recipient.owner_id;
1406
1407 let mut recip_repo = MockRecipientRepo::new();
1408 let recip_clone = recipient.clone();
1409 recip_repo
1410 .expect_find_by_id()
1411 .returning(move |_| Ok(Some(recip_clone.clone())));
1412
1413 let uc = make_use_cases_mandataire(
1414 recip_repo,
1415 self_owner_id,
1416 crate::domain::plateforme::user::UserRole::Owner,
1417 );
1418
1419 let result = uc.set_recipient_proxy(recipient_id, self_owner_id).await;
1420
1421 assert!(result.is_err());
1422 assert!(result.unwrap_err().contains("Cannot delegate to self"));
1423 }
1424
1425 #[tokio::test]
1436 async fn le_syndic_ne_peut_pas_etre_mandataire() {
1437 let conv_id = Uuid::new_v4();
1438 let owner_id = Uuid::new_v4();
1439 let syndic_owner_id = Uuid::new_v4();
1440 let recipient = make_recipient(conv_id, owner_id);
1441 let recipient_id = recipient.id;
1442
1443 let mut recip_repo = MockRecipientRepo::new();
1444 let recip_clone = recipient.clone();
1445 recip_repo
1446 .expect_find_by_id()
1447 .returning(move |_| Ok(Some(recip_clone.clone())));
1448 let uc = make_use_cases_mandataire(
1452 recip_repo,
1453 syndic_owner_id,
1454 crate::domain::plateforme::user::UserRole::Syndic,
1455 );
1456
1457 let result = uc.set_recipient_proxy(recipient_id, syndic_owner_id).await;
1458
1459 let erreur = result.expect_err("le mandat au syndic doit être refusé");
1460 assert!(
1461 erreur.contains("3.87"),
1462 "le message doit citer l'article qui fonde le refus, reçu : {erreur}"
1463 );
1464 }
1465
1466 #[tokio::test]
1470 async fn un_coproprietaire_ordinaire_reste_un_mandataire_valable() {
1471 let conv_id = Uuid::new_v4();
1472 let owner_id = Uuid::new_v4();
1473 let proxy_owner_id = Uuid::new_v4();
1474 let recipient = make_recipient(conv_id, owner_id);
1475 let recipient_id = recipient.id;
1476
1477 let mut recip_repo = MockRecipientRepo::new();
1478 let recip_clone = recipient.clone();
1479 recip_repo
1480 .expect_find_by_id()
1481 .returning(move |_| Ok(Some(recip_clone.clone())));
1482 recip_repo.expect_update().returning(|r| Ok(r.clone()));
1483
1484 let uc = make_use_cases_mandataire(
1485 recip_repo,
1486 proxy_owner_id,
1487 crate::domain::plateforme::user::UserRole::Owner,
1488 );
1489
1490 let result = uc.set_recipient_proxy(recipient_id, proxy_owner_id).await;
1491
1492 assert!(result.is_ok(), "reçu : {:?}", result.err());
1493 }
1494
1495 fn make_owner(id: Uuid, first_name: &str, last_name: &str, email: &str) -> Owner {
1504 Owner {
1505 id,
1506 organization_id: Uuid::new_v4(),
1507 user_id: None,
1508 first_name: first_name.to_string(),
1509 last_name: last_name.to_string(),
1510 email: email.to_string(),
1511 phone: None,
1512 address: "1 Rue du Test".to_string(),
1513 city: "Bruxelles".to_string(),
1514 postal_code: "1000".to_string(),
1515 country: "Belgium".to_string(),
1516 created_at: Utc::now(),
1517 updated_at: Utc::now(),
1518 }
1519 }
1520
1521 #[tokio::test]
1525 async fn happy_list_eligible_recipients_dedup_et_trie_par_nom() {
1526 let building_id = Uuid::new_v4();
1527 let zoe_id = Uuid::new_v4();
1528 let adam_id = Uuid::new_v4();
1529
1530 let mut unit_owner_repo = MockUnitOwnerRepo::new();
1531 unit_owner_repo
1532 .expect_find_active_by_building()
1533 .returning(move |_| {
1534 Ok(vec![
1535 (Uuid::new_v4(), zoe_id, rust_decimal::Decimal::from(500)),
1536 (Uuid::new_v4(), adam_id, rust_decimal::Decimal::from(300)),
1537 (Uuid::new_v4(), zoe_id, rust_decimal::Decimal::from(200)),
1539 ])
1540 });
1541
1542 let mut owner_repo = MockOwnerRepo::new();
1543 owner_repo.expect_find_by_id().returning(move |id| {
1544 if id == zoe_id {
1545 Ok(Some(make_owner(zoe_id, "Zoé", "Dupont", "zoe@example.be")))
1546 } else if id == adam_id {
1547 Ok(Some(make_owner(
1548 adam_id,
1549 "Adam",
1550 "Peeters",
1551 "adam@example.be",
1552 )))
1553 } else {
1554 Ok(None)
1555 }
1556 });
1557
1558 let uc = ConvocationUseCases::new(
1559 Arc::new(MockConvRepo::new()),
1560 Arc::new(MockRecipientRepo::new()),
1561 Arc::new(owner_repo),
1562 Arc::new(MockBuildingRepo::new()),
1563 Arc::new(MockMeetingRepo::new()),
1564 Arc::new(unit_owner_repo),
1565 Arc::new(MockUserRepo::new()),
1566 );
1567
1568 let result = uc.list_eligible_recipients(building_id).await;
1569
1570 assert!(result.is_ok(), "reçu : {:?}", result.err());
1571 let destinataires = result.unwrap();
1572 assert_eq!(destinataires.len(), 2, "Zoé ne doit compter qu'une fois");
1573 assert_eq!(destinataires[0].full_name, "Adam Peeters");
1574 assert_eq!(destinataires[1].full_name, "Zoé Dupont");
1575 assert_eq!(destinataires[1].email, "zoe@example.be");
1576 }
1577
1578 #[tokio::test]
1582 async fn edge_list_eligible_recipients_immeuble_sans_lots_rend_liste_vide() {
1583 let mut unit_owner_repo = MockUnitOwnerRepo::new();
1584 unit_owner_repo
1585 .expect_find_active_by_building()
1586 .returning(|_| Ok(vec![]));
1587
1588 let uc = ConvocationUseCases::new(
1589 Arc::new(MockConvRepo::new()),
1590 Arc::new(MockRecipientRepo::new()),
1591 Arc::new(MockOwnerRepo::new()),
1592 Arc::new(MockBuildingRepo::new()),
1593 Arc::new(MockMeetingRepo::new()),
1594 Arc::new(unit_owner_repo),
1595 Arc::new(MockUserRepo::new()),
1596 );
1597
1598 let result = uc.list_eligible_recipients(Uuid::new_v4()).await;
1599
1600 assert!(result.is_ok(), "reçu : {:?}", result.err());
1601 assert!(result.unwrap().is_empty());
1602 }
1603
1604 #[tokio::test]
1608 async fn negative_list_eligible_recipients_fiche_coproprietaire_introuvable_est_une_erreur() {
1609 let owner_id = Uuid::new_v4();
1610 let mut unit_owner_repo = MockUnitOwnerRepo::new();
1611 unit_owner_repo
1612 .expect_find_active_by_building()
1613 .returning(move |_| {
1614 Ok(vec![(
1615 Uuid::new_v4(),
1616 owner_id,
1617 rust_decimal::Decimal::from(1000),
1618 )])
1619 });
1620
1621 let mut owner_repo = MockOwnerRepo::new();
1622 owner_repo.expect_find_by_id().returning(|_| Ok(None));
1623
1624 let uc = ConvocationUseCases::new(
1625 Arc::new(MockConvRepo::new()),
1626 Arc::new(MockRecipientRepo::new()),
1627 Arc::new(owner_repo),
1628 Arc::new(MockBuildingRepo::new()),
1629 Arc::new(MockMeetingRepo::new()),
1630 Arc::new(unit_owner_repo),
1631 Arc::new(MockUserRepo::new()),
1632 );
1633
1634 let result = uc.list_eligible_recipients(Uuid::new_v4()).await;
1635
1636 assert!(result.is_err());
1637 assert!(matches!(result.unwrap_err(), AppError::NotFound(_)));
1638 }
1639
1640 #[tokio::test]
1646 async fn negative_send_convocation_avec_selection_explicitement_vide_est_refuse() {
1647 let uc = make_use_cases(
1648 MockConvRepo::new(),
1649 MockRecipientRepo::new(),
1650 MockOwnerRepo::new(),
1651 MockBuildingRepo::new(),
1652 MockMeetingRepo::new(),
1653 );
1654
1655 let result = uc
1656 .send_convocation(
1657 Uuid::new_v4(),
1658 SendConvocationRequest {
1659 recipient_owner_ids: Some(vec![]),
1660 },
1661 )
1662 .await;
1663
1664 assert!(result.is_err());
1665 let erreur = result.unwrap_err();
1666 assert!(
1667 erreur.contains("destinataire"),
1668 "le refus doit nommer ce qui manque, reçu : {erreur}"
1669 );
1670 }
1671}